feat(coding-agent): show generated session summaries in the resume picker - #2155
feat(coding-agent): show generated session summaries in the resume picker#2155MarkAronov wants to merge 14 commits into
Conversation
Add a `session_summary` session entry recording a generated one-line summary alongside the id of the last user/assistant message it describes. The resume picker reads it during its existing single pass over the session file and treats it as fresh only while that id is still the newest conversation message, so a summary retires the moment the conversation moves on. A later `branch_summary` retires it too, since the branch it described was abandoned. The anchor is the last message id rather than the leaf id. Appends move the leaf -- including the summary's own append, and every model or thinking-level change -- so a leaf-based check would never match and would regenerate on every idle. Summary usage counts toward session usage totals and the footer, matching branch summaries. It is deliberately excluded from cache-continuity stats, where compaction and branch summaries reset the chain: a summary request is a separate call and does not break the main prompt prefix. Generation and display follow in later commits. Refs: bastani-inc#1033
…t/resume-session-summaries
Add a one-line session summarizer that runs after `agent_end` and persists a `session_summary` entry for the resume picker. It reuses the existing summarization path -- entry selection under a token budget, transcript serialization, credential resolution, stream function and retry policy -- with a new one-line prompt, no reasoning request, and a hard clamp on the stored line so a chatty model cannot break picker rendering. Generation is fire-and-forget and silent by design: nothing awaits it, failures and aborts return without surfacing anything, and retries carry no lifecycle callbacks, since the picker already falls back to the session name or first message. It declines to run when disabled, in print or json mode, while streaming or compacting, without a model, on workflow-stage sessions, on very short sessions, or when the last conversation message has not moved since the stored summary -- which is the main cost control. Concurrency is handled by a monotonic token plus a session-scoped AbortController. After the request returns, the run re-reads both the token and the last conversation message id and discards its own result if either moved, so a slow call cannot persist a summary the conversation has already outrun. The controller is cleared only by the run that still owns it. Adds `sessionSummary.enabled` (default true) as a kill switch. Cancellation wiring from the prompt and shutdown paths, and seeding the in-memory anchor on resume, follow in a later commit. Refs: bastani-inc#1033 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Surface the generated summary in `/resume` and `atomic -r`. The selector now displays `summary ?? name ?? firstMessage`, so a fresh summary takes the row and today's display becomes the fallback. Staleness needs no handling here: a missing or outdated summary is simply absent from `SessionInfo`, so the chain falls through on its own. Session id, message count, age, and cwd stay on the row, and search matches against the summary as well. Carry the field across the interactive-engine boundary too -- row type, protocol parser, and the row-to-SessionInfo mapper -- otherwise summaries would be silently dropped for engine-hosted pickers. The workflows package mirrors the row type by hand rather than importing it, so it gets the field on both sides; nothing enforces that mirror, and omitting either half would drop summaries from `/workflow resume` with no type error. Refs: bastani-inc#1033 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wire up the two lifecycle gaps and cover the feature with tests. Cancellation: `abortSessionSummary()` now runs when the next prompt starts and during session disposal. A background summary can no longer outlive the conversation it describes or hold the process open at shutdown. Resume: the "nothing new to summarize" check falls back to the persisted `summarizedThroughId` when the in-memory anchor is empty, so the first idle after resuming a session no longer regenerates a summary that is already current. Tests cover the listing rules (fresh, stale after a newer message, retired by a later branch summary, unaffected by tool results, absent when never generated) and the generation rules (anchored to the newest conversation message, no regeneration while the conversation has not moved, disabled by setting, skipped in non-interactive modes, and no persistence once the conversation has outrun the request). Documents the entry type and its staleness rule in the session format, the picker behaviour and its fallbacks in the sessions guide, and the new setting. Refs: bastani-inc#1033
…-summaries # Conflicts: # packages/coding-agent/CHANGELOG.md
Credential resolution throws outright when no API key is configured, which is an ordinary state for a session that never prompts. Because the summary runs as `void this._maybeGenerateSessionSummary()`, that throw escaped as an unhandled rejection and could take the process down mid-run; the workflow tool-node quit integration test hit it through a real CLI child and timed out. Background work now swallows every failure -- credential resolution, generation, and persistence alike -- which is what fire-and-forget has to mean here. The returned `error`/`aborted` cases were already silent; thrown ones were not. Refs: bastani-inc#1033
…rompt() Cancellation ran at the top of `prompt()`, ahead of the workflow-delivery authorization boundary and the slash-command path, both of which must observe an untouched session. It now runs once real user input is admitted, which is also when a summary of the previous turn actually becomes stale. Two prompt tests drive `prompt.call()` against hand-built stub sessions, so a new method call on `this` surfaced there as `abortSessionSummary is not a function` rather than as an assertion failure. Their fixtures gain the method. Refs: bastani-inc#1033
… abort Two P1 findings from the automated review, both verified by running code. A retired summary suppressed its own replacement. The resume fallback read the latest persisted `session_summary` as its freshness anchor without the later-`branch_summary` retirement rule the picker applies, so a session whose summary was retired by a branch would match the anchor, skip generation on every idle, and show fallback text in `/resume` indefinitely. `getLatestSessionSummary` is now retirement-aware and the picker calls it too, instead of tracking the same rule inline -- one lookup, both sides, which is what the split had broken. An aborted request could still persist. `abortSessionSummary()` cancels the signal without bumping the ordering token, so a provider that ignores the signal and returns an ordinary result would pass the token and anchor checks and write a summary that was explicitly cancelled. The signal is now checked directly before persisting. Adds listing coverage for a summary generated *after* a branch summary, which must survive: retirement is positional, not permanent. Refs: bastani-inc#1033
…tion Third P1 from the automated review. Moving the leaf invalidates a summary still being generated, but the anchor check could not see it: a `branch_summary` is not a conversation message, and navigating to an existing assistant message leaves the last conversation message id unchanged. A request that returned after the move therefore passed the token and anchor checks and was persisted against a branch that was no longer active. `navigateTree` now cancels the in-flight summary the same way `prompt()` does, which the signal check added for the previous finding turns into a silent discard -- including for a provider that ignores cancellation and returns an ordinary result. The regression test holds the summary request open with a faux response factory, moves the leaf past the last assistant with a `session_info` entry, then navigates back to that message, so the anchor genuinely survives the move. Verified to fail without the fix. Refs: bastani-inc#1033
…-summaries # Conflicts: # packages/coding-agent/CHANGELOG.md
…-summaries # Conflicts: # packages/coding-agent/CHANGELOG.md
|
Hi @MarkAronov thanks for the thoughtful work on this and for the detailed tests and documentation. We tested the exact PR head ( 1. Start summary generation only after the agent has fully become idleThe summary attempt currently starts from We reproduced this with the current built Please move the launch to a confirmed post-settlement/idle boundary, or schedule a safe deferred retry when streaming clears. Please add regressions for both the normal CLI and Relevant location: 2. Prevent queued summary work from running after disposal
We reproduced this with a held-queue harness: it paused Please track a disposed/session-generation state and reject summary work both when it starts and after each asynchronous boundary. At minimum, check ownership/disposal before auth, before contacting the provider, and before persisting the result. Please add a regression that holds the queue, disposes the session, releases the queue, and asserts zero provider calls and zero post-disposal appends. Relevant location: These findings are based on repeatable runtime evidence rather than source review alone. In short, the first issue can silently prevent the feature from running, while the second can let it run after the session should be finished. Thank you again for the contribution and for taking a look at these cases. |
|
One workflow tip: you can have Atomic run this verification for you after you make the fixes instead of driving the TUI manually. From your PR checkout, start Atomic and give it a prompt like: Atomic turns that into a tracked workflow: it scopes the acceptance criteria from the issue and PR, builds your branch, launches that exact build inside a tmux session, operates the resume picker and the rest of the TUI manually, and surfaces anything it reproduces — with the evidence (commands, pane captures, session-file contents) posted back here as a comment. Two details in the prompt matter: "this branch's own Atomic CLI" keeps it from testing the globally installed binary, and "don't modify the code or the PR" keeps it review-only. If a specific path matters, name it in one extra sentence — for example the You can watch the run with |
|
Hi @flora131, thanks for taking the time to run this against a real build and write it up so clearly. Both findings reproduce. The first has a sharper cause than the callback running early. I subscribed to Fixed both in one commit, since they interact: deferring to idle widens the disposal window the second finding is about.
Regressions: a launch fired from inside One scope question before I go further. |
`agent_end` fires while the agent still reports `isStreaming`, and that flag survives the entire microtask queue, clearing only on a later macrotask. The summary launch read the flag immediately and returned, with nothing to retry it, so generation only ever happened when `_checkCompaction` on the preceding line happened to cross a macrotask boundary. It does under the unit harness and does not in the real TUI, so the suite stayed green while the feature produced nothing. Wait for `agent.waitForIdle()` instead, and claim the supersession token before the wait so a later turn abandons a launch that is still parked. Deferring to idle widens the window in which a launch exists with no AbortController, which `abortSessionSummary()` cannot reach, so disposal is now tracked as terminal state. `_disposed` is checked on entry, after the wait, before auth, before the provider call, and before persisting. `abortSessionSummary()` bumps the token as well as aborting, and `dispose()` is idempotent. Reported by @flora131 from a real tmux TUI run against the PR head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-summaries # Conflicts: # packages/coding-agent/CHANGELOG.md
Closes #1033
Problem
The resume picker identifies sessions by a truncated first message, so you have to guess which conversation is which, or open them one at a time to find out.
Solution
Atomic now generates a one-line summary of each session once the agent goes idle, stores it in the session file, and shows it in
/resumeandatomic -r.Per @flora131's guidance, this reuses the existing summarization request path — model, credentials, stream function, retry policy — with a new one-line prompt, stored separately from
branch_summary, generated on idle rather than when the picker opens, and falling back to today's display when unavailable.How it works
core/compaction/session-summarization.ts) mirrorsgenerateBranchSummary's pipeline minus the parts that don't apply: no entry collection, file-op tracking, preamble, custom instructions, or reasoning request. The response has whitespace collapsed and length clamped, because the picker renders one row per session and the prompt can only ask for one line.core/agent-session-summary.ts) runs fire-and-forget fromagent_end. It declines when disabled, in--print/JSON mode, while streaming or compacting, without a model, on workflow-stage sessions, on very short sessions, and — the main cost control — when the last conversation message hasn't moved since the stored summary.branch_summaryalso retires it, since the branch it described was abandoned.AbortController. After the request returns, the run re-reads both the token and the anchor and discards its own result if either moved. The next prompt and session disposal both cancel it.summary ?? name ?? firstMessage. A stale or missing summary is simply absent fromSessionInfo, so the fallback needs no logic in the selector. Session id, message count, age, and cwd stay on the row.sessionSummary.enabled(defaulttrue) turns it off.Decisions worth a second opinion
branch_summary. It's deliberately excluded fromcache-stats.ts, wherecompaction/branch_summaryreset prompt-cache continuity — a separate one-shot request doesn't break the main prefix.Tests
Added
test/session-manager/session-summary-listing.test.ts(fresh; stale after a newer message; retired by a later branch summary; unaffected by tool results; absent when never generated) andtest/suite/agent-session-summary.test.ts(anchored to the newest conversation message; no regeneration while the conversation hasn't moved; disabled by setting; skipped in non-interactive modes; no persistence once the conversation has outrun the request).npm run checkpasses. The fullnpm run test:unithas two pre-existing failures on my Windows machine, both verified against cleanupstream/mainwith none of these commits present:flaky-test-suite-runner.test.tsfails deterministically ('C:\Program' is not recognized— an unquoted path with a space when it spawns the child suite), and one load-sensitive timeout that varies by run and passes in isolation. Happy to open a separate issue for the first if it's not already known.Follow-up
Workflow-run summaries are a second PR, as agreed on the issue — those use different storage and have no single shared transcript, so stage names and results are the right source there.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Greptile Summary
Atomic now creates concise summaries after interactive sessions become idle, stores each summary with a conversation anchor, and uses fresh summaries in resume selection.
Verified behavior:
Disproved concerns:
Confidence Score: 5/5
The PR is safe to merge; no blocking failure remains.
The final defect set is empty. Executable checks confirmed cancellation and disposal protections, freshness and retirement handling, picker display behavior, and summary search behavior.
Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile