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
Open
Give a conversation a name of its own, and draw it where the last message was#318zopeVaibhav wants to merge 8 commits into
zopeVaibhav wants to merge 8 commits into
Conversation
zopeVaibhav
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
September 1, 2026 17:23
zopeVaibhav
force-pushed
the
feat/channel-summary-titles
branch
from
September 1, 2026 19:02
c30ccc6 to
ba5509a
Compare
zopeVaibhav
force-pushed
the
feat/channel-summary-titles
branch
from
September 1, 2026 20:45
ba5509a to
089366c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
A conversation gets a name of its own, so a roster row says what it is about.
A channel's
nameis only the names of the Bots in it (channelNamein routes.ts). Somebody whoasks 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_atrecords when the name was written, so a later change can decide whether torevisit it.
On the work queue, not as a headless turn. Reusing
routines/run-turn.tswould be the obviousmove, 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.tsalready uses. The offer is derived from the tablerather 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.modelnames. That isthe 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
channels(summary,summary_at)and rows in the existing
work_items. Both are in Postgres. Nothing is held in a process.claimfilterswhere "kind" = ${kind}, so this consumer cannot see, claim or release handoff or routinework, and
for update skip lockedkeeps two replicas off the same conversation. Two that bothsomehow reached the write are harmless: the update is conditional on
summary is null, so thesecond changes no rows and announces nothing.
is
for update skip lockedonwork_items, the mechanism Run OpenBot on Kubernetes: Bots and all, proven on EKS #235 shipped for exactly this.channel_activityNOTIFY, thesame 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.
rather than
worker/, because the worker is not in the shipped single-image container and workonly it runs would silently never happen there. It is
repeatAfterEach, so it never overlapsitself, 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_idxthe moment itis 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, soa 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
and it takes no action on anybody's behalf.
the work item with its reason recorded on that row.
this deployment's own tables and its own Intelligence thread.
Changelog
CHANGELOG.mdunderUnreleased.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 inchannel-event-patch.test.ts, andchannel-summary.integration.test.tsfor 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 lintandbunx biome format .are clean.The lease-based integration tests around
work_itemsare flaky on this machine independently of thischange:
mainfails the same family, and three runs of the same subset on this branch produced 8, 8and 7 failures with no code change between them.