Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3a1313b
feat(coding-agent): persist per-session resume summaries
MarkAronov Aug 1, 2026
2ac8b8b
Merge branch 'main' of https://github.com/bastani-inc/atomic into fea…
MarkAronov Aug 2, 2026
5208463
feat(coding-agent): generate resume summaries when the agent goes idle
MarkAronov Aug 3, 2026
74733f0
feat(coding-agent): show session summaries in the resume picker
MarkAronov Aug 3, 2026
7afbdfe
feat(coding-agent): cancel, reseed and document session summaries
MarkAronov Aug 3, 2026
aedba63
Merge remote-tracking branch 'upstream/main' into feat/resume-session…
MarkAronov Aug 3, 2026
9107f60
fix(coding-agent): stop a failed session summary from rejecting
MarkAronov Aug 3, 2026
2946af1
fix(coding-agent): cancel the session summary at the right point in p…
MarkAronov Aug 3, 2026
3bd7336
fix(coding-agent): apply one retirement rule to summaries, and honour…
MarkAronov Aug 3, 2026
209144e
fix(coding-agent): cancel an in-flight session summary on tree naviga…
MarkAronov Aug 3, 2026
9fc2a84
Merge remote-tracking branch 'upstream/main' into feat/resume-session…
MarkAronov Aug 3, 2026
aee30eb
Merge remote-tracking branch 'upstream/main' into feat/resume-session…
MarkAronov Aug 3, 2026
9d340ab
fix(coding-agent): generate session summaries at a real idle boundary
MarkAronov Aug 4, 2026
99ee61a
Merge remote-tracking branch 'upstream/main' into feat/resume-session…
MarkAronov Aug 4, 2026
7eb28a1
Merge remote-tracking branch 'upstream/main' into feat/resume-session…
MarkAronov Aug 7, 2026
83a4fc3
fix(coding-agent): collapse overlapping session-summary launches into…
MarkAronov Aug 7, 2026
3563414
Merge branch 'main' into feat/resume-session-summaries
MarkAronov Aug 8, 2026
63ec766
fix(test): keep the cycle-fallback request count to the prompt turn
MarkAronov Aug 10, 2026
9bcb071
Merge branch 'main' into feat/resume-session-summaries
MarkAronov Aug 11, 2026
7f30478
fix(coding-agent): render session summaries in a dedicated picker column
flora131 Aug 12, 2026
68e5250
fix(coding-agent): regenerate session summary after branch retirement
flora131 Aug 12, 2026
543ec1a
fix(coding-agent): restore the released 0.9.13-alpha.2 changelog section
flora131 Aug 12, 2026
c46bc9c
chore: retrigger CI
flora131 Aug 12, 2026
6654fac
Merge remote-tracking branch 'origin/main' into pr-2155
flora131 Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Added generated session summaries to the resume picker, so `/resume` and `atomic -r` show what a conversation was about instead of a truncated first message. Atomic writes a one-line summary once the agent goes idle, reusing the session's existing model, credentials, stream function, and retry policy with a dedicated one-line prompt and no reasoning request; the stored line has its whitespace collapsed and length clamped so a long response cannot break picker layout. Each summary is persisted as a `session_summary` session entry anchored to the last user/assistant message it describes, and the picker shows it only while that message is still the newest one — a later message, or a later branch summary, retires it and the display falls back to the session name or first message, exactly as it does when a summary has not been generated, has failed, or is still in flight. Summaries are matched by picker search, and their token usage is counted toward session usage totals and the footer. Generation is best-effort and invisible: it is skipped for very short sessions, workflow stage sessions, and `--print`/JSON modes, it never blocks or delays a turn, it is cancelled when the next prompt starts or the session shuts down, and a slow request that outlives the conversation discards its own result rather than persisting a stale summary. Set `sessionSummary.enabled` to `false` to disable it ([#1033](https://github.com/bastani-inc/atomic/issues/1033)).

## [0.9.11-alpha.10] - 2026-08-01

### Fixed
Expand Down
15 changes: 15 additions & 0 deletions packages/coding-agent/docs/session-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,20 @@ Session metadata (e.g., user-defined display name). Set via `/name`, `--name` /

The session name is displayed in the session selector (`/resume`) instead of the first message when set.

### SessionSummaryEntry

A generated one-line description of the session, written automatically once the agent goes idle and shown in the session selector. Never sent to the LLM.

```json
{"type":"session_summary","id":"l2m3n4o5","parentId":"k1l2m3n4","timestamp":"2024-12-03T14:36:00.000Z","summary":"Refactored auth module token refresh and added retry tests","summarizedThroughId":"j0k1l2m3","usage":{"input":812,"output":21}}
```

- `summary`: The stored line. Whitespace is collapsed and length is clamped before writing.
- `summarizedThroughId`: Entry ID of the last user/assistant message the summary covers. The selector shows the summary only while this is still the newest conversation message; anything newer makes it stale and the selector falls back to the session name or first message. Tool results do not count.
- `usage`: Optional token usage from the model call, counted toward session usage totals.

A `branch_summary` written after a `session_summary` also retires it, because the branch it described was abandoned.

## Tree Structure

Entries form a tree:
Expand Down Expand Up @@ -420,6 +434,7 @@ for (const stage of stages.filter((session) => session.internal)) {
- `appendCompaction(compactedText, firstKeptEntryId, tokensBefore, details)` - Add a durable verbatim-line compaction boundary; pass `null` when no pre-boundary message is retained
- `appendCustomEntry(customType, data?)` - Extension state (not in context)
- `appendSessionInfo(name)` - Set session display name
- `appendSessionSummary(summary, summarizedThroughId, usage?)` - Store a generated resume-picker summary, anchored to the message it describes
- `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context)
- `appendLabelChange(targetId, label)` - Set/clear label

Expand Down
8 changes: 8 additions & 0 deletions packages/coding-agent/docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ In the picker you can:

When available, Atomic uses the `trash` CLI for deletion instead of permanently removing files.

### Session summaries

Each row shows a short generated description of what the session was about, so you can recognize a conversation without opening it. Atomic writes one after the agent goes idle, using the model the session is already configured with, and stores it in the session file as a `session_summary` entry.

A summary describes the conversation up to a specific message. Once a newer message arrives it is considered stale and the picker falls back to the session name, or to the first message — the same display you get when a summary has not been generated yet, could not be generated, or is still in flight. Summaries are also searchable along with the rest of the session text.

Generation is best-effort and never blocks a turn: it is skipped for very short sessions, for workflow stage sessions, and in `--print` and JSON modes, it is cancelled when you send the next message or quit, and its failures are silent. Set `sessionSummary.enabled` to `false` to turn it off entirely.

The picker opens instantly: its header, search field, and loading indicator paint on the first frame, then sessions are discovered and parsed off the terminal's UI loop. Large session directories are scanned in cooperative batches and a single very large transcript is parsed in yielding chunks, so search, navigation, and cancel stay responsive and no individual session can freeze the picker while it loads. Closing the picker cancels any in-flight scan and discards stale results, so a slow load that finishes after you leave never updates the list.

### Internal (workflow) sessions
Expand Down
6 changes: 6 additions & 0 deletions packages/coding-agent/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ The model emits numbered line ranges only; Atomic reconstructs retained text mec
| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved for branch summarization |
| `branchSummary.skipPrompt` | boolean | `false` | Skip "Summarize branch?" prompt on `/tree` navigation (defaults to no summary) |

### Session Summary

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `sessionSummary.enabled` | boolean | `true` | Generate a one-line summary of each session for the `/resume` picker once the agent goes idle |

### Retry

| Setting | Type | Default | Description |
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/core/agent-session-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export async function _processAgentEvent(this: AgentSession, event: AgentEvent):

this._resolveRetry();
await this._checkCompaction(msg);
if (event.type === "agent_end") void this._maybeGenerateSessionSummary();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}
}

Expand Down Expand Up @@ -472,6 +473,8 @@ export function _reconnectToAgent(this: AgentSession): void {
*/

export function dispose(this: AgentSession): void {
// A background summary must never keep the process alive past shutdown.
this.abortSessionSummary();
// Fail closed while protected input remains queued, or flush a consumed
// reconciliation before invalidation can discard its recovery state.
prepareProtectedStreamingCustomMessagesForDisposal(this);
Expand Down
4 changes: 3 additions & 1 deletion packages/coding-agent/src/core/agent-session-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export function getSessionStats(this: AgentSession): SessionStats {
let toolCalls = 0;
const totals = createUsageTotals();
for (const entry of this.sessionManager.getEntries()) {
if (entry.type === "branch_summary" && entry.usage) addUsageToTotals(totals, entry.usage);
if ((entry.type === "branch_summary" || entry.type === "session_summary") && entry.usage) {
addUsageToTotals(totals, entry.usage);
}
if (entry.type !== "message") continue;
totalMessages++;
const message = entry.message;
Expand Down
6 changes: 6 additions & 0 deletions packages/coding-agent/src/core/agent-session-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export interface AgentSessionMethodSurface extends AgentSessionQueuePauseControl
compact(options?: Partial<VerbatimCompactionParameters>): Promise<VerbatimCompactionResult>;
abortCompaction(): void;
abortBranchSummary(): void;
abortSessionSummary(): void;
_maybeGenerateSessionSummary(): Promise<void>;
_checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck?: boolean): Promise<void>;
_dropTrailingAutoCompactionRetryAssistantIfPresent(): void;
_schedulePostAutoCompactionContinuationProbe(reason: "overflow" | "threshold", willRetry: boolean): void;
Expand Down Expand Up @@ -324,6 +326,7 @@ export interface AgentSessionPublicSurface
| "autoCompactionEnabled"
| "isRetrying"
| "autoRetryEnabled"
| "abortSessionSummary"
| "isBashRunning"
| "hasPendingBashMessages"
| "extensionRunner"
Expand Down Expand Up @@ -438,6 +441,9 @@ export interface AgentSessionInternalSurface extends AgentSessionMethodSurface,
_orchestrationContext?: OrchestrationContext;
_extensionUIContext?: ExtensionUIContext;
_extensionMode: ExtensionMode;
_sessionSummaryAbortController: AbortController | undefined;
_sessionSummaryToken: number;
_lastSummarizedMessageId: string | undefined;
_extensionCommandContextActions?: ExtensionCommandContextActions;
_extensionShutdownHandler?: () => void;
_extensionErrorListener?: ExtensionErrorListener;
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/core/agent-session-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export async function tryExecuteSessionSlashCommand(
export async function prompt(this: AgentSession, text: string, options?: PromptOptions): Promise<void> {
const owner = resolveWorkflowStageDeliveryTarget(this);
if (owner !== this) return owner.prompt(text, options);
// A summary of the conversation up to the previous turn is about to be stale; stop paying
// for it. The generation itself is discarded by its own token/anchor checks either way.
this.abortSessionSummary();
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
const preflightResult = options?.preflightResult;
const workflowDelivery = (options as PromptOptionsWithWorkflowDelivery | undefined)?.__workflowDelivery;
Expand Down
82 changes: 82 additions & 0 deletions packages/coding-agent/src/core/agent-session-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Session summary generation for the resume picker.
*
* Runs once the agent goes idle, decides whether a fresh summary is worth generating, and
* persists the result. Generation itself lives in core/compaction/session-summarization.ts.
*/

import type { AgentSessionInternalSurface as AgentSession } from "./agent-session-methods.ts";
import { generateSessionSummary } from "./compaction/index.ts";
import { getLastConversationMessageId, getLatestSessionSummary } from "./session-manager-entries.ts";

/** Sessions shorter than this are already legible from their first message. */
const MIN_ENTRIES_FOR_SUMMARY = 4;

export async function _maybeGenerateSessionSummary(this: AgentSession): Promise<void> {
// --- Bail-outs, cheapest first ------------------------------------------------------
if (!this.settingsManager.getSessionSummarySettings().enabled) return;

// One-shot scripted runs should not pay for a background call; the process may exit before
// it lands. "tui" and "rpc" are the resumable, interactive modes.
if (this._extensionMode === "print" || this._extensionMode === "json") return;

if (this.isStreaming || this.isCompacting) return;
const model = this.model;
if (!model) return;

// Workflow-stage sessions are excluded from /resume entirely.
if (this.sessionManager.getHeader()?.internal) return;

const branch = this.sessionManager.getBranch();
if (branch.length < MIN_ENTRIES_FOR_SUMMARY) return;

const throughId = getLastConversationMessageId(branch);
if (!throughId) return;

// On a resumed session the in-memory anchor starts empty, so fall back to the persisted
// summary. Without this the first idle after every resume regenerates a summary that is
// already current.
const lastSummarized =
this._lastSummarizedMessageId ?? getLatestSessionSummary(this.sessionManager.getEntries())?.summarizedThroughId;
if (throughId === lastSummarized) return;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

// Claim the run. The token answers "is this still the current attempt, and is this state
// still mine to touch?" after the awaits below; the controller lives on the session so
// abortSessionSummary() can reach it from the prompt and shutdown paths.
this.abortSessionSummary();
const sessionSummaryToken = ++this._sessionSummaryToken;
this._sessionSummaryAbortController = new AbortController();
const signal = this._sessionSummaryAbortController.signal;

try {
const { apiKey, headers, baseUrl } = await this._getRequiredRequestAuth(model);
const result = await generateSessionSummary(branch, {
model,
apiKey,
headers,
baseUrl,
signal,
streamFn: this.agent.streamFunction,
retry: this.settingsManager.getRetrySettings(),
});
// Failures stay silent: nothing awaits this, and the picker falls back on its own.
if (result.aborted || result.error || !result.summary) return;

// A newer run, or a newer message, means this summary no longer describes the session.
if (this._sessionSummaryToken !== sessionSummaryToken) return;
if (getLastConversationMessageId(this.sessionManager.getBranch()) !== throughId) return;

this.sessionManager.appendSessionSummary(result.summary, throughId, result.usage);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
this._lastSummarizedMessageId = throughId;
} finally {
if (this._sessionSummaryToken === sessionSummaryToken) {
this._sessionSummaryAbortController = undefined;
}
}
}

export function abortSessionSummary(this: AgentSession): void {
this._sessionSummaryAbortController?.abort();
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

export const agentSessionSummaryMethods = { _maybeGenerateSessionSummary, abortSessionSummary };
5 changes: 5 additions & 0 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { agentSessionPostToolCompactionMethods } from "./agent-session-post-tool
import { agentSessionPromptMethods } from "./agent-session-prompt.ts";
import { agentSessionRetryMethods } from "./agent-session-retry.ts";
import { agentSessionStateMethods } from "./agent-session-state.ts";
import { agentSessionSummaryMethods } from "./agent-session-summary.ts";
import { agentSessionToolHooksMethods } from "./agent-session-tool-hooks.ts";
import { agentSessionToolRegistryMethods } from "./agent-session-tool-registry.ts";
import { agentSessionTreeMethods } from "./agent-session-tree.ts";
Expand Down Expand Up @@ -104,6 +105,9 @@ class AgentSessionBase {
protected _pendingPostToolCompactionGuard: PendingPostToolCompactionGuard | undefined = undefined;
protected _terminatingToolCallIds = new Set<string>();
protected _branchSummaryAbortController: AbortController | undefined = undefined;
protected _sessionSummaryAbortController: AbortController | undefined = undefined;
protected _sessionSummaryToken = 0;
protected _lastSummarizedMessageId: string | undefined = undefined;
protected _retryAbortController: AbortController | undefined = undefined;
protected _retryAttempt = 0;
protected _retryPromise: Promise<void> | undefined = undefined;
Expand Down Expand Up @@ -218,4 +222,5 @@ Object.assign(
agentSessionBashMethods,
agentSessionTreeMethods,
agentSessionExportMethods,
agentSessionSummaryMethods,
);
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
case "custom":
case "label":
case "session_info":
case "session_summary":
case "context_compaction":
return undefined;
}
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/core/compaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from "./planner-outcome.js";
export * from "./range-planner.js";
export * from "./range-planner-diagnostics.js";
export * from "./region-trimming.js";
export * from "./session-summarization.ts";
export * from "./transcript-serialization.js";
export * from "./utils.ts";
Loading