Skip to content

Give a conversation a name of its own, and draw it where the last message was - #318

Open
zopeVaibhav wants to merge 8 commits into
CopilotKit:mainfrom
zopeVaibhav:feat/channel-summary-titles
Open

Give a conversation a name of its own, and draw it where the last message was#318
zopeVaibhav wants to merge 8 commits into
CopilotKit:mainfrom
zopeVaibhav:feat/channel-summary-titles

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

What this changes

A conversation gets a name of its own, so a roster row says what it is about.

A channel's name is only the names of the Bots in it (channelName in routes.ts). Somebody who
asks one Bot about six unrelated things has six rows reading "Knowledge", told apart by a preview of
whatever was said last, which is usually the tail of an answer and says nothing about the question.
This writes the missing half: a few words naming the subject, drawn where the preview was, falling
back to that preview until a name exists. A row is never blank and never worse off than it is today.

How it works

Named once, from the opening exchange. The first thing asked and the first thing answered is
what a title is about, and it is what somebody scanning a roster is looking for. A conversation that
later wanders is still filed under what it was opened for, which is how every product that does this
behaves. summary_at records when the name was written, so a later change can decide whether to
revisit it.

On the work queue, not as a headless turn. Reusing routines/run-turn.ts would be the obvious
move, but it takes the Intelligence thread lock, and while that is held the person's own next
message in that thread is refused with 409 for up to the lock TTL. Naming a conversation is
housekeeping, and locking somebody out of the conversation being named is not a trade worth making.
So this reads the thread with getThreadMessages, which takes no lock.

Offer, then claim, the shape work/culler.ts already uses. The offer is derived from the table
rather than from an event, which is the part that matters: a missed event would mean a conversation
never named with no evidence anywhere that it should have been, where a missed sweep costs two
seconds.

One model call, not a Bot and not a turn. No streaming, no tools, no thread, using the provider
and key every other model call in the deployment already uses. Nothing new is configured to switch
it on, and a deployment with no key resolves to null and names nothing.

What a deployment should know

The opening exchange, up to 600 code points, is sent to whatever tenantPackage.model names. That is
the same provider the Bots already use, so it is not new egress, but it is sent as housekeeping
rather than because somebody asked for it.

The roster's second line now holds the name rather than the last message. That trades knowing what
was last said for knowing what the conversation is about.

Where it runs

  • New state that outlives a request? Yes, two columns on channels (summary, summary_at)
    and rows in the existing work_items. Both are in Postgres. Nothing is held in a process.
  • What happens on the second replica? Every replica offers and claims. claim filters
    where "kind" = ${kind}, so this consumer cannot see, claim or release handoff or routine
    work, and for update skip locked keeps two replicas off the same conversation. Two that both
    somehow reached the write are harmless: the update is conditional on summary is null, so the
    second changes no rows and announces nothing.
  • Anything serialised? The write is a conditional update, not a check-then-write. The claim
    is for update skip locked on work_items, the mechanism Run OpenBot on Kubernetes: Bots and all, proven on EKS #235 shipped for exactly this.
  • Anything fanned out to a browser? The name goes out as a channel_activity NOTIFY, the
    same path a message uses, so a socket held by another process gets it. It is its own event
    rather than folded into the activity that prompted it, because the sweep answers seconds later
    and that activity has long since been announced.
  • New listener, port, or schedule? No listener and no port. One new loop, in the API process
    rather than worker/, because the worker is not in the shipped single-image container and work
    only it runs would silently never happen there. It is repeatAfterEach, so it never overlaps
    itself, and a hundred copies collide on the same rows rather than duplicating the work.

Cost, and what stops it running away

One model call per conversation, once. A channel leaves channels_awaiting_summary_idx the moment it
is named, so it is never offered again. A failure releases with a delay rather than retrying hot and
gives up after maxAttempts, and the 24-hour purge window is the backoff before a fresh attempt, so
a deployment whose model is unreachable costs one attempt per conversation per day rather than a
loop. A conversation with nothing in it is never offered at all: the query requires
last_message_at is not null.

Boundary and audit

  • Every acting call still goes through the gateway. No acting path is touched: this is not a Bot
    and it takes no action on anybody's behalf.
  • New refusals and new failures each write a row. There is no refusal here. A failure releases
    the work item with its reason recorded on that row.
  • Nothing new is trusted from the client. The browser sends nothing. Every input is read from
    this deployment's own tables and its own Intelligence thread.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

Run against a local stack with a real model key. All 28 seeded conversations were named and the
roster drew the names in place of the previews. A conversation opened with a direct question was
named "Today's Biggest News" in the same pass that recorded the message.

Covered by channel-titler.test.ts (5), typed-reveal.test.ts (6), the summary cases in
channel-event-patch.test.ts, and channel-summary.integration.test.ts for the queue paths,
including a channel deleted between the offer and the claim, a thread Intelligence has not persisted
yet, and two replicas racing the same write. 222 app tests pass. bun run typecheck,
bun run lint and bunx biome format . are clean.

The lease-based integration tests around work_items are flaky on this machine independently of this
change: main fails the same family, and three runs of the same subset on this branch produced 8, 8
and 7 failures with no code change between them.

@zopeVaibhav
zopeVaibhav force-pushed the feat/channel-summary-titles branch from ba5509a to 089366c Compare September 1, 2026 20:45
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.

1 participant