feat(coding-agent): show generated session summaries in the resume picker - #2155
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
|
Thanks @MarkAronov. Your explanation and fixes address the two findings. The harness coverage for The branch now conflicts with |
…-summaries Two conflicts, both in packages/coding-agent. `agent-session-events.ts`: the session-summary launch landed on the same line as the context-overflow fallback block and the turn-scoped model restore added by bastani-inc#2201. Both sides are kept, with the launch last, for two reasons. A successful `_trySwitchToFallbackModel()` returns above it, so a turn that continues on another model is not summarized mid-flight. And `_restoreFallbackModel()` has already run by then, which matters because the launch reads `this.model` synchronously before it parks on `waitForIdle()`; launching any earlier would send the summary request on the fallback model rather than the one the user selected. `CHANGELOG.md`: both sides appended to `[Unreleased]`. Kept both, `### Added` first per the section order in AGENTS.md, with upstream's text and its new `0.9.13-alpha.1` section unchanged.
… one request Every turn schedules a summary launch, and the previous turn's can still be in flight when the next one wakes. The newcomer aborted its predecessor and issued its own request, so both spent a provider call and only the second could persist. The ordering that hid this was incidental: bastani-inc#2201 added awaits to the turn path, and the wasted request became reliable rather than rare. Publish the in-flight request as `_sessionSummaryRun`, carrying the `throughId` it describes. A launch that wakes to find a run covering the same conversation state now awaits it instead of replacing it; a launch describing a newer state still supersedes, exactly as before. Once a run is published, ownership of that slot rather than the token is what licenses a write. A joiner claims the token on its way in, so a token check after the request would have the joiner invalidate the very run it is waiting for. The token still guards the parked phase, where a launch holds no AbortController and nothing else can reach it. `abortSessionSummary()` clears the run as well, so a provider that ignores its signal still fails the ownership check, and a later launch cannot join a cancelled run. The deferred behind that promise is hand-rolled rather than `Promise.withResolvers`. coding-agent is the one compiled package here and its lib target predates ES2024, so the shipped sources cannot use it even though the raw-TypeScript packages do. Only `test:integration`, which compiles the package the way the build does, catches that; the root `tsc --noEmit` runs against a newer lib and passes. Guard the launch itself with `typeof this._maybeGenerateSessionSummary === "function"`, matching every other optional method in that block. The main-chat fallback suites drive `_processAgentEvent` on a synthetic session object, so an unguarded call threw and took `a compactable context overflow does not spend a fallback candidate` with it. Two tests budgeted no response for the turn-2 launch, which reaches the provider before disposal or the next prompt lands and is cancelled mid-request. That request is spent either way and cannot be recalled, so both now budget it. No assertion changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hey @flora131! Thanks for getting back to me, and no worries at all about the delay, Will do. One thing: I'd like to merge On preserving both: the collision is that my summary launch sits exactly where
Two things the merge turned up: A race it exposed. #2201's extra awaits changed the turn timing enough that One bug that was mine. I hadn't guarded the launch with Results: One ask: the Tests workflow is stuck at Also hit four Windows issues in the repo's tooling, none related to this PR.
|
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
|
Hey @flora131 - following up on this. Pushed the fix for both issues on Aug 5 (9d340ab: deferred generation to a real idle boundary, added terminal disposal tracking). |
Thanks for your patience, we are working on a large suite of fixes with the latest pi dependency upgrade to 0.84.1 which is taking longer than expected. Merge makes sense for conflict resolution, but we will most likely need to fix some conflicts because of the larger refactor for our pi dependency. We'll handle the conflicts and put your changes into the merge queue once you have the E2E test implemented as described below. An E2E harness test is preferred: just ask Atomic to "spawn an instance of itself and mention that it's authenticated and can drive the CLI to make sure it works and then ask Atomic to comment on this PR with tmux screenshot evidence that the change works." |
|
Thanks for finding the dev environment issues in Windows @MarkAronov! Yes, it would be great to file them, so we can understand if these are still issues in the latest main branch. |
sessionSummary generation issues a background provider request once the agent goes idle, which the fake server in this test counts alongside the cycled prompt's turn, so requests reached 2. Turn summaries off in the fixture settings. The counter exists to prove the fallback path does not fire a second turn for one prompt, and that claim is only readable when the prompt is the sole caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Fixed the failing unit test in 63ec766. The idle session-summary launch is a second caller of the same fake provider in Worth knowing for later: any fixture test that counts provider requests is now sensitive to background summary traffic. This is the only one today. |
|
Ran a local E2E pass of this branch (macOS, Bun 1.3.14, tmux-driven dummy CLI session):
Stored entry: {"type":"session_summary","summary":"Answered a quick question defining git worktrees as linked working directories for multiple branch checkouts","summarizedThroughId":"a080e293",...} |
- keep the session name/first message as the row identity; the generated summary renders beside it and never displaces it - show a "No summary available." placeholder when a summary is missing, stale, or failed; omit the column entirely on narrow terminals - gate the disposal test on request start so turn 2s launch provably spends a response before dispose(), fixing the linux CI failure where the pending-response count depended on scheduler timing - update docs/sessions.md and the changelog wording to match Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 7f30478 with three amendments:
Docs ( |
abortSessionSummary() now drops the in-memory anchor cache. The cache is not retirement-aware: after branchWithSummary() retired the stored summary without moving the last conversation message id, a cached anchor still matching that id skipped regeneration and left the picker on fallback text until the next real turn. The persisted lookup already handles retirement, so the cache defers to it after any cancellation. Addresses the one substantiated Greptile P1 on bastani-inc#2155. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Updated E2E capture of the column layout on Row identity (first message) stays in the left column; the generated summary sits in its own aligned column, with the muted italic placeholder for the session that has no summary yet. |
The branch's merge of main dropped the immutable [0.9.13-alpha.2] section and left its entries duplicated under [Unreleased], which the changelog immutability test rejects against the release tag. CHANGELOG.md is now origin/main's content plus only this PR's own entry under [Unreleased]. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Pull request was closed
# Conflicts: # packages/coding-agent/CHANGELOG.md
|
Thanks @flora131, and thanks for the amount of work you put into this one. Sorry about the changelog. My Aug 7 merge folded the released I went through your other commits. The summary column is better than what On the Windows side, #2291 is filed and already fixed. The rest of what I Planning to pick up #2329 next if possible. Thanks again :) |


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 generates concise, bounded summaries for eligible idle interactive sessions, persists them with message freshness anchors, and shows current summaries in resume pickers.
Executed session-lifecycle and picker checks ruled out duplicate idle generation, stale writes after prompt changes, navigation, or disposal, and stale summary display after branch retirement. The branch-retirement behavior was also reproduced on the earlier implementation and confirmed fixed in this change.
Confidence Score: 5/5
The PR is safe to merge; no blocking failure remains.
Focused execution covered summary freshness, idle scheduling, concurrent launches, prompt supersession, navigation cancellation, disposal, persisted listing, and resume-picker rendering. These checks confirmed that the feature avoids stale summary writes and display.
What T-Rex did
Reviews (13): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile