githubnext/chopinPublic

Let's compose rich plans together

Announce workflow run updates only after they are saved, and close helpers safely

A run card and its finish message were broadcast before they were saved,
so a failed save left people watching a job finish that a reload then
lost. The update is now built as a candidate outside shared chat state and
saved first: the save stores the candidate transcript and run cards (read
when the commit runs, so messages added meanwhile are kept), and only when
it succeeds are the card and finish message added to the chat and
announced together. While a save is pending, or after it fails, nothing
else that broadcasts chat state or greets a joining client can show the
unsaved card or message. Reports are handled one at a time, so a report
that arrives while an earlier save is pending is computed from the state
after that save settles.

When the save of a report fails, that report is remembered. If the helper
is released afterwards, the release first waits for pending reports and
then publishes the remembered report instead of treating the runs as cut
off, so a transient storage error no longer turns a finished run into a
"was stopped" card; the finished card and its single finish message are
saved and announced together.

Other review fixes from Maggie Appleton on the same code:
- A quick job that ended before the Planner's reply finished now keeps
  its last card and gets its finish message.
- A run first reported as already ended gets one finish message, and a
  run already recorded as ended never gets another.
- When the last job ends while a reply (or a queued reply) is still
  running, cleanup is checked again once it finishes and releases only
  the same helper, and only if it still has no active or paused runs.
- A slow release of an old helper no longer clears the owner or
  overwrites the run cards of a newer helper.

Every run card belongs to the Planner session that reported it, and that
is the only rule for reports and releases. A report from a session
replaces or updates only the cards that session owns and appends its new
ones; live cards of another session still being released are carried
through unchanged, so a newer helper reporting its own runs no longer
drops an older helper's card. Releasing a session replays its own
remembered unsaved report first (a failed final finished report still
publishes as finished), then marks stopped exactly the unended cards it
owns, saving and announcing one "was stopped" message each in a single
save. Cards owned by another session, and the owner and agent of a newer
helper, are never touched; the owner is cleared only when the released
session is still the current agent and no turn is running. If the final
save also fails, nothing is announced and the cards of the released
session are stopped by the next report from any session.

Assistant-model: Claude Sonnet 5.5
Assistant-workflow: goal (run 6091839b-867c-4965-aec9-b583f62d4b1f)
Assistant-verification: table test "releasing an old helper after %s stops exactly its own cards once" (four cases: none, run-less turn, own runs, run-less turn after a failed save) failed before the fix for the own-runs case (run-1 dropped from chat.runs when the newer helper reported run-2; 25 pass, 1 fail in invoke.test.ts) and passes for all four after (26 pass, 0 fail); earlier round-1 to round-5 tests stay green
Assistant-verification: bun test passed: 1918 pass, 2 PostgreSQL skips, 0 fail (private TMPDIR)
Assistant-verification: bun run types passed: all workspaces and e2e
Assistant-verification: bun run ci passed: dprint, oxlint (0 errors, 6 warnings, none in touched files), tokens, design contract, design record, Impeccable
Co-authored-by: Alex Lavaee <lavaman131@github.com>
ALAlex Lavaee authored
d02d7e0Patch
Parentcf2058d
4 files changed+579-33
StackedSplit
apps/server/src/chat/invoke.test.ts+464 โˆ’4
apps/server/src/chat/service.ts+97 โˆ’18
apps/server/src/main.ts+1 โˆ’1
apps/server/src/plan/service.ts+17 โˆ’10