Skip to content

feat(coding-agent): show generated session summaries in the resume picker - #2155

Open
MarkAronov wants to merge 14 commits into
bastani-inc:mainfrom
MarkAronov:feat/resume-session-summaries
Open

feat(coding-agent): show generated session summaries in the resume picker#2155
MarkAronov wants to merge 14 commits into
bastani-inc:mainfrom
MarkAronov:feat/resume-session-summaries

Conversation

@MarkAronov

@MarkAronov MarkAronov commented Aug 3, 2026

Copy link
Copy Markdown

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 /resume and atomic -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

  • Generation (core/compaction/session-summarization.ts) mirrors generateBranchSummary'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.
  • Orchestration (core/agent-session-summary.ts) runs fire-and-forget from agent_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.
  • Staleness is anchored to the id of the last user/assistant message the summary covers, not 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 regenerate on every idle. The picker shows the summary only while that anchor is still the newest conversation message; a later branch_summary also retires it, since the branch it described was abandoned.
  • Concurrency uses a monotonic token plus a session-scoped 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.
  • Display is summary ?? name ?? firstMessage. A stale or missing summary is simply absent from SessionInfo, so the fallback needs no logic in the selector. Session id, message count, age, and cwd stay on the row.

sessionSummary.enabled (default true) turns it off.

Decisions worth a second opinion

  • A generated summary currently outranks an explicitly user-set session name. That follows the fallback ordering in Improve resume list context with chat summaries #1033 ("fall back to the current session name or first message"), but it's the one call I'd happily reverse — it's a one-line change plus a test.
  • Summary token usage counts toward session totals and the footer, matching branch_summary. It's deliberately excluded from cache-stats.ts, where compaction/branch_summary reset prompt-cache continuity — a separate one-shot request doesn't break the main prefix.
  • Retries are silent. The retry policy is passed, but not the lifecycle callbacks, which drive a foreground "retrying summarization…" banner meant for work the user initiated.
  • Known gap: navigating to an earlier point without summarizing leaves no trace in the file, so the picker can briefly show a summary describing an abandoned branch. It self-heals on the next turn; the cost of being wrong is one slightly-off line.

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) and test/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).

test/suite/agent-session-summary.test.ts                    5 passed
test/session-manager/session-summary-listing.test.ts        5 passed
test/unit/host-session-picker-local.test.ts                 
test/unit/interactive-engine-session-picker.test.ts         31 passed
test/unit/workflow-resume-selector-host-picker.test.ts      
test/session-selector-*.test.ts, internal-session-resume-*  24 passed

npm run check passes. The full npm run test:unit has two pre-existing failures on my Windows machine, both verified against clean upstream/main with none of these commits present: flaky-test-suite-runner.test.ts fails 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.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with 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:

  • A background summary response that returns after session disposal does not persist stale metadata, including when the provider ignores cancellation; the focused lifecycle suite passed 13/13 tests.
  • Fresh persisted summaries replace fallback session text and are searchable in resume selection.
  • Summaries retired by a branch change fall back to the session name and are excluded from summary search.

Disproved concerns:

  • Stale or cancelled summary requests persisting after disposal were disproved by an executed abort-ignoring provider reproduction that observed zero stored summaries.
  • Incorrect rendering or search behavior for fresh and retired summaries was disproved by terminal rendering checks and focused listing/search tests.

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

MarkAronov and others added 6 commits August 1, 2026 21:03
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
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
Comment thread packages/coding-agent/src/core/agent-session-events.ts
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
Comment thread packages/coding-agent/src/core/agent-session-summary.ts Outdated
Comment thread packages/coding-agent/src/core/agent-session-summary.ts
… 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
Comment thread packages/coding-agent/src/core/agent-session-summary.ts
…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
@flora131

flora131 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Hi @MarkAronov thanks for the thoughtful work on this and for the detailed tests and documentation. We tested the exact PR head (aee30eb0ae934a6683754714fdc62c5b93c2561a) using the built Atomic CLI in a real tmux TUI session and found two lifecycle issues that need to be fixed before approval.

1. Start summary generation only after the agent has fully become idle

The summary attempt currently starts from agent_end, but that callback can run while the agent still reports isStreaming. _maybeGenerateSessionSummary() then returns early and nothing retries it.

We reproduced this with the current built --no-extensions TUI: two user/assistant turns completed, the session remained idle, and the JSONL contained four conversation messages but no session_summary entries. The resume picker therefore showed the existing fallback instead of a generated summary.

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 --no-extensions path so an eligible completed turn reliably produces a summary after idle.

Relevant location: packages/coding-agent/src/core/agent-session-events.ts:267.

2. Prevent queued summary work from running after disposal

dispose() currently aborts a summary controller only if that controller already exists. Work queued before the controller is created can resume after disposal, create a new request, contact the provider, and append to retired session state.

We reproduced this with a held-queue harness: it paused _checkCompaction(), called dispose(), and then released the queued agent_end work. The provider was called and a summary was appended after disposal.

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: packages/coding-agent/src/core/agent-session-events.ts:479-481.

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.

@flora131

flora131 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

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:

Test the changes on this PR for correctness. Build and run this branch's own
Atomic CLI in a tmux session and manually verify the feature works as the
linked issue describes, then post the evidence of the results to the PR as
a comment. Don't modify the code or the PR itself.

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 --no-extensions flow from the findings above.

You can watch the run with /workflow connect <run-id> while it works, and the tmux session stays alive afterward for your own inspection.

@MarkAronov

Copy link
Copy Markdown
Author

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 agent_end and measured it: isStreaming is true there and stays true across the whole microtask queue, clearing only on a later macrotask. So generation worked only when await this._checkCompaction(msg) on the previous line happened to cross that boundary. It does in the unit harness, which is why seven tests passed while your run produced nothing. Good catch, this would have shipped silently.

Fixed both in one commit, since they interact: deferring to idle widens the disposal window the second finding is about.

  1. await this.agent.waitForIdle() inside _maybeGenerateSessionSummary, with the supersession token claimed before the wait so a later turn abandons a parked launch.
  2. A terminal _disposed flag set by dispose(), checked on entry, after the wait, before auth, before the provider call, and before persisting. abortSessionSummary() bumps the token too, since a parked launch has no AbortController yet. dispose() is idempotent.

Regressions: a launch fired from inside agent_end while isStreaming is true, which also asserts that condition is reproduced so it cannot pass for the wrong reason; and dispose then release, asserting zero provider calls and zero appends. Both fail against the pre-fix source, with two more covering the new token semantics.

One scope question before I go further. --no-extensions only affects the resource loader, not _extensionMode, and the harness already runs with no extensions, so that path is covered. For the normal CLI I have stayed at harness level. Would you like a real CLI regression that spawns the binary and checks the JSONL? Happy to add it, just want to match the bar you have in mind.

MarkAronov and others added 2 commits August 5, 2026 00:15
`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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve resume list context with chat summaries

2 participants