Skip to content

Offer a conversation to be named only when the queue holds nothing for it - #587

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:summary-offer-settled
Sep 16, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:summary-offer-settled

Conversation

@kevin9327

@kevin9327 kevin9327 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What this changes

server/src/index.ts runs offerChannelsAwaitingSummary on every pass of its summary loop, 10 seconds after the last one ends. With no limit passed, it reads 20 channels with no summary, not deleted, and with a last message. It offers each one to the queue. There is no ORDER BY, so the planner's order decides which 20 it gets, and that order is the same on every pass.

A conversation the job cannot name keeps summary null and stays in that set:

  • the model answered with no text, so summariseOne returns "nothing to name it with" and the work row is finished;
  • the first user message has no words, such as a dropped screenshot, so openingOf returns null, it is "not yet" until the attempts run out, and the row gives up;
  • the transcript cannot be read, which throws until the row gives up.

While that finished or given-up row stands (an hour or a day, per forgetSettledSummaries), queue.offer is a no-op for that key. The query still returns the conversation, though, and it still takes one of the 20 places. Once enough of them sort ahead of a new conversation, the new one is never offered. Its sidebar entry keeps the plain channel name instead of a title. Conversations that cannot be named only pile up, because named ones leave the set and these do not.

The fix adds NOT EXISTS (work_items row for kind 'channel.summary' and this channel's id) to the offer query. The primary key (kind, key) answers that from its index. That covers:

  • pending: already queued, so offering it again did nothing anyway;
  • finished or given up: offering it again did nothing until forgetSettledSummaries deletes the row. After that it is offered again, exactly as today.

Nothing about claiming, naming, retries or purging changes. The only difference is which conversations fill the 20 places.

Where it runs

  • New state that outlives a request? None. The condition reads the work_items table the queue already keeps.
  • What happens on the second replica? The same query, with the same answer from the shared table. Two replicas offering the same conversation still collapse onto one row through (kind, key).
  • Anything serialised? Unchanged. offer is still ON CONFLICT DO NOTHING, and a row committed between this read and the offer is still a conflict.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Naming a conversation is not an acting call.
  • New refusals and new failures each write a row. None are added.
  • Nothing new is trusted from the client that the server can resolve itself. Nothing here is client input.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

I have no local PostgreSQL, so everything that touches the database ran on GitHub Actions in my fork. The job used the same pgvector/pgvector:pg17 service, bun install --frozen-lockfile, drizzle-kit migrate and Bun 1.3.14 as ci.yml.

Two cases added to server/tests/channel-summary.integration.test.ts:

  • does not spend a place on a conversation whose settled work still stands. Offer one used conversation, run a pass whose titler answers null, then call offerChannelsAwaitingSummary.
  • offers it again once its settled work has been forgotten. Delete the row, as forgetSettledSummaries does, and the conversation is offered again. This passes before and after, and guards the "as before".

With summary.ts from main (only the tests added):

error: expect(received).not.toContain(expected)
Expected to not contain: "channel_666de4ba-40a1-4e46-9ab1-1adc5417ade8"
Received: [ "channel_666de4ba-40a1-4e46-9ab1-1adc5417ade8" ]
(fail) offering conversations to be named > does not spend a place on a conversation whose settled work still stands
 16 pass
 1 fail

With the fix, bun test --coverage server/tests/channel-summary.integration.test.ts passes 17 of 17. The changed query runs, and the report's uncovered lines are all outside it.

To see the symptom rather than the mechanism, I ran a throwaway test (not part of this PR) in the same job. It creates 20 used conversations and settles them all with a titler that answers null. It then adds 5 new conversations and calls the production form, offerChannelsAwaitingSummary({ database, queue }), three times:

main:     pass 1: offered 20, of them settled 16, new 4 of 5
          pass 2: offered 20, of them settled 16, new 4 of 5
          pass 3: offered 20, of them settled 16, new 4 of 5
this PR:  pass 1: offered 5, of them settled 0, new 5 of 5

On main, 16 of the 20 places went to conversations that could not be named on every pass, and a new conversation was left out each time. With more conversations that cannot be named, more new ones are left out.

Locally: bun run typecheck (app, server, worker) exits 0, and bunx biome check on the changed files is clean.

This PR adds its entry at the top of ## Unreleased, where other open PRs add theirs, so the changelog may conflict. I can rebase if it does.

🤖 Generated with Claude Code

…r it

A pass offers at most twenty unnamed conversations. One the pass could not
name keeps no summary and stays in that set, and offering it again while
its settled work row stands does nothing but take a place. Enough of them
ahead of a new conversation left the new one unoffered on every pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-verified clean; CI green on this sha.

@davidmckayv
davidmckayv merged commit d2b7319 into CopilotKit:main Sep 16, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants