Skip to content

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

Merged
flora131 merged 24 commits into
bastani-inc:mainfrom
MarkAronov:feat/resume-session-summaries
Aug 12, 2026
Merged

feat(coding-agent): show generated session summaries in the resume picker#2155
flora131 merged 24 commits into
bastani-inc:mainfrom
MarkAronov:feat/resume-session-summaries

Conversation

@MarkAronov

@MarkAronov MarkAronov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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 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.

T-Rex T-Rex Logs

What T-Rex did

  • The team reproduced the branch-retirement summary behavior against the earlier implementation and confirmed that the current change regenerates the summary after retirement.
  • Focused real-harness tests for idle freshness, concurrent-launch coalescing, prompt supersession, navigation cancellation, and disposal completed with all seven tests passing.
  • Persisted-listing and picker tests for fresh, stale, retired, tool-result, and rebranched summaries completed with all twelve tests passing.
  • Obsolete summaries are neither persisted nor displayed, while valid summaries are regenerated when retirement invalidates an earlier one.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (13): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | 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 Outdated
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
Contributor 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
@flora131

flora131 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks @MarkAronov. Your explanation and fixes address the two findings. The harness coverage for --no-extensions sounds reasonable, and I don’t think an additional real-CLI regression is necessary if the existing regression reliably reproduces the timing condition that failed in the TUI.

The branch now conflicts with main in agent-session-events.ts due to the newer model-fallback handling. Could you merge or rebase the latest main, preserve both the fallback lifecycle logic and the session-summary launch, and rerun the relevant checks? Sorry for the delay on our side. Once the conflicts are resolved and checks pass, we should be good.

MarkAronov and others added 2 commits August 7, 2026 08:29
…-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>
@MarkAronov

Copy link
Copy Markdown
Contributor Author

Hey @flora131! Thanks for getting back to me, and no worries at all about the delay,
it really wasn't a problem on my end.

Will do. One thing: I'd like to merge main in rather than rebase, if that's
fine by you. Three of the commits here touch agent-session-events.ts, so a
rebase replays the same conflict two or three times, and the force push would
mark the existing threads outdated and make it harder to see what actually
changed since you last looked. Since the PR squash-merges, main ends up
identical either way.

On preserving both: the collision is that my summary launch sits exactly where
#2201 added the overflow-fallback block, at the tail of _processAgentEvent.
Keeping both, with the launch last, for two reasons:

  • A fallback switch returns before it, so a turn that continues on another model
    doesn't get summarized mid-flight.
  • _restoreFallbackModel() has already run by then. That matters because the
    launch reads this.model synchronously before it parks on waitForIdle(), so
    launching any earlier would send the summary request on the fallback model
    instead of the one the user actually picked.

CHANGELOG.md was the other conflict. Both sides had added to [Unreleased],
so I kept both.

Two things the merge turned up:

A race it exposed. #2201's extra awaits changed the turn timing enough that
two overlapping summary launches each sent a provider request, and only the
second could save. That was always possible, the old timing just hid it. Now the
in-flight request is published with the throughId it covers, so a launch for
the same state waits on it instead of cancelling it and starting over.

One bug that was mine. I hadn't guarded the launch with
typeof this._maybeGenerateSessionSummary === "function" like the other optional
methods there, so it threw on the fake session the main-chat fallback tests use
and broke a compactable context overflow does not spend a fallback candidate.
Fixed, 21/21 again.

Results: npm run check clean, test:integration 485 passed, summary/listing/
retry suites 29 passed. test:unit has 10 failures but all 10 also fail on clean
upstream/main here, and main-chat-model-fallback passes.

One ask: the Tests workflow is stuck at action_required on every push to this
PR, so CI has never run. Could you approve it?

Also hit four Windows issues in the repo's tooling, none related to this PR.
Happy to open issues:

  • test/global-setup-natives.ts calls spawnSync("npm", ...) without
    shell: true. npm is npm.cmd on Windows, so it's ENOENT and all three root
    suites die before any test runs.
  • cargo clippy -- -D warnings fails on crates/atomic-natives/src/grep/run.rs
    (refactor(natives): split grep and fs_cache into responsibility modules #2206). Its #[cfg(test)] helpers are only used by #[cfg(unix)] tests, so
    they're dead code on Windows.
  • rustfmt.toml sets newline_style = "Unix" but .gitattributes has
    * text=auto, so with core.autocrlf=true every .rs file checks out CRLF
    and cargo fmt --check can never pass.
  • release-publisher-contracts.test.ts calls bare tar, which picks up Git's
    GNU tar on Windows and reads C:\... as a remote host.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@MarkAronov

Copy link
Copy Markdown
Contributor Author

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).
Also still curious on the scope question from my last comment - want a real CLI regression that spawns the binary and checks the JSONL, or is harness-level coverage enough?

@flora131

Copy link
Copy Markdown
Collaborator

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). Also still curious on the scope question from my last comment - want a real CLI regression that spawns the binary and checks the JSONL, or is harness-level coverage enough?

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."

@flora131

Copy link
Copy Markdown
Collaborator

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>
@MarkAronov

Copy link
Copy Markdown
Contributor Author

Fixed the failing unit test in 63ec766.

The idle session-summary launch is a second caller of the same fake provider in interactive-engine-cycle-fallback, so the requests === 1 assertion saw 2. The fixture now opts out of session summaries.

Worth knowing for later: any fixture test that counts provider requests is now sensitive to background summary traffic. This is the only one today.

Comment thread packages/coding-agent/test/suite/agent-session-summary.test.ts
@flora131

Copy link
Copy Markdown
Collaborator

Ran a local E2E pass of this branch (macOS, Bun 1.3.14, tmux-driven dummy CLI session):

  1. Fresh session in a throwaway cwd, one prompt, agent went idle → a session_summary entry landed in the session file ~17s later, anchored via summarizedThroughId and counted in session usage totals.
  2. Relaunched with -r: the picker row shows the generated summary instead of the truncated first message.
  3. Selecting the row resumes the correct session with the full prior conversation.

resume picker showing generated session summary

Stored entry:

{"type":"session_summary","summary":"Answered a quick question defining git worktrees as linked working directories for multiple branch checkouts","summarizedThroughId":"a080e293",...}

@flora131
flora131 enabled auto-merge (squash) August 12, 2026 21:11
@flora131
flora131 disabled auto-merge August 12, 2026 21:18
- 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>
@flora131

Copy link
Copy Markdown
Collaborator

Pushed 7f30478 with three amendments:

  1. Summary is now its own column. The row identity (session name or first message) is never displaced; the generated summary renders beside it, dim. The column only appears when at least one listed row has a summary, so the durable-workflow picker and fresh installs keep the old single-column layout.
  2. Explicit fallback. Rows without a usable summary (missing, stale, failed, whitespace-only) show a muted italic "No summary available." placeholder; the column is skipped entirely on narrow terminals.
  3. CI fix. The linux agent-suite failure was a race in "runs no summary work once the session has been disposed": it assumed turn 2's fire-and-forget launch reached the provider before dispose(). It now gates on request start with the same Promise.withResolvers mechanism as the neighboring mid-request test, making the pending-response count deterministic.

Docs (docs/sessions.md) and the changelog entry updated to match. New render coverage in test/session-selector-summary-column.test.ts (6 tests: column, name+summary coexistence, placeholder, whitespace-only, no-summary lists, narrow terminals).

@flora131
flora131 enabled auto-merge (squash) August 12, 2026 21:37
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>
@flora131

Copy link
Copy Markdown
Collaborator

Updated E2E capture of the column layout on 68e5250 (tmux-driven, same dummy-session flow as before, now with two sessions to show both states):

resume picker with dedicated summary column: one row with a generated summary, one row showing the "No summary available." placeholder

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>
@flora131 flora131 closed this Aug 12, 2026
auto-merge was automatically disabled August 12, 2026 22:14

Pull request was closed

@flora131 flora131 reopened this Aug 12, 2026
@flora131
flora131 enabled auto-merge (squash) August 12, 2026 22:16
@flora131
flora131 merged commit c4b6526 into bastani-inc:main Aug 12, 2026
15 checks passed
@MarkAronov

MarkAronov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @flora131, and thanks for the amount of work you put into this one.
Building the branch and driving it in tmux to find the agent_end timing
problem is well beyond a source review, and both findings were real.

Sorry about the changelog. My Aug 7 merge folded the released
0.9.13-alpha.2 entries into [Unreleased] and dropped the section
header. I took the immutability rule to mean don't edit released entries
and missed that main had already promoted that set.

I went through your other commits. The summary column is better than what
I had, keeping the row identity and adding an explicit placeholder is the
right call. And clearing _lastSummarizedMessageId in
abortSessionSummary() is correct: the in-memory anchor is not
retirement-aware and needs to defer to the persisted lookup after any
cancellation. Good one to have caught.

On the Windows side, #2291 is filed and already fixed. The rest of what I
hit looks environment-specific rather than worth separate issues.

Planning to pick up #2329 next if possible.

Thanks again :)

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