Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### Reopening a channel no longer hides the end of the last conversation

Opening a channel joins the realtime gateway, and the snapshot the join returns can lag the durable
store. When it did, the last exchange of a finished turn was missing from the transcript on every
reload, with no unread marker or anything else to explain the gap, and the stored copy never got a
chance to replace it because history was only restored into an empty channel.

The stored thread now wins whenever it holds more than the channel does and holds everything the
channel already shows. A message typed while history is still loading is not in the store yet, and
a run still streaming has messages the store has not seen, so neither is rolled back.

### Coworkers are made in a wizard and managed in a dialog

Creating a coworker is now a three-step wizard — who it is, who may see it, then where it runs,
Expand Down
26 changes: 20 additions & 6 deletions app/src/components/channels/channel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,26 @@ export function ChannelChat({
channel.threadId,
runtimeAgentId,
);
// Never overwrite local messages that arrived while history was loading.
if (
current &&
stored.messages.length > 0 &&
agent.messages.length === 0
) {
/*
* The durable store wins when it is ahead of what the join delivered.
*
* The join replaces the agent's messages with the realtime gateway's snapshot of the thread,
* and that snapshot can lag the store: a turn that finished, was persisted and answered in
* full came back from the join without its last exchange, on every reload, with no
* unreadable count to explain the gap. Restoring only into an empty agent kept that stale
* snapshot for good.
*
* So the store is applied when it holds more than the agent does AND everything the agent
* holds is in the store. The second half is the guard this replaced: a message typed while
* history was loading is not in the store yet, so it is never overwritten, and a run still
* streaming has messages the store has not seen, so its snapshot is never rolled back.
*/
const local = agent.messages;
const storedIds = new Set(stored.messages.map((m) => m.id));
const storeIsAhead =
stored.messages.length > local.length &&
local.every((m) => storedIds.has(m.id));
if (current && stored.messages.length > 0 && storeIsAhead) {
agent.setMessages(stored.messages);
}
/*
Expand Down