feat(coding-agent/session): typed CompactionCancelledError sentinel and CompactionOutcome
Introduce `CompactionCancelledError` and `CompactionOutcome` ("ok" |
"cancelled" | "failed") so callers can discriminate user-driven aborts
from generic failures via `instanceof`, instead of inspecting error
messages or `AbortError`-name strings.
`AgentSession.compact()`'s two abort-rejection sites now throw the
typed sentinel; the model-call wrapper normalizes AbortError-shaped
rejections to the sentinel only when the compaction's abort signal
is actually set, preserving every other exception unchanged so real
compaction bugs are not silently relabeled as cancellations.
`CommandController.executeCompaction` and `handleCompactCommand`
return `Promise<CompactionOutcome>`; the catch classifies via
`instanceof CompactionCancelledError`. Existing callers (`/compact`,
loop runner, auto-compact) ignore the return value — non-breaking.
Op: extend
This commit is contained in:
@@ -37,6 +37,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
||||
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
||||
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
||||
import type { AuthStorage } from "../../session/auth-storage";
|
||||
import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
|
||||
import type { NewSessionOptions } from "../../session/session-manager";
|
||||
import { outputMeta } from "../../tools/output-meta";
|
||||
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
||||
@@ -1071,16 +1072,16 @@ export class CommandController {
|
||||
this.ctx.ui.requestRender();
|
||||
}
|
||||
|
||||
async handleCompactCommand(customInstructions?: string): Promise<void> {
|
||||
async handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
|
||||
const entries = this.ctx.sessionManager.getEntries();
|
||||
const messageCount = entries.filter(e => e.type === "message").length;
|
||||
|
||||
if (messageCount < 2) {
|
||||
this.ctx.showWarning("Nothing to compact (no messages yet)");
|
||||
return;
|
||||
return "ok";
|
||||
}
|
||||
|
||||
await this.executeCompaction(customInstructions, false);
|
||||
return this.executeCompaction(customInstructions, false);
|
||||
}
|
||||
|
||||
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
||||
@@ -1098,7 +1099,10 @@ export class CommandController {
|
||||
}
|
||||
}
|
||||
|
||||
async executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto = false): Promise<void> {
|
||||
async executeCompaction(
|
||||
customInstructionsOrOptions?: string | CompactOptions,
|
||||
isAuto = false,
|
||||
): Promise<CompactionOutcome> {
|
||||
if (this.ctx.loadingAnimation) {
|
||||
this.ctx.loadingAnimation.stop();
|
||||
this.ctx.loadingAnimation = undefined;
|
||||
@@ -1122,6 +1126,7 @@ export class CommandController {
|
||||
this.ctx.statusContainer.addChild(compactingLoader);
|
||||
this.ctx.ui.requestRender();
|
||||
|
||||
let outcome: CompactionOutcome = "ok";
|
||||
try {
|
||||
const instructions = typeof customInstructionsOrOptions === "string" ? customInstructionsOrOptions : undefined;
|
||||
const options =
|
||||
@@ -1135,10 +1140,12 @@ export class CommandController {
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorTopBorder();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
||||
if (error instanceof CompactionCancelledError) {
|
||||
outcome = "cancelled";
|
||||
this.ctx.showError("Compaction cancelled");
|
||||
} else {
|
||||
outcome = "failed";
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.ctx.showError(`Compaction failed: ${message}`);
|
||||
}
|
||||
} finally {
|
||||
@@ -1147,6 +1154,7 @@ export class CommandController {
|
||||
this.ctx.editor.onEscape = originalOnEscape;
|
||||
}
|
||||
await this.ctx.flushCompactionQueue({ willRetry: false });
|
||||
return outcome;
|
||||
}
|
||||
|
||||
async handleHandoffCommand(customInstructions?: string): Promise<void> {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-
|
||||
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
||||
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
import type { CompactionOutcome } from "../session/compaction";
|
||||
import { HistoryStorage } from "../session/history-storage";
|
||||
import type { SessionContext, SessionManager } from "../session/session-manager";
|
||||
import { getRecentSessions } from "../session/session-manager";
|
||||
@@ -1727,7 +1728,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
await controller.handle(text);
|
||||
}
|
||||
|
||||
handleCompactCommand(customInstructions?: string): Promise<void> {
|
||||
handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
|
||||
return this.#commandController.handleCompactCommand(customInstructions);
|
||||
}
|
||||
|
||||
@@ -1735,7 +1736,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
return this.#commandController.handleHandoffCommand(customInstructions);
|
||||
}
|
||||
|
||||
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void> {
|
||||
executeCompaction(
|
||||
customInstructionsOrOptions?: string | CompactOptions,
|
||||
isAuto?: boolean,
|
||||
): Promise<CompactionOutcome> {
|
||||
return this.#commandController.executeCompaction(customInstructionsOrOptions, isAuto);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import type { MCPManager } from "../mcp";
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
import type { CompactionOutcome } from "../session/compaction";
|
||||
import type { HistoryStorage } from "../session/history-storage";
|
||||
import type { SessionContext, SessionManager } from "../session/session-manager";
|
||||
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
|
||||
@@ -207,13 +208,16 @@ export interface InteractiveModeContext {
|
||||
handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
|
||||
handleMCPCommand(text: string): Promise<void>;
|
||||
handleSSHCommand(text: string): Promise<void>;
|
||||
handleCompactCommand(customInstructions?: string): Promise<void>;
|
||||
handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome>;
|
||||
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
||||
handleMoveCommand(targetPath: string): Promise<void>;
|
||||
handleRenameCommand(title: string): Promise<void>;
|
||||
handleMemoryCommand(text: string): Promise<void>;
|
||||
handleSTTToggle(): Promise<void>;
|
||||
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|
||||
executeCompaction(
|
||||
customInstructionsOrOptions?: string | CompactOptions,
|
||||
isAuto?: boolean,
|
||||
): Promise<CompactionOutcome>;
|
||||
openInBrowser(urlOrPath: string): void;
|
||||
refreshSlashCommandState(cwd?: string): Promise<void>;
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ import { extractFileMentions, generateFileMentionMessages } from "../utils/file-
|
||||
import { buildNamedToolChoice } from "../utils/tool-choice";
|
||||
import type { AuthStorage } from "./auth-storage";
|
||||
import {
|
||||
CompactionCancelledError,
|
||||
type CompactionPreparation,
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
@@ -4503,7 +4504,7 @@ export class AgentSession {
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
throw new Error("Compaction cancelled");
|
||||
throw new CompactionCancelledError();
|
||||
}
|
||||
|
||||
if (result?.compaction) {
|
||||
@@ -4545,27 +4546,47 @@ export class AgentSession {
|
||||
details = hookCompaction.details;
|
||||
preserveData ??= hookCompaction.preserveData;
|
||||
} else {
|
||||
// Generate compaction result
|
||||
const result = await this.#compactWithFallbackModel(
|
||||
preparation,
|
||||
customInstructions,
|
||||
compactionAbortController.signal,
|
||||
{
|
||||
promptOverride: hookPrompt,
|
||||
extraContext: hookContext,
|
||||
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
||||
},
|
||||
);
|
||||
summary = result.summary;
|
||||
shortSummary = result.shortSummary;
|
||||
firstKeptEntryId = result.firstKeptEntryId;
|
||||
tokensBefore = result.tokensBefore;
|
||||
details = result.details;
|
||||
preserveData = { ...(preserveData ?? {}), ...(result.preserveData ?? {}) };
|
||||
// Generate compaction result. Only convert known abort-shaped
|
||||
// rejections (AbortError raised while the abort signal is set,
|
||||
// or an already-typed sentinel) into `CompactionCancelledError`
|
||||
// so downstream callers can discriminate cancel from generic
|
||||
// failure via `instanceof` without inspecting message strings.
|
||||
// Real compaction bugs (network, server, parsing, etc.) keep
|
||||
// their original shape — they must not be silently relabeled
|
||||
// as cancellations even if the signal happens to be aborted
|
||||
// for an unrelated reason. Assignments live inside the try
|
||||
// block because every catch path throws — the post-try reads
|
||||
// of the result-derived locals are reachable only on success.
|
||||
try {
|
||||
const result = await this.#compactWithFallbackModel(
|
||||
preparation,
|
||||
customInstructions,
|
||||
compactionAbortController.signal,
|
||||
{
|
||||
promptOverride: hookPrompt,
|
||||
extraContext: hookContext,
|
||||
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
||||
},
|
||||
);
|
||||
summary = result.summary;
|
||||
shortSummary = result.shortSummary;
|
||||
firstKeptEntryId = result.firstKeptEntryId;
|
||||
tokensBefore = result.tokensBefore;
|
||||
details = result.details;
|
||||
preserveData = { ...(preserveData ?? {}), ...(result.preserveData ?? {}) };
|
||||
} catch (err) {
|
||||
if (err instanceof CompactionCancelledError) {
|
||||
throw err;
|
||||
}
|
||||
if (compactionAbortController.signal.aborted && err instanceof Error && err.name === "AbortError") {
|
||||
throw new CompactionCancelledError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (compactionAbortController.signal.aborted) {
|
||||
throw new Error("Compaction cancelled");
|
||||
throw new CompactionCancelledError();
|
||||
}
|
||||
|
||||
this.sessionManager.appendCompaction(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Compaction error types.
|
||||
*
|
||||
* `CompactionCancelledError` is the canonical signal raised when a compaction
|
||||
* is explicitly aborted — operator Esc, extension hook returning `cancel`,
|
||||
* programmatic `session.abortCompaction()` call, or any other deliberate
|
||||
* abort source. Downstream callers (e.g. `executeCompaction`) discriminate
|
||||
* cancellation from other failures via `instanceof CompactionCancelledError`
|
||||
* rather than introspecting error messages or `name` fields — the typed
|
||||
* sentinel makes classification source-agnostic and refactor-stable.
|
||||
*/
|
||||
|
||||
export class CompactionCancelledError extends Error {
|
||||
readonly name = "CompactionCancelledError" as const;
|
||||
|
||||
constructor(message = "Compaction cancelled") {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outcome of a compaction attempt, surfaced by `CommandController.executeCompaction`
|
||||
* so callers (e.g. the plan-mode approval flow) can distinguish a deliberate abort
|
||||
* from an unrelated failure.
|
||||
*
|
||||
* "ok" — compaction completed; transcript was summarized.
|
||||
* "cancelled" — `CompactionCancelledError` was raised. Operator Esc, extension
|
||||
* hook, programmatic abort — all source-agnostic.
|
||||
* "failed" — any other rejection from `session.compact()`.
|
||||
*/
|
||||
export type CompactionOutcome = "ok" | "cancelled" | "failed";
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
export * from "./branch-summarization";
|
||||
export * from "./compaction";
|
||||
export * from "./errors";
|
||||
export * from "./utils";
|
||||
|
||||
Reference in New Issue
Block a user