@oh-my-pi/pi-coding-agent 16.2.13 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -7
- package/dist/cli.js +6033 -5982
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -62,8 +62,8 @@ import {
|
|
|
62
62
|
generateHandoffFromContext,
|
|
63
63
|
prepareCompaction,
|
|
64
64
|
renderHandoffPrompt,
|
|
65
|
+
resolveBudgetReserveTokens,
|
|
65
66
|
resolveThresholdTokens,
|
|
66
|
-
type SessionEntry,
|
|
67
67
|
type SessionMessageEntry,
|
|
68
68
|
type ShakeConfig,
|
|
69
69
|
type ShakeRegion,
|
|
@@ -128,6 +128,7 @@ import {
|
|
|
128
128
|
isBunTestRuntime,
|
|
129
129
|
isEnoent,
|
|
130
130
|
logger,
|
|
131
|
+
postmortem,
|
|
131
132
|
prompt,
|
|
132
133
|
relativePathWithinRoot,
|
|
133
134
|
Snowflake,
|
|
@@ -237,6 +238,7 @@ import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
|
237
238
|
import type { PlanModeState } from "../plan-mode/state";
|
|
238
239
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
239
240
|
import goalTodoContextPrompt from "../prompts/goals/goal-todo-context.md" with { type: "text" };
|
|
241
|
+
import parentIrcSteerTemplate from "../prompts/steering/parent-irc.md" with { type: "text" };
|
|
240
242
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
241
243
|
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
242
244
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
@@ -245,17 +247,20 @@ import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-remin
|
|
|
245
247
|
import interruptedThinkingTemplate from "../prompts/system/interrupted-thinking.md" with { type: "text" };
|
|
246
248
|
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
247
249
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
250
|
+
import midRunTodoNudgePrompt from "../prompts/system/mid-run-todo-nudge.md" with { type: "text" };
|
|
248
251
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
249
252
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
250
253
|
import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
|
|
251
254
|
type: "text",
|
|
252
255
|
};
|
|
256
|
+
import rewindReportTemplate from "../prompts/system/rewind-report.md" with { type: "text" };
|
|
253
257
|
import sideChannelNoToolsReminder from "../prompts/system/side-channel-no-tools.md" with { type: "text" };
|
|
254
258
|
import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
|
|
255
259
|
import toolCallLoopRedirectTemplate from "../prompts/system/tool-call-loop-redirect.md" with { type: "text" };
|
|
256
260
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
257
261
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
258
262
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
263
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
259
264
|
import {
|
|
260
265
|
deobfuscateAssistantContent,
|
|
261
266
|
deobfuscateSessionContext,
|
|
@@ -268,6 +273,7 @@ import {
|
|
|
268
273
|
AUTO_THINKING,
|
|
269
274
|
type ConfiguredThinkingLevel,
|
|
270
275
|
clampAutoThinkingEffort,
|
|
276
|
+
concreteThinkingLevel,
|
|
271
277
|
parseConfiguredThinkingLevel,
|
|
272
278
|
resolveProvisionalAutoLevel,
|
|
273
279
|
resolveThinkingLevelForModel,
|
|
@@ -289,7 +295,7 @@ import {
|
|
|
289
295
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
290
296
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
291
297
|
import { normalizeToolNames } from "../tools/builtin-names";
|
|
292
|
-
import type { CheckpointState } from "../tools/checkpoint";
|
|
298
|
+
import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
|
|
293
299
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
294
300
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
295
301
|
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
@@ -315,6 +321,14 @@ import {
|
|
|
315
321
|
shouldPromptCodexAutoRedeem,
|
|
316
322
|
} from "./codex-auto-reset";
|
|
317
323
|
import { findCompactMode } from "./compact-modes";
|
|
324
|
+
import {
|
|
325
|
+
collectPendingToolCalls,
|
|
326
|
+
SESSION_EXIT_CUSTOM_TYPE,
|
|
327
|
+
type SessionExitData,
|
|
328
|
+
summarizeToolArguments,
|
|
329
|
+
TOOL_EXECUTION_START_CUSTOM_TYPE,
|
|
330
|
+
type ToolExecutionStartData,
|
|
331
|
+
} from "./exit-diagnostics";
|
|
318
332
|
import {
|
|
319
333
|
type BashExecutionMessage,
|
|
320
334
|
type CustomMessage,
|
|
@@ -333,7 +347,7 @@ import {
|
|
|
333
347
|
import type { BuildSessionContextOptions, SessionContext } from "./session-context";
|
|
334
348
|
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
|
|
335
349
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
336
|
-
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
|
|
350
|
+
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionEntry } from "./session-entries";
|
|
337
351
|
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
338
352
|
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
339
353
|
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
@@ -345,6 +359,33 @@ import { YieldQueue } from "./yield-queue";
|
|
|
345
359
|
|
|
346
360
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
347
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Mutating tool results (`bash`/`eval`/`edit`/`write`/`ast_edit`) without the
|
|
364
|
+
* agent touching the `todo` tool that trip the mid-run reconciliation nudge.
|
|
365
|
+
* Read-only exploration (grep/read/glob/lsp) never ticks this: an agent
|
|
366
|
+
* researching for a long stretch has nothing to flip. Picked so a normal
|
|
367
|
+
* fix-verify loop (~3-6 mutations) never sees the nudge, but a sustained run
|
|
368
|
+
* of landed work without flipping any todos does. Without this nudge, long
|
|
369
|
+
* runs drive the live todo HUD to `0/N` until the final stop, then batch-flip
|
|
370
|
+
* to `N/N` (issue #3651).
|
|
371
|
+
*/
|
|
372
|
+
const MID_RUN_TODO_NUDGE_MUTATION_THRESHOLD = 12;
|
|
373
|
+
/** Mid-run nudges per prompt cycle. Deliberately tighter than
|
|
374
|
+
* `todo.reminders.max` (the stop-time budget): this is a gentle hidden hint,
|
|
375
|
+
* not an escalation ladder. */
|
|
376
|
+
const MID_RUN_TODO_NUDGE_MAX_PER_CYCLE = 2;
|
|
377
|
+
/** Tool results that count as landed work for the mid-run todo nudge. */
|
|
378
|
+
const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
|
379
|
+
bash: true,
|
|
380
|
+
eval: true,
|
|
381
|
+
edit: true,
|
|
382
|
+
write: true,
|
|
383
|
+
ast_edit: true,
|
|
384
|
+
};
|
|
385
|
+
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
|
386
|
+
* the model but never renders in the TUI or transcript. */
|
|
387
|
+
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
|
388
|
+
|
|
348
389
|
/** Abort reason for the Gemini reasoning-header runaway interrupt. Surfaced on the
|
|
349
390
|
* discarded assistant turn only; never reaches the model. */
|
|
350
391
|
const GEMINI_HEADER_INTERRUPT_REASON = "Interrupted: emit a tool call instead of more planning";
|
|
@@ -355,6 +396,61 @@ const GEMINI_TOOL_REMINDER_TYPE = "gemini-tool-call-reminder";
|
|
|
355
396
|
const THINKING_LOOP_REDIRECT_TYPE = "thinking-loop-redirect";
|
|
356
397
|
const TOOL_CALL_LOOP_REDIRECT_TYPE = "tool-call-loop-redirect";
|
|
357
398
|
|
|
399
|
+
function customMessageContentText(content: string | (TextContent | ImageContent)[]): string {
|
|
400
|
+
if (typeof content === "string") return content;
|
|
401
|
+
const parts: string[] = [];
|
|
402
|
+
for (const part of content) {
|
|
403
|
+
if (part.type === "text") parts.push(part.text);
|
|
404
|
+
}
|
|
405
|
+
return parts.join("\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function stringProperty(value: object, key: string): string | undefined {
|
|
409
|
+
const field = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
410
|
+
return typeof field === "string" ? field : undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function reportFromRewindReportContent(content: string): string {
|
|
414
|
+
const marker = "\nReport:\n";
|
|
415
|
+
const index = content.lastIndexOf(marker);
|
|
416
|
+
const report = index >= 0 ? content.slice(index + marker.length) : content;
|
|
417
|
+
return report.trim();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | undefined {
|
|
421
|
+
if (entry.type !== "custom_message" || entry.customType !== "rewind-report") return undefined;
|
|
422
|
+
const details = entry.details;
|
|
423
|
+
if (!details || typeof details !== "object") return undefined;
|
|
424
|
+
const startedAt = stringProperty(details, "startedAt");
|
|
425
|
+
const rewoundAt = stringProperty(details, "rewoundAt");
|
|
426
|
+
if (!startedAt || !rewoundAt) return undefined;
|
|
427
|
+
const report =
|
|
428
|
+
stringProperty(details, "report")?.trim() ||
|
|
429
|
+
reportFromRewindReportContent(customMessageContentText(entry.content));
|
|
430
|
+
return report.length > 0 ? { report, startedAt, rewoundAt } : undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isSuccessfulCheckpointEntry(entry: SessionEntry): entry is SessionMessageEntry & {
|
|
434
|
+
message: { role: "toolResult"; toolName: "checkpoint"; isError?: false };
|
|
435
|
+
} {
|
|
436
|
+
return (
|
|
437
|
+
entry.type === "message" &&
|
|
438
|
+
entry.message.role === "toolResult" &&
|
|
439
|
+
entry.message.toolName === "checkpoint" &&
|
|
440
|
+
entry.message.isError !== true
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function checkpointStartedAtFromEntry(entry: SessionEntry): string | undefined {
|
|
445
|
+
if (!isSuccessfulCheckpointEntry(entry)) return undefined;
|
|
446
|
+
const details = entry.message.details;
|
|
447
|
+
if (details && typeof details === "object") {
|
|
448
|
+
const startedAt = stringProperty(details, "startedAt");
|
|
449
|
+
if (startedAt) return startedAt;
|
|
450
|
+
}
|
|
451
|
+
return entry.timestamp;
|
|
452
|
+
}
|
|
453
|
+
|
|
358
454
|
// A side-channel assistant response is signed for the hidden prompt/history that
|
|
359
455
|
// produced it. If we persist that response under a different user turn, native
|
|
360
456
|
// replay anchors become invalid; keep only visible, non-cryptographic content.
|
|
@@ -421,11 +517,32 @@ const UNEXPECTED_STOP_MAX_RETRIES = 3;
|
|
|
421
517
|
const UNEXPECTED_STOP_TIMEOUT_MS = 4000;
|
|
422
518
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
423
519
|
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
520
|
+
/**
|
|
521
|
+
* Budget for callers on the user-visible `/quit` / `/exit` shutdown path that
|
|
522
|
+
* want to cap how long they wait for `MnemopiSessionState.dispose()` to finish
|
|
523
|
+
* its consolidate pass. Consolidate fires fresh LLM fact extractions, each a
|
|
524
|
+
* 1–3 s round-trip, so interactive shutdown passes this budget to keep the
|
|
525
|
+
* UI responsive. Callers that keep the process/session host alive must omit it
|
|
526
|
+
* so dispose still awaits the full consolidate-then-close pipeline.
|
|
527
|
+
*/
|
|
528
|
+
export const SHUTDOWN_CONSOLIDATE_BUDGET_MS = 1_500;
|
|
529
|
+
|
|
530
|
+
export interface AgentSessionDisposeOptions {
|
|
531
|
+
mnemopiConsolidateTimeoutMs?: number;
|
|
532
|
+
/**
|
|
533
|
+
* Postmortem reason that triggered this dispose (signal/fatal teardown
|
|
534
|
+
* paths). When set, the persisted `session_exit` diagnostic records it
|
|
535
|
+
* instead of the generic `"dispose"` used for normal programmatic disposal
|
|
536
|
+
* (`/quit`, test teardown, subagent completion).
|
|
537
|
+
*/
|
|
538
|
+
reason?: postmortem.Reason;
|
|
539
|
+
}
|
|
424
540
|
|
|
425
541
|
type CompactionCheckResult = Readonly<{
|
|
426
542
|
deferredHandoff: boolean;
|
|
427
543
|
continuationScheduled: boolean;
|
|
428
544
|
automaticContinuationBlocked?: boolean;
|
|
545
|
+
historyRewritten?: boolean;
|
|
429
546
|
}>;
|
|
430
547
|
|
|
431
548
|
const COMPACTION_CHECK_NONE: CompactionCheckResult = {
|
|
@@ -446,6 +563,18 @@ const COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION: CompactionCheckResult = {
|
|
|
446
563
|
automaticContinuationBlocked: true,
|
|
447
564
|
};
|
|
448
565
|
|
|
566
|
+
/**
|
|
567
|
+
* User-facing notice for a compaction dead end: maintenance freed too little
|
|
568
|
+
* to retry safely. `remedies` names the recovery actions available on the
|
|
569
|
+
* emitting path (the shake-rescue path can additionally offer `/shake images`).
|
|
570
|
+
*/
|
|
571
|
+
function compactionDeadEndWarning(remedies: string): string {
|
|
572
|
+
return (
|
|
573
|
+
"Compaction freed too little context to make progress — pausing automatic maintenance to avoid a compaction loop. " +
|
|
574
|
+
`The most recent turn alone is too large to reduce further; ${remedies} or switch to a larger-context model.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
449
578
|
/**
|
|
450
579
|
* Per-turn prune cache window. A tool result whose all-message suffix exceeds
|
|
451
580
|
* this is in the warm, already-sent prompt-cache prefix: re-writing it costs the
|
|
@@ -670,6 +799,14 @@ export interface AgentSessionConfig {
|
|
|
670
799
|
* teardown never tears down the shared servers.
|
|
671
800
|
*/
|
|
672
801
|
disconnectOwnedMcpManager?: () => Promise<void>;
|
|
802
|
+
/**
|
|
803
|
+
* Override the bundled system prompt used by automatic session-title
|
|
804
|
+
* generation paths (initial title + replan refresh). Source-of-truth is
|
|
805
|
+
* `TITLE_SYSTEM.md` discovered via {@link discoverTitleSystemPromptFile} and
|
|
806
|
+
* resolved through {@link resolvePromptInput}; refresh after a `/move`-style
|
|
807
|
+
* cwd change via {@link AgentSession.setTitleSystemPrompt}.
|
|
808
|
+
*/
|
|
809
|
+
titleSystemPrompt?: string;
|
|
673
810
|
}
|
|
674
811
|
|
|
675
812
|
/** Options for AgentSession.prompt() */
|
|
@@ -727,7 +864,7 @@ export interface RoleModelCycleResult {
|
|
|
727
864
|
export interface ResolvedRoleModel {
|
|
728
865
|
role: string;
|
|
729
866
|
model: Model;
|
|
730
|
-
thinkingLevel?:
|
|
867
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
731
868
|
explicitThinkingLevel: boolean;
|
|
732
869
|
}
|
|
733
870
|
|
|
@@ -870,6 +1007,7 @@ function parseRetryFallbackSelector(
|
|
|
870
1007
|
if (!trimmed) return undefined;
|
|
871
1008
|
const parsed = parseModelString(trimmed, {
|
|
872
1009
|
allowMaxAlias: true,
|
|
1010
|
+
allowAutoAlias: true,
|
|
873
1011
|
isLiteralModelId: (provider, id) => modelLookup?.find(provider, id) !== undefined,
|
|
874
1012
|
});
|
|
875
1013
|
if (!parsed) return undefined;
|
|
@@ -877,7 +1015,7 @@ function parseRetryFallbackSelector(
|
|
|
877
1015
|
raw: trimmed,
|
|
878
1016
|
provider: parsed.provider,
|
|
879
1017
|
id: parsed.id,
|
|
880
|
-
thinkingLevel: parsed.thinkingLevel,
|
|
1018
|
+
thinkingLevel: concreteThinkingLevel(parsed.thinkingLevel),
|
|
881
1019
|
};
|
|
882
1020
|
}
|
|
883
1021
|
|
|
@@ -1272,6 +1410,22 @@ type MessageEndPersistenceSlot = {
|
|
|
1272
1410
|
release: () => void;
|
|
1273
1411
|
};
|
|
1274
1412
|
|
|
1413
|
+
type PostPromptSkipReason = "aborted" | "stale-generation";
|
|
1414
|
+
|
|
1415
|
+
type AgentContinueSkipReason =
|
|
1416
|
+
| PostPromptSkipReason
|
|
1417
|
+
| "session-unavailable"
|
|
1418
|
+
| "should-continue-false"
|
|
1419
|
+
| "post-restore-unavailable";
|
|
1420
|
+
|
|
1421
|
+
type ScheduledAgentContinueOptions = {
|
|
1422
|
+
delayMs?: number;
|
|
1423
|
+
generation?: number;
|
|
1424
|
+
shouldContinue?: () => boolean;
|
|
1425
|
+
onSkip?: (reason: AgentContinueSkipReason) => void;
|
|
1426
|
+
onError?: () => void;
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1275
1429
|
const REPLAN_TITLE_CONTEXT_TURN_LIMIT = 6;
|
|
1276
1430
|
|
|
1277
1431
|
type SessionTitleSource = "auto" | "user";
|
|
@@ -1350,6 +1504,8 @@ export class AgentSession {
|
|
|
1350
1504
|
|
|
1351
1505
|
// Event subscription state
|
|
1352
1506
|
#unsubscribeAgent?: () => void;
|
|
1507
|
+
#cancelExitRecorder?: () => void;
|
|
1508
|
+
#exitRecorded = false;
|
|
1353
1509
|
#unsubscribeAppendOnly?: () => void;
|
|
1354
1510
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
1355
1511
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
@@ -1418,8 +1574,25 @@ export class AgentSession {
|
|
|
1418
1574
|
* instruction") does not drive 1/3 → 2/3 → 3/3 without user input.
|
|
1419
1575
|
*/
|
|
1420
1576
|
#todoReminderAwaitingProgress = false;
|
|
1577
|
+
/**
|
|
1578
|
+
* Successful mutating tool results (bash/eval/edit/write/ast_edit) since the
|
|
1579
|
+
* agent last touched the `todo` tool. Drives {@link #takeMidRunTodoNudge} so
|
|
1580
|
+
* the live HUD stays in sync with actual progress instead of flipping
|
|
1581
|
+
* `0/N -> N/N` only at the very end of a long run (issue #3651). Read-only
|
|
1582
|
+
* tools and errored results never tick it. Reset to 0 on any `todo` tool
|
|
1583
|
+
* result, on a nudge fire (cooldown), on a stop-time reminder, and at every
|
|
1584
|
+
* new-prompt / clear / handoff lifecycle boundary.
|
|
1585
|
+
*/
|
|
1586
|
+
#mutationsSinceLastTodoTouch = 0;
|
|
1587
|
+
/** Mid-run nudges fired this prompt cycle; capped by
|
|
1588
|
+
* {@link MID_RUN_TODO_NUDGE_MAX_PER_CYCLE}, reset with the counter above. */
|
|
1589
|
+
#midRunNudgeCount = 0;
|
|
1421
1590
|
#todoPhases: TodoPhase[] = [];
|
|
1422
1591
|
#replanTitleRefreshInFlight: Promise<void> | undefined = undefined;
|
|
1592
|
+
/** Resolved TITLE_SYSTEM.md override applied to every automatic session-title
|
|
1593
|
+
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
1594
|
+
* the session cwd changes. */
|
|
1595
|
+
#titleSystemPrompt: string | undefined;
|
|
1423
1596
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1424
1597
|
|
|
1425
1598
|
// Bash execution state
|
|
@@ -1447,8 +1620,10 @@ export class AgentSession {
|
|
|
1447
1620
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
1448
1621
|
#evalExecutionDisposing = false;
|
|
1449
1622
|
|
|
1450
|
-
// Incoming IRC messages received while a turn was streaming
|
|
1451
|
-
//
|
|
1623
|
+
// Incoming IRC messages received while a turn was streaming. Parent IRCs
|
|
1624
|
+
// enter the steering queue; peer IRCs enter the interrupt queue and drain as
|
|
1625
|
+
// asides at the next boundary; passive IRC records stay in the aside queue.
|
|
1626
|
+
#pendingIrcInterrupts: CustomMessage[] = [];
|
|
1452
1627
|
#pendingIrcAsides: CustomMessage[] = [];
|
|
1453
1628
|
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
1454
1629
|
#agentId: string | undefined;
|
|
@@ -1591,6 +1766,7 @@ export class AgentSession {
|
|
|
1591
1766
|
#pruneToolDescriptions = false;
|
|
1592
1767
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1593
1768
|
#pendingRewindReport: string | undefined = undefined;
|
|
1769
|
+
#lastCompletedRewind: CompletedRewindState | undefined = undefined;
|
|
1594
1770
|
#rewoundToolResultIds = new Set<string>();
|
|
1595
1771
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1596
1772
|
/**
|
|
@@ -1673,16 +1849,17 @@ export class AgentSession {
|
|
|
1673
1849
|
this.#resumeStrandedIrcAsides();
|
|
1674
1850
|
}
|
|
1675
1851
|
|
|
1676
|
-
/** IRC
|
|
1677
|
-
* poll — land in
|
|
1678
|
-
* gate (agent.hasQueuedMessages()) does not count
|
|
1679
|
-
* responds to the peer. Skip only when a queued steer/follow-up will itself drive a
|
|
1680
|
-
* whose aside poll already consumes these (no double-wake). */
|
|
1852
|
+
/** IRC records that arrive after the loop's final aside poll — or while an abort skipped that
|
|
1853
|
+
* poll — land in pending IRC queues with no loop left to drain them; the queued-message drain's
|
|
1854
|
+
* gate (agent.hasQueuedMessages()) does not count peer IRC interrupts. Once idle, wake a turn so
|
|
1855
|
+
* the agent responds to the peer. Skip only when a queued steer/follow-up will itself drive a
|
|
1856
|
+
* resume turn whose aside poll already consumes these (no double-wake). */
|
|
1681
1857
|
#resumeStrandedIrcAsides(): void {
|
|
1682
1858
|
if (this.#isDisposed || this.isStreaming) return;
|
|
1683
|
-
if (this.#pendingIrcAsides.length === 0) return;
|
|
1859
|
+
if (this.#pendingIrcInterrupts.length === 0 && this.#pendingIrcAsides.length === 0) return;
|
|
1684
1860
|
if (this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages()) return;
|
|
1685
|
-
const records = this.#pendingIrcAsides;
|
|
1861
|
+
const records = [...this.#pendingIrcInterrupts, ...this.#pendingIrcAsides];
|
|
1862
|
+
this.#pendingIrcInterrupts = [];
|
|
1686
1863
|
this.#pendingIrcAsides = [];
|
|
1687
1864
|
this.#wakeForIrc(records);
|
|
1688
1865
|
}
|
|
@@ -1809,6 +1986,7 @@ export class AgentSession {
|
|
|
1809
1986
|
this.#advisorSharedInstructions = config.advisorSharedInstructions;
|
|
1810
1987
|
this.#advisorContextPrompt = config.advisorContextPrompt;
|
|
1811
1988
|
this.#advisorConfigs = config.advisorConfigs;
|
|
1989
|
+
this.#titleSystemPrompt = config.titleSystemPrompt;
|
|
1812
1990
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1813
1991
|
this.#validateRetryFallbackChains();
|
|
1814
1992
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
@@ -1892,13 +2070,19 @@ export class AgentSession {
|
|
|
1892
2070
|
},
|
|
1893
2071
|
});
|
|
1894
2072
|
// Background-job completions / late diagnostics are pulled into the run at
|
|
1895
|
-
// each step boundary as non-interrupting asides
|
|
1896
|
-
//
|
|
2073
|
+
// each step boundary as non-interrupting asides. Peer IRCs share the aside
|
|
2074
|
+
// injection boundary, but also expose a non-consuming interrupt peek so
|
|
2075
|
+
// `job poll` / `irc wait` can return early before the boundary drains them.
|
|
2076
|
+
this.agent.hasIrcInterrupts = () => this.#pendingIrcInterrupts.length > 0;
|
|
1897
2077
|
this.agent.setAsideMessageProvider(() => {
|
|
1898
|
-
const pendingIrc = this.#pendingIrcAsides;
|
|
2078
|
+
const pendingIrc = [...this.#pendingIrcInterrupts, ...this.#pendingIrcAsides];
|
|
2079
|
+
this.#pendingIrcInterrupts = [];
|
|
1899
2080
|
this.#pendingIrcAsides = [];
|
|
1900
2081
|
const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
|
|
1901
2082
|
thunks.push(...this.yieldQueue.drainLazy());
|
|
2083
|
+
// Mid-run todo reconciliation — evaluated at injection time so a turn
|
|
2084
|
+
// that flips a todo just before this poll suppresses the nudge.
|
|
2085
|
+
thunks.push(() => this.#takeMidRunTodoNudge());
|
|
1902
2086
|
return thunks;
|
|
1903
2087
|
});
|
|
1904
2088
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
@@ -1987,10 +2171,15 @@ export class AgentSession {
|
|
|
1987
2171
|
);
|
|
1988
2172
|
},
|
|
1989
2173
|
});
|
|
2174
|
+
this.#cancelExitRecorder = postmortem.register(`agent-session:${this.sessionManager.getSessionId()}`, reason => {
|
|
2175
|
+
this.#recordSessionExit(reason);
|
|
2176
|
+
});
|
|
1990
2177
|
|
|
1991
2178
|
this.#advisorEnabled = this.settings.get("advisor.enabled") as boolean;
|
|
1992
2179
|
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
1993
2180
|
|
|
2181
|
+
this.#rehydrateCheckpointRewindState();
|
|
2182
|
+
|
|
1994
2183
|
// Always subscribe to agent events for internal handling
|
|
1995
2184
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
1996
2185
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
@@ -2102,7 +2291,7 @@ export class AgentSession {
|
|
|
2102
2291
|
if (config.model) {
|
|
2103
2292
|
const resolved = resolveModelOverride([config.model], this.#modelRegistry, this.settings);
|
|
2104
2293
|
model = resolved.model;
|
|
2105
|
-
thinkingLevel = resolved.thinkingLevel;
|
|
2294
|
+
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
2106
2295
|
if (!model) {
|
|
2107
2296
|
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2108
2297
|
continue;
|
|
@@ -2116,7 +2305,7 @@ export class AgentSession {
|
|
|
2116
2305
|
continue;
|
|
2117
2306
|
}
|
|
2118
2307
|
model = sel.model;
|
|
2119
|
-
thinkingLevel = sel.thinkingLevel;
|
|
2308
|
+
thinkingLevel = concreteThinkingLevel(sel.thinkingLevel);
|
|
2120
2309
|
}
|
|
2121
2310
|
const advisorModel = model;
|
|
2122
2311
|
const advisorName = config.name;
|
|
@@ -2157,6 +2346,12 @@ export class AgentSession {
|
|
|
2157
2346
|
// Mirror the SDK's provider-shaping options (streamFn/onPayload/...,
|
|
2158
2347
|
// providerSessionState, promptCacheKey, transformProviderContext) so each
|
|
2159
2348
|
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
2349
|
+
// `promptCacheKey` preserves an explicitly pinned provider cache key
|
|
2350
|
+
// unchanged so tan/shared-session advisor calls read the exact shard the
|
|
2351
|
+
// parent turn populated, while keeping only `sessionId` advisor-scoped;
|
|
2352
|
+
// sessions without a pinned key fall back to the advisor session id for
|
|
2353
|
+
// stable advisor-local caching (see can1357/oh-my-pi#3639).
|
|
2354
|
+
const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorSessionId;
|
|
2160
2355
|
const advisorAgent = new Agent({
|
|
2161
2356
|
initialState: {
|
|
2162
2357
|
systemPrompt,
|
|
@@ -2166,7 +2361,7 @@ export class AgentSession {
|
|
|
2166
2361
|
},
|
|
2167
2362
|
appendOnlyContext,
|
|
2168
2363
|
sessionId: advisorSessionId,
|
|
2169
|
-
promptCacheKey:
|
|
2364
|
+
promptCacheKey: advisorPromptCacheKey,
|
|
2170
2365
|
providerSessionState: this.#providerSessionState,
|
|
2171
2366
|
preferWebsockets: this.#preferWebsockets,
|
|
2172
2367
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
@@ -2791,6 +2986,67 @@ export class AgentSession {
|
|
|
2791
2986
|
this.#emit({ type: "notice", level, message, source });
|
|
2792
2987
|
}
|
|
2793
2988
|
|
|
2989
|
+
#recordToolExecutionStart(event: Extract<AgentEvent, { type: "tool_execution_start" }>): void {
|
|
2990
|
+
const data: ToolExecutionStartData = {
|
|
2991
|
+
toolCallId: event.toolCallId,
|
|
2992
|
+
toolName: event.toolName,
|
|
2993
|
+
startedAt: new Date().toISOString(),
|
|
2994
|
+
};
|
|
2995
|
+
// The assistant message already persists the full arguments; store only
|
|
2996
|
+
// the command/path projection the resume warning renders.
|
|
2997
|
+
const args = summarizeToolArguments(event.args);
|
|
2998
|
+
if (args) data.args = args;
|
|
2999
|
+
if (event.intent) data.intent = event.intent;
|
|
3000
|
+
this.sessionManager.appendCustomEntry(TOOL_EXECUTION_START_CUSTOM_TYPE, data);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
#recordSessionExit(reason: postmortem.Reason | "dispose"): void {
|
|
3004
|
+
if (this.#exitRecorded) return;
|
|
3005
|
+
this.#exitRecorded = true;
|
|
3006
|
+
const pendingToolCalls = collectPendingToolCalls(this.sessionManager.getBranch());
|
|
3007
|
+
if (
|
|
3008
|
+
pendingToolCalls.length === 0 &&
|
|
3009
|
+
!this.sessionManager.getEntries().some(entry => entry.type === "message" && entry.message.role === "assistant")
|
|
3010
|
+
) {
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const kind: SessionExitData["kind"] =
|
|
3014
|
+
reason === "dispose" || reason === postmortem.Reason.MANUAL
|
|
3015
|
+
? "normal"
|
|
3016
|
+
: reason === postmortem.Reason.UNCAUGHT_EXCEPTION || reason === postmortem.Reason.UNHANDLED_REJECTION
|
|
3017
|
+
? "fatal"
|
|
3018
|
+
: reason === postmortem.Reason.EXIT
|
|
3019
|
+
? "process_exit"
|
|
3020
|
+
: "signal";
|
|
3021
|
+
const data: SessionExitData = {
|
|
3022
|
+
reason,
|
|
3023
|
+
kind,
|
|
3024
|
+
recordedAt: new Date().toISOString(),
|
|
3025
|
+
};
|
|
3026
|
+
if (pendingToolCalls.length > 0) data.pendingToolCalls = pendingToolCalls;
|
|
3027
|
+
try {
|
|
3028
|
+
this.sessionManager.appendCustomEntry(SESSION_EXIT_CUSTOM_TYPE, data);
|
|
3029
|
+
this.sessionManager.flushSync();
|
|
3030
|
+
// Only pending tool calls or an abnormal teardown are noteworthy; a
|
|
3031
|
+
// clean dispose logs at debug so routine exits don't read as problems.
|
|
3032
|
+
const exitLog = pendingToolCalls.length > 0 || kind !== "normal" ? logger.warn : logger.debug;
|
|
3033
|
+
exitLog("Session exit recorded", {
|
|
3034
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
3035
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
3036
|
+
reason,
|
|
3037
|
+
kind,
|
|
3038
|
+
pendingToolCalls: pendingToolCalls.length,
|
|
3039
|
+
});
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
logger.error("Failed to record session exit", {
|
|
3042
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
3043
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
3044
|
+
reason,
|
|
3045
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
2794
3050
|
#queuedExtensionEvents: Promise<void> = Promise.resolve();
|
|
2795
3051
|
|
|
2796
3052
|
#queueExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
@@ -3043,6 +3299,25 @@ export class AgentSession {
|
|
|
3043
3299
|
}
|
|
3044
3300
|
|
|
3045
3301
|
#processAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
3302
|
+
// Step the mid-run todo counter synchronously, BEFORE any await in this
|
|
3303
|
+
// handler. The agent loop's next-turn `getAsideMessages` poll can run
|
|
3304
|
+
// before queued microtasks drain, so `#takeMidRunTodoNudge` MUST see the
|
|
3305
|
+
// freshest counter — otherwise a turn that just invoked `todo` could
|
|
3306
|
+
// trip a spurious nudge against stale state, and a turn that just hit
|
|
3307
|
+
// the threshold could fail to nudge until a later turn (issue #3651).
|
|
3308
|
+
// Pure in-memory math — no ordering requirement vs persistence or
|
|
3309
|
+
// session-event fan-out. Keyed on toolResult (not the assistant toolCall
|
|
3310
|
+
// turn) so planned-but-aborted or permission-denied calls never count,
|
|
3311
|
+
// and only successful mutating tools tick — read-only exploration is
|
|
3312
|
+
// not progress an agent could mark done.
|
|
3313
|
+
if (event.type === "message_end" && event.message.role === "toolResult") {
|
|
3314
|
+
const { toolName, isError } = event.message;
|
|
3315
|
+
if (toolName === "todo") {
|
|
3316
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
3317
|
+
} else if (!isError && MID_RUN_TODO_NUDGE_MUTATING_TOOLS[toolName]) {
|
|
3318
|
+
this.#mutationsSinceLastTodoTouch++;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3046
3321
|
// Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
|
|
3047
3322
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
3048
3323
|
// Invariant (must hold across refactors): this branch precedes the
|
|
@@ -3108,6 +3383,10 @@ export class AgentSession {
|
|
|
3108
3383
|
});
|
|
3109
3384
|
}
|
|
3110
3385
|
|
|
3386
|
+
if (event.type === "tool_execution_start") {
|
|
3387
|
+
this.#recordToolExecutionStart(event);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3111
3390
|
try {
|
|
3112
3391
|
await this.#emitSessionEvent(displayEvent);
|
|
3113
3392
|
} catch (error) {
|
|
@@ -3336,6 +3615,7 @@ export class AgentSession {
|
|
|
3336
3615
|
startedAt: details?.startedAt ?? new Date().toISOString(),
|
|
3337
3616
|
};
|
|
3338
3617
|
this.#pendingRewindReport = undefined;
|
|
3618
|
+
this.#lastCompletedRewind = undefined;
|
|
3339
3619
|
}
|
|
3340
3620
|
if (toolName === "rewind" && !isError && this.#checkpointState) {
|
|
3341
3621
|
const detailReport = typeof details?.report === "string" ? details.report.trim() : "";
|
|
@@ -3536,7 +3816,11 @@ export class AgentSession {
|
|
|
3536
3816
|
this.#trackPostPromptTask(compactionTask);
|
|
3537
3817
|
compactionResult = await compactionTask;
|
|
3538
3818
|
}
|
|
3539
|
-
//
|
|
3819
|
+
// Stop-time todo reconciliation only fires at a text-only final stop. A run
|
|
3820
|
+
// that ends still mid-tool-use (deadline hit, context full, etc.) skips the
|
|
3821
|
+
// reminder so we don't pile a follow-up onto an already in-flight turn.
|
|
3822
|
+
// Mid-run sync is handled separately via #takeMidRunTodoNudge so a long
|
|
3823
|
+
// tool-use loop still gets prodded to keep the live HUD honest (issue #3651).
|
|
3540
3824
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
3541
3825
|
if (hasToolCalls) {
|
|
3542
3826
|
await emitAgentEndNotification();
|
|
@@ -3624,7 +3908,7 @@ export class AgentSession {
|
|
|
3624
3908
|
|
|
3625
3909
|
#schedulePostPromptTask(
|
|
3626
3910
|
task: (signal: AbortSignal) => Promise<void>,
|
|
3627
|
-
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
3911
|
+
options?: { delayMs?: number; generation?: number; onSkip?: (reason: PostPromptSkipReason) => void },
|
|
3628
3912
|
): void {
|
|
3629
3913
|
const delayMs = options?.delayMs ?? 0;
|
|
3630
3914
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
@@ -3637,11 +3921,11 @@ export class AgentSession {
|
|
|
3637
3921
|
}
|
|
3638
3922
|
}
|
|
3639
3923
|
if (signal.aborted) {
|
|
3640
|
-
options?.onSkip?.();
|
|
3924
|
+
options?.onSkip?.("aborted");
|
|
3641
3925
|
return;
|
|
3642
3926
|
}
|
|
3643
3927
|
if (options?.generation !== undefined && this.#promptGeneration !== options.generation) {
|
|
3644
|
-
options.onSkip?.();
|
|
3928
|
+
options.onSkip?.("stale-generation");
|
|
3645
3929
|
return;
|
|
3646
3930
|
}
|
|
3647
3931
|
await task(signal);
|
|
@@ -3649,13 +3933,12 @@ export class AgentSession {
|
|
|
3649
3933
|
this.#trackPostPromptTask(scheduled);
|
|
3650
3934
|
}
|
|
3651
3935
|
|
|
3652
|
-
#
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
}): void {
|
|
3936
|
+
#skipAgentContinue(reason: AgentContinueSkipReason, options: ScheduledAgentContinueOptions | undefined): void {
|
|
3937
|
+
logger.debug("agent.continue skipped after scheduling", { reason });
|
|
3938
|
+
options?.onSkip?.(reason);
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
#scheduleAgentContinue(options?: ScheduledAgentContinueOptions): void {
|
|
3659
3942
|
this.#schedulePostPromptTask(
|
|
3660
3943
|
async signal => {
|
|
3661
3944
|
// Defense in depth: if compaction/handoff slipped onto the post-prompt queue
|
|
@@ -3664,24 +3947,25 @@ export class AgentSession {
|
|
|
3664
3947
|
// reset. The first-class fix is in #checkCompaction/the agent_end handler,
|
|
3665
3948
|
// but this guard catches anything that bypasses that path.
|
|
3666
3949
|
if (signal.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) {
|
|
3667
|
-
options
|
|
3950
|
+
this.#skipAgentContinue("session-unavailable", options);
|
|
3668
3951
|
return;
|
|
3669
3952
|
}
|
|
3670
3953
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
3671
|
-
options
|
|
3954
|
+
this.#skipAgentContinue("should-continue-false", options);
|
|
3672
3955
|
return;
|
|
3673
3956
|
}
|
|
3674
3957
|
this.#beginInFlight();
|
|
3675
3958
|
try {
|
|
3676
3959
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
3677
3960
|
if (signal.aborted || this.#isDisposed) {
|
|
3678
|
-
options
|
|
3961
|
+
this.#skipAgentContinue("post-restore-unavailable", options);
|
|
3679
3962
|
return;
|
|
3680
3963
|
}
|
|
3681
3964
|
await this.agent.continue();
|
|
3682
3965
|
} catch (error) {
|
|
3683
3966
|
logger.warn("agent.continue failed after scheduling", {
|
|
3684
3967
|
error: error instanceof Error ? error.message : String(error),
|
|
3968
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
3685
3969
|
});
|
|
3686
3970
|
options?.onError?.();
|
|
3687
3971
|
} finally {
|
|
@@ -3691,7 +3975,7 @@ export class AgentSession {
|
|
|
3691
3975
|
{
|
|
3692
3976
|
delayMs: options?.delayMs,
|
|
3693
3977
|
generation: options?.generation,
|
|
3694
|
-
onSkip: options
|
|
3978
|
+
onSkip: reason => this.#skipAgentContinue(reason, options),
|
|
3695
3979
|
},
|
|
3696
3980
|
);
|
|
3697
3981
|
}
|
|
@@ -5097,6 +5381,7 @@ export class AgentSession {
|
|
|
5097
5381
|
this.#flushPendingIrcAsides();
|
|
5098
5382
|
this.yieldQueue.clear();
|
|
5099
5383
|
this.agent.setAsideMessageProvider(undefined);
|
|
5384
|
+
this.agent.hasIrcInterrupts = undefined;
|
|
5100
5385
|
this.#stopAdvisorRuntime();
|
|
5101
5386
|
this.#evalExecutionDisposing = true;
|
|
5102
5387
|
}
|
|
@@ -5104,9 +5389,24 @@ export class AgentSession {
|
|
|
5104
5389
|
/**
|
|
5105
5390
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
5106
5391
|
* Call this when completely done with the session.
|
|
5392
|
+
*
|
|
5393
|
+
* Idempotent: concurrent or repeated calls share one settled promise. The
|
|
5394
|
+
* keypress `InteractiveMode.shutdown()` path and the postmortem
|
|
5395
|
+
* `SIGTERM`/`SIGHUP`/`uncaughtException` callback can both target this
|
|
5396
|
+
* method, so a second invocation must never re-emit `session_shutdown` or
|
|
5397
|
+
* double-drain the owned `AsyncJobManager` (issue #4080).
|
|
5107
5398
|
*/
|
|
5108
|
-
|
|
5399
|
+
#disposeCall?: Promise<void>;
|
|
5400
|
+
dispose(options: AgentSessionDisposeOptions = {}): Promise<void> {
|
|
5401
|
+
if (!this.#disposeCall) this.#disposeCall = this.#doDispose(options);
|
|
5402
|
+
return this.#disposeCall;
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
async #doDispose(options: AgentSessionDisposeOptions = {}): Promise<void> {
|
|
5109
5406
|
this.beginDispose();
|
|
5407
|
+
this.#recordSessionExit(options.reason ?? "dispose");
|
|
5408
|
+
this.#cancelExitRecorder?.();
|
|
5409
|
+
this.#cancelExitRecorder = undefined;
|
|
5110
5410
|
try {
|
|
5111
5411
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
5112
5412
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -5219,7 +5519,7 @@ export class AgentSession {
|
|
|
5219
5519
|
this.setHindsightSessionState(undefined);
|
|
5220
5520
|
hindsightState?.dispose();
|
|
5221
5521
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
5222
|
-
await mnemopiState?.dispose();
|
|
5522
|
+
await mnemopiState?.dispose({ timeoutMs: options.mnemopiConsolidateTimeoutMs });
|
|
5223
5523
|
// Tear down the embeddings subprocess AFTER mnemopi state.dispose:
|
|
5224
5524
|
// consolidate-on-dispose may still call `embed()` to store the final
|
|
5225
5525
|
// memories, and that round-trips through the worker we are about to
|
|
@@ -6426,13 +6726,71 @@ export class AgentSession {
|
|
|
6426
6726
|
this.agent.setTools(activeTools);
|
|
6427
6727
|
}
|
|
6428
6728
|
|
|
6729
|
+
#clearCheckpointRuntimeState(): void {
|
|
6730
|
+
this.#checkpointState = undefined;
|
|
6731
|
+
this.#pendingRewindReport = undefined;
|
|
6732
|
+
this.#lastCompletedRewind = undefined;
|
|
6733
|
+
this.#rewoundToolResultIds.clear();
|
|
6734
|
+
}
|
|
6735
|
+
|
|
6736
|
+
/**
|
|
6737
|
+
* Rebuild checkpoint/rewind runtime state from the current branch. Handles two
|
|
6738
|
+
* cases surfaced by session resume, `switchSession()` reloading the same file,
|
|
6739
|
+
* and tree navigation:
|
|
6740
|
+
* - The branch's most recent checkpoint has already been rewound → restore
|
|
6741
|
+
* `#lastCompletedRewind` so a repeat `rewind` call receives the
|
|
6742
|
+
* "checkpoint already completed" recovery guidance.
|
|
6743
|
+
* - The branch's most recent checkpoint has NOT been rewound (e.g. the run
|
|
6744
|
+
* was aborted between `checkpoint` and `rewind`) → restore
|
|
6745
|
+
* `#checkpointState` so the next `rewind` call can complete the
|
|
6746
|
+
* checkpoint instead of failing with "No active checkpoint".
|
|
6747
|
+
*/
|
|
6748
|
+
#rehydrateCheckpointRewindState(): void {
|
|
6749
|
+
this.#clearCheckpointRuntimeState();
|
|
6750
|
+
let completed: CompletedRewindState | undefined;
|
|
6751
|
+
let pending: { entryId: string; startedAt: string; messageCount: number } | undefined;
|
|
6752
|
+
let messageCount = 0;
|
|
6753
|
+
for (const entry of this.sessionManager.getBranch()) {
|
|
6754
|
+
if (entry.type === "message") messageCount++;
|
|
6755
|
+
if (isSuccessfulCheckpointEntry(entry)) {
|
|
6756
|
+
completed = undefined;
|
|
6757
|
+
pending = {
|
|
6758
|
+
entryId: entry.id,
|
|
6759
|
+
startedAt: checkpointStartedAtFromEntry(entry) ?? entry.timestamp,
|
|
6760
|
+
messageCount,
|
|
6761
|
+
};
|
|
6762
|
+
continue;
|
|
6763
|
+
}
|
|
6764
|
+
const completedFromEntry = completedRewindFromEntry(entry);
|
|
6765
|
+
if (completedFromEntry) {
|
|
6766
|
+
completed = completedFromEntry;
|
|
6767
|
+
pending = undefined;
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
if (pending) {
|
|
6771
|
+
this.#checkpointState = {
|
|
6772
|
+
checkpointEntryId: pending.entryId,
|
|
6773
|
+
startedAt: pending.startedAt,
|
|
6774
|
+
checkpointMessageCount: pending.messageCount,
|
|
6775
|
+
};
|
|
6776
|
+
return;
|
|
6777
|
+
}
|
|
6778
|
+
this.#lastCompletedRewind = completed;
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6429
6781
|
getCheckpointState(): CheckpointState | undefined {
|
|
6430
6782
|
return this.#checkpointState;
|
|
6431
6783
|
}
|
|
6432
6784
|
|
|
6785
|
+
getLastCompletedRewind(): CompletedRewindState | undefined {
|
|
6786
|
+
return this.#lastCompletedRewind;
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6433
6789
|
setCheckpointState(state: CheckpointState | undefined): void {
|
|
6434
6790
|
this.#checkpointState = state;
|
|
6435
|
-
if (
|
|
6791
|
+
if (state) {
|
|
6792
|
+
this.#lastCompletedRewind = undefined;
|
|
6793
|
+
} else {
|
|
6436
6794
|
this.#pendingRewindReport = undefined;
|
|
6437
6795
|
}
|
|
6438
6796
|
}
|
|
@@ -6523,9 +6881,8 @@ export class AgentSession {
|
|
|
6523
6881
|
|
|
6524
6882
|
const planFilePath = this.#planReferencePath;
|
|
6525
6883
|
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath, this.#localProtocolOptions());
|
|
6526
|
-
let planContent: string;
|
|
6527
6884
|
try {
|
|
6528
|
-
|
|
6885
|
+
await fs.promises.access(resolvedPlanPath, fs.constants.R_OK);
|
|
6529
6886
|
} catch (error) {
|
|
6530
6887
|
if (isEnoent(error)) {
|
|
6531
6888
|
return null;
|
|
@@ -6535,7 +6892,6 @@ export class AgentSession {
|
|
|
6535
6892
|
|
|
6536
6893
|
const content = prompt.render(planModeReferencePrompt, {
|
|
6537
6894
|
planFilePath,
|
|
6538
|
-
planContent,
|
|
6539
6895
|
});
|
|
6540
6896
|
|
|
6541
6897
|
this.#planReferenceSent = true;
|
|
@@ -6966,6 +7322,8 @@ export class AgentSession {
|
|
|
6966
7322
|
// Reset todo reminder count on new user prompt
|
|
6967
7323
|
this.#todoReminderCount = 0;
|
|
6968
7324
|
this.#todoReminderAwaitingProgress = false;
|
|
7325
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
7326
|
+
this.#midRunNudgeCount = 0;
|
|
6969
7327
|
this.#emptyStopRetryCount = 0;
|
|
6970
7328
|
this.#unexpectedStopRetryCount = 0;
|
|
6971
7329
|
// A new prompt cycle starts: drop any sticky yield-termination from the
|
|
@@ -7817,6 +8175,7 @@ export class AgentSession {
|
|
|
7817
8175
|
sessionId,
|
|
7818
8176
|
this.model,
|
|
7819
8177
|
provider => this.agent.metadataForProvider(provider),
|
|
8178
|
+
this.#titleSystemPrompt,
|
|
7820
8179
|
);
|
|
7821
8180
|
if (!title) return;
|
|
7822
8181
|
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
@@ -7826,6 +8185,21 @@ export class AgentSession {
|
|
|
7826
8185
|
await setSessionName.call(this.sessionManager, title, "auto", "replan");
|
|
7827
8186
|
}
|
|
7828
8187
|
|
|
8188
|
+
/** Currently-applied {@link TITLE_SYSTEM.md} override, or undefined when the
|
|
8189
|
+
* bundled prompt is in effect. Consumed by {@link InteractiveMode} so the
|
|
8190
|
+
* first-input title path and the replan refresh share one source. */
|
|
8191
|
+
get titleSystemPrompt(): string | undefined {
|
|
8192
|
+
return this.#titleSystemPrompt;
|
|
8193
|
+
}
|
|
8194
|
+
|
|
8195
|
+
/** Replace the title-generation system prompt override. Called by
|
|
8196
|
+
* {@link InteractiveMode.refreshTitleSystemPrompt} after the session cwd
|
|
8197
|
+
* changes (e.g. `/move` relocation) so the next replan refresh resolves
|
|
8198
|
+
* against the destination project's override. */
|
|
8199
|
+
setTitleSystemPrompt(prompt: string | undefined): void {
|
|
8200
|
+
this.#titleSystemPrompt = prompt;
|
|
8201
|
+
}
|
|
8202
|
+
|
|
7829
8203
|
#syncTodoPhasesFromBranch(): void {
|
|
7830
8204
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
7831
8205
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
@@ -7982,6 +8356,7 @@ export class AgentSession {
|
|
|
7982
8356
|
await this.sessionManager.flush();
|
|
7983
8357
|
}
|
|
7984
8358
|
await this.sessionManager.newSession(options);
|
|
8359
|
+
this.#clearCheckpointRuntimeState();
|
|
7985
8360
|
this.setTodoPhases([]);
|
|
7986
8361
|
this.#freshProviderSessionId = undefined;
|
|
7987
8362
|
this.#syncAgentSessionId();
|
|
@@ -8006,6 +8381,8 @@ export class AgentSession {
|
|
|
8006
8381
|
|
|
8007
8382
|
this.#todoReminderCount = 0;
|
|
8008
8383
|
this.#todoReminderAwaitingProgress = false;
|
|
8384
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
8385
|
+
this.#midRunNudgeCount = 0;
|
|
8009
8386
|
this.#planReferenceSent = false;
|
|
8010
8387
|
this.#planReferencePath = "local://PLAN.md";
|
|
8011
8388
|
this.#resetAdvisorSessionState();
|
|
@@ -8254,8 +8631,16 @@ export class AgentSession {
|
|
|
8254
8631
|
|
|
8255
8632
|
if (models.length === 0) return undefined;
|
|
8256
8633
|
|
|
8634
|
+
// Trust the recorded role only while its resolved model still IS the
|
|
8635
|
+
// active model. A model switch through another surface (alt+m, retry
|
|
8636
|
+
// fallback, /model) or a role re-configuration leaves the recorded role
|
|
8637
|
+
// pointing at a model the session no longer runs; cycling from that
|
|
8638
|
+
// stale slot lands on the wrong neighbor and reads as a skipped entry.
|
|
8257
8639
|
const lastRole = this.sessionManager.getLastModelChangeRole();
|
|
8258
8640
|
let currentIndex = lastRole ? models.findIndex(entry => entry.role === lastRole) : -1;
|
|
8641
|
+
if (currentIndex !== -1 && !modelsAreEqual(models[currentIndex].model, currentModel)) {
|
|
8642
|
+
currentIndex = -1;
|
|
8643
|
+
}
|
|
8259
8644
|
if (currentIndex === -1) {
|
|
8260
8645
|
currentIndex = models.findIndex(entry => modelsAreEqual(entry.model, currentModel));
|
|
8261
8646
|
}
|
|
@@ -9383,6 +9768,7 @@ export class AgentSession {
|
|
|
9383
9768
|
await this.sessionManager.flush();
|
|
9384
9769
|
this.#cancelOwnAsyncJobs();
|
|
9385
9770
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
9771
|
+
this.#clearCheckpointRuntimeState();
|
|
9386
9772
|
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
9387
9773
|
// steers/follow-ups (RPC/SDK steer()/followUp() issued during the handoff, or a
|
|
9388
9774
|
// pre-loader TUI steer) so they survive into the post-handoff session instead of
|
|
@@ -9403,6 +9789,8 @@ export class AgentSession {
|
|
|
9403
9789
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
9404
9790
|
this.#todoReminderCount = 0;
|
|
9405
9791
|
this.#todoReminderAwaitingProgress = false;
|
|
9792
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
9793
|
+
this.#midRunNudgeCount = 0;
|
|
9406
9794
|
|
|
9407
9795
|
// Inject the handoff document as a custom message
|
|
9408
9796
|
const handoffContent = createHandoffContext(handoffText);
|
|
@@ -9957,15 +10345,27 @@ export class AgentSession {
|
|
|
9957
10345
|
});
|
|
9958
10346
|
}
|
|
9959
10347
|
|
|
9960
|
-
#removeAssistantMessageFromActiveContext(
|
|
10348
|
+
#removeAssistantMessageFromActiveContext(
|
|
10349
|
+
assistantMessage: AssistantMessage,
|
|
10350
|
+
reason = "assistant-context-cleanup",
|
|
10351
|
+
): void {
|
|
9961
10352
|
const messages = this.agent.state.messages;
|
|
9962
10353
|
const lastMessage = messages[messages.length - 1];
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
this.#isSameAssistantMessage(lastMessage as AssistantMessage, assistantMessage)
|
|
9966
|
-
) {
|
|
10354
|
+
const lastAssistant: AssistantMessage | undefined = lastMessage?.role === "assistant" ? lastMessage : undefined;
|
|
10355
|
+
if (lastAssistant !== undefined && this.#isSameAssistantMessage(lastAssistant, assistantMessage)) {
|
|
9967
10356
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
10357
|
+
return;
|
|
9968
10358
|
}
|
|
10359
|
+
// A miss means the failed turn is still in active context (or was never
|
|
10360
|
+
// there); log just enough to explain why the identity check failed.
|
|
10361
|
+
logger.debug("agent active context assistant removal missed", {
|
|
10362
|
+
reason,
|
|
10363
|
+
lastRole: lastMessage?.role,
|
|
10364
|
+
candidateTimestamp: assistantMessage.timestamp,
|
|
10365
|
+
lastTimestamp: lastAssistant?.timestamp,
|
|
10366
|
+
candidateStopReason: assistantMessage.stopReason,
|
|
10367
|
+
lastStopReason: lastAssistant?.stopReason,
|
|
10368
|
+
});
|
|
9969
10369
|
}
|
|
9970
10370
|
|
|
9971
10371
|
/**
|
|
@@ -9986,15 +10386,17 @@ export class AgentSession {
|
|
|
9986
10386
|
* Drop the failed assistant turn from persisted history, run
|
|
9987
10387
|
* {@link #runAutoCompaction} for an `overflow` / `incomplete` recovery, and
|
|
9988
10388
|
* restore the assistant entry if compaction did not actually commit
|
|
9989
|
-
* anything (no usable model/preparation, hook cancel,
|
|
10389
|
+
* anything (no usable model/preparation, hook cancel, compaction error,
|
|
10390
|
+
* or a no-progress automatic-continuation block before any summary was
|
|
10391
|
+
* written).
|
|
9990
10392
|
*
|
|
9991
10393
|
* Compaction has to see a clean branch — otherwise its `prepareCompaction`
|
|
9992
10394
|
* pass would keep the failed turn in the kept region and the retry would
|
|
9993
|
-
* replay it. But a
|
|
9994
|
-
* summary means no recovery is in progress,
|
|
9995
|
-
*
|
|
9996
|
-
*
|
|
9997
|
-
*
|
|
10395
|
+
* replay it. But a return that was not paired with a fresh compaction
|
|
10396
|
+
* summary or a successful history rewrite means no recovery is in progress,
|
|
10397
|
+
* even if queued user input gets drained next. Restoring the failed turn
|
|
10398
|
+
* before that continuation preserves the visible stop reason and rebuilds the
|
|
10399
|
+
* active assistant tail that `Agent.continue()` needs to dequeue follow-ups.
|
|
9998
10400
|
*/
|
|
9999
10401
|
async #runRecoveryCompactionWithRollback(
|
|
10000
10402
|
reason: "overflow" | "incomplete",
|
|
@@ -10005,17 +10407,25 @@ export class AgentSession {
|
|
|
10005
10407
|
const compactionEntryBefore = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
10006
10408
|
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
10007
10409
|
const result = await this.#runAutoCompaction(reason, true, false, allowDefer, options);
|
|
10008
|
-
const
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
const compactionEntryAfter = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
10012
|
-
if (compactionEntryAfter === compactionEntryBefore) {
|
|
10013
|
-
this.sessionManager.appendMessage(assistantMessage);
|
|
10014
|
-
}
|
|
10410
|
+
const compactionEntryAfter = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
10411
|
+
if (result.historyRewritten !== true && compactionEntryAfter === compactionEntryBefore) {
|
|
10412
|
+
this.#restoreFailedAssistantTurn(assistantMessage);
|
|
10015
10413
|
}
|
|
10016
10414
|
return result;
|
|
10017
10415
|
}
|
|
10018
10416
|
|
|
10417
|
+
#restoreFailedAssistantTurn(assistantMessage: AssistantMessage): void {
|
|
10418
|
+
this.sessionManager.appendMessage(assistantMessage);
|
|
10419
|
+
const lastMessage = this.agent.state.messages.at(-1);
|
|
10420
|
+
if (
|
|
10421
|
+
lastMessage?.role === "assistant" &&
|
|
10422
|
+
this.#isSameAssistantMessage(lastMessage as AssistantMessage, assistantMessage)
|
|
10423
|
+
) {
|
|
10424
|
+
return;
|
|
10425
|
+
}
|
|
10426
|
+
this.agent.appendMessage(assistantMessage);
|
|
10427
|
+
}
|
|
10428
|
+
|
|
10019
10429
|
/**
|
|
10020
10430
|
* Drop an assistant turn from BOTH the live agent context and the persisted
|
|
10021
10431
|
* session branch (reparenting the leaf to the turn's parent), so a discarded
|
|
@@ -10108,8 +10518,16 @@ export class AgentSession {
|
|
|
10108
10518
|
});
|
|
10109
10519
|
this.sessionManager.branchWithSummary(null, report, { startedAt: checkpointState.startedAt });
|
|
10110
10520
|
}
|
|
10111
|
-
const
|
|
10112
|
-
|
|
10521
|
+
const rewoundAt = new Date().toISOString();
|
|
10522
|
+
const details = { report, startedAt: checkpointState.startedAt, rewoundAt };
|
|
10523
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
10524
|
+
"rewind-report",
|
|
10525
|
+
prompt.render(rewindReportTemplate, { report }),
|
|
10526
|
+
false,
|
|
10527
|
+
details,
|
|
10528
|
+
"agent",
|
|
10529
|
+
);
|
|
10530
|
+
this.#lastCompletedRewind = { report, startedAt: checkpointState.startedAt, rewoundAt };
|
|
10113
10531
|
|
|
10114
10532
|
if (activeMessages) {
|
|
10115
10533
|
for (const message of activeMessages) {
|
|
@@ -10395,6 +10813,11 @@ export class AgentSession {
|
|
|
10395
10813
|
timestamp: Date.now(),
|
|
10396
10814
|
};
|
|
10397
10815
|
|
|
10816
|
+
// A stop-time reminder starts a fresh reminder runway. Without resetting
|
|
10817
|
+
// the mid-run counter here, a run that stopped just below the threshold
|
|
10818
|
+
// would spend its stale pre-reminder count and fire "Mid-run reminder 2/3"
|
|
10819
|
+
// after only a little post-reminder work.
|
|
10820
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
10398
10821
|
this.#todoReminderAwaitingProgress = true;
|
|
10399
10822
|
// Inject reminder and persist it so the JSONL transcript matches model context.
|
|
10400
10823
|
this.agent.appendMessage(reminderMessage);
|
|
@@ -10403,6 +10826,68 @@ export class AgentSession {
|
|
|
10403
10826
|
return true;
|
|
10404
10827
|
}
|
|
10405
10828
|
|
|
10829
|
+
/**
|
|
10830
|
+
* Build the next mid-run todo reconciliation nudge when the agent has landed
|
|
10831
|
+
* {@link MID_RUN_TODO_NUDGE_MUTATION_THRESHOLD} mutating tool results without
|
|
10832
|
+
* invoking the `todo` tool and incomplete items remain. Returns the hidden
|
|
10833
|
+
* (`display: false`) custom message when it should fire, or `null` to skip.
|
|
10834
|
+
* Called once per turn via the aside provider; mutates internal counters when
|
|
10835
|
+
* it fires so the caller does not need to track delivery state.
|
|
10836
|
+
*
|
|
10837
|
+
* Deliberately a SEPARATE concept from {@link #checkTodoCompletion}'s
|
|
10838
|
+
* stop-time reminder: this is a gentle model-only hint (no `todo_reminder`
|
|
10839
|
+
* event, no TUI render, no escalation counter, own per-cycle budget), while
|
|
10840
|
+
* the stop-time reminder is the user-visible escalation ladder. Without this
|
|
10841
|
+
* nudge, long runs drive the live HUD to `0/N` until the final stop, then
|
|
10842
|
+
* batch-flip to `N/N` (issue #3651).
|
|
10843
|
+
*/
|
|
10844
|
+
#takeMidRunTodoNudge(): AgentMessage | null {
|
|
10845
|
+
if (this.#mutationsSinceLastTodoTouch < MID_RUN_TODO_NUDGE_MUTATION_THRESHOLD) return null;
|
|
10846
|
+
if (this.#midRunNudgeCount >= MID_RUN_TODO_NUDGE_MAX_PER_CYCLE) return null;
|
|
10847
|
+
if (!this.settings.get("todo.enabled")) return null;
|
|
10848
|
+
if (!this.settings.get("todo.reminders")) return null;
|
|
10849
|
+
// Plan-mode runs are authoring a plan file, not implementing it; todos
|
|
10850
|
+
// don't apply, mirroring {@link #createEagerTodoPrelude}.
|
|
10851
|
+
if (this.#planModeState?.enabled) return null;
|
|
10852
|
+
// Tool discovery / explicit active-tool lists can hide `todo` from this
|
|
10853
|
+
// run while `todo.enabled` remains true (e.g. `setActiveToolsByName`
|
|
10854
|
+
// restricting the slate). Mirror {@link #createEagerTodoPrelude}'s
|
|
10855
|
+
// guard so we never ask the model to call a tool that is not in its
|
|
10856
|
+
// schema — the request would fabricate an unknown tool call.
|
|
10857
|
+
if (!this.getActiveToolNames().includes("todo")) return null;
|
|
10858
|
+
|
|
10859
|
+
const incomplete = this.getTodoPhases()
|
|
10860
|
+
.flatMap(phase => phase.tasks)
|
|
10861
|
+
.filter(task => task.status === "pending" || task.status === "in_progress");
|
|
10862
|
+
if (incomplete.length === 0) return null;
|
|
10863
|
+
|
|
10864
|
+
// Reset the mutation counter so the nudge has another full runway before
|
|
10865
|
+
// the next fire; #midRunNudgeCount caps total nudges per prompt cycle.
|
|
10866
|
+
this.#mutationsSinceLastTodoTouch = 0;
|
|
10867
|
+
this.#midRunNudgeCount++;
|
|
10868
|
+
|
|
10869
|
+
const { toolRefs } = this.#buildEagerPreludeContext();
|
|
10870
|
+
const reminder = prompt.render(midRunTodoNudgePrompt, {
|
|
10871
|
+
toolRefs,
|
|
10872
|
+
incompleteCount: incomplete.length,
|
|
10873
|
+
plural: incomplete.length !== 1,
|
|
10874
|
+
});
|
|
10875
|
+
|
|
10876
|
+
logger.debug("Mid-run todo nudge fired", {
|
|
10877
|
+
incomplete: incomplete.length,
|
|
10878
|
+
nudge: this.#midRunNudgeCount,
|
|
10879
|
+
});
|
|
10880
|
+
|
|
10881
|
+
return {
|
|
10882
|
+
role: "custom",
|
|
10883
|
+
customType: MID_RUN_TODO_NUDGE_MESSAGE_TYPE,
|
|
10884
|
+
content: reminder,
|
|
10885
|
+
display: false,
|
|
10886
|
+
attribution: "agent",
|
|
10887
|
+
timestamp: Date.now(),
|
|
10888
|
+
};
|
|
10889
|
+
}
|
|
10890
|
+
|
|
10406
10891
|
/**
|
|
10407
10892
|
* Attempt context promotion to a larger model.
|
|
10408
10893
|
* Returns true if promotion succeeded (caller should retry without compacting).
|
|
@@ -10758,6 +11243,7 @@ export class AgentSession {
|
|
|
10758
11243
|
|
|
10759
11244
|
const parsed = parseModelString(trimmedTarget, {
|
|
10760
11245
|
allowMaxAlias: true,
|
|
11246
|
+
allowAutoAlias: true,
|
|
10761
11247
|
isLiteralModelId: (provider, id) =>
|
|
10762
11248
|
availableModels.some(model => model.provider === provider && model.id === id),
|
|
10763
11249
|
});
|
|
@@ -11157,12 +11643,14 @@ export class AgentSession {
|
|
|
11157
11643
|
* `0.8 × 170k = 136k` and was wrongly refused (PR #3412 review).
|
|
11158
11644
|
*
|
|
11159
11645
|
* Measures residual context against the usable budget (`contextWindow - reserve`).
|
|
11160
|
-
* The default absolute reserve can exceed bundled small-context windows,
|
|
11161
|
-
*
|
|
11162
|
-
*
|
|
11163
|
-
*
|
|
11164
|
-
*
|
|
11165
|
-
*
|
|
11646
|
+
* The default absolute reserve can exceed bundled small-context windows, or
|
|
11647
|
+
* nearly consume a 16k-class window; those known-impossible defaults fall
|
|
11648
|
+
* back to the proportional 15% reserve. Explicit valid reserves still define
|
|
11649
|
+
* the usable prompt budget so retries do not enter headroom the user
|
|
11650
|
+
* intentionally reserved. Callers MUST
|
|
11651
|
+
* invoke this AFTER dropping the failed assistant from `this.messages`, so
|
|
11652
|
+
* the just-failed turn (which the retry prompt will not include) is excluded
|
|
11653
|
+
* from the estimate.
|
|
11166
11654
|
*
|
|
11167
11655
|
* When the model/window is unknown we cannot evaluate the budget, so we
|
|
11168
11656
|
* optimistically allow the retry (preserving prior behavior).
|
|
@@ -11175,10 +11663,7 @@ export class AgentSession {
|
|
|
11175
11663
|
this.getContextUsage({ contextWindow })?.tokens ?? 0,
|
|
11176
11664
|
this.#estimateStoredContextTokens(),
|
|
11177
11665
|
);
|
|
11178
|
-
const
|
|
11179
|
-
const defaultReserveTokens = Math.floor(contextWindow * 0.15);
|
|
11180
|
-
const fitReserveTokens = Math.min(reserveTokens, defaultReserveTokens);
|
|
11181
|
-
const fitBudget = Math.max(0, contextWindow - fitReserveTokens);
|
|
11666
|
+
const fitBudget = Math.max(0, contextWindow - resolveBudgetReserveTokens(contextWindow, compactionSettings));
|
|
11182
11667
|
return residualTokens <= fitBudget;
|
|
11183
11668
|
}
|
|
11184
11669
|
|
|
@@ -11364,7 +11849,10 @@ export class AgentSession {
|
|
|
11364
11849
|
if (continuationScheduled) {
|
|
11365
11850
|
this.#scheduleAutoContinuePrompt(generation);
|
|
11366
11851
|
}
|
|
11367
|
-
return
|
|
11852
|
+
return {
|
|
11853
|
+
...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
|
|
11854
|
+
historyRewritten: true,
|
|
11855
|
+
};
|
|
11368
11856
|
}
|
|
11369
11857
|
}
|
|
11370
11858
|
|
|
@@ -11409,15 +11897,25 @@ export class AgentSession {
|
|
|
11409
11897
|
willRetry: false,
|
|
11410
11898
|
skipped: true,
|
|
11411
11899
|
});
|
|
11412
|
-
|
|
11900
|
+
const noProgressDeadEnd = reason !== "idle";
|
|
11901
|
+
let continuationScheduled = false;
|
|
11902
|
+
if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
11413
11903
|
this.#scheduleAgentContinue({
|
|
11414
11904
|
delayMs: 100,
|
|
11415
11905
|
generation,
|
|
11416
11906
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
11417
11907
|
});
|
|
11418
|
-
|
|
11908
|
+
continuationScheduled = true;
|
|
11419
11909
|
}
|
|
11420
|
-
|
|
11910
|
+
if (noProgressDeadEnd) {
|
|
11911
|
+
this.emitNotice(
|
|
11912
|
+
"warning",
|
|
11913
|
+
compactionDeadEndWarning("shrink it (e.g. clear large tool output)"),
|
|
11914
|
+
"compaction",
|
|
11915
|
+
);
|
|
11916
|
+
}
|
|
11917
|
+
if (continuationScheduled) return COMPACTION_CHECK_CONTINUATION;
|
|
11918
|
+
return noProgressDeadEnd ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION : COMPACTION_CHECK_NONE;
|
|
11421
11919
|
}
|
|
11422
11920
|
|
|
11423
11921
|
let hookCompaction: CompactionResult | undefined;
|
|
@@ -11828,7 +12326,7 @@ export class AgentSession {
|
|
|
11828
12326
|
if (noProgressDeadEnd) {
|
|
11829
12327
|
this.emitNotice(
|
|
11830
12328
|
"warning",
|
|
11831
|
-
"
|
|
12329
|
+
compactionDeadEndWarning("clear large tool output, run `/shake images` to drop attached images,"),
|
|
11832
12330
|
"compaction",
|
|
11833
12331
|
);
|
|
11834
12332
|
}
|
|
@@ -11991,7 +12489,17 @@ export class AgentSession {
|
|
|
11991
12489
|
});
|
|
11992
12490
|
continuationScheduled = true;
|
|
11993
12491
|
}
|
|
11994
|
-
|
|
12492
|
+
if (!reclaimed) {
|
|
12493
|
+
return willRetry && continuationScheduled
|
|
12494
|
+
? { ...COMPACTION_CHECK_CONTINUATION, historyRewritten: true }
|
|
12495
|
+
: continuationScheduled
|
|
12496
|
+
? COMPACTION_CHECK_CONTINUATION
|
|
12497
|
+
: COMPACTION_CHECK_NONE;
|
|
12498
|
+
}
|
|
12499
|
+
return {
|
|
12500
|
+
...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
|
|
12501
|
+
historyRewritten: true,
|
|
12502
|
+
};
|
|
11995
12503
|
} catch (error) {
|
|
11996
12504
|
if (signal.aborted) {
|
|
11997
12505
|
await this.#emitSessionEvent({
|
|
@@ -12662,7 +13170,7 @@ export class AgentSession {
|
|
|
12662
13170
|
});
|
|
12663
13171
|
|
|
12664
13172
|
// Remove the failed assistant message from active context before retrying.
|
|
12665
|
-
this.#removeAssistantMessageFromActiveContext(message);
|
|
13173
|
+
this.#removeAssistantMessageFromActiveContext(message, "auto-retry");
|
|
12666
13174
|
|
|
12667
13175
|
// A thinking/response loop retried into identical context loops again. Inject a
|
|
12668
13176
|
// hidden redirect so the retried turn sees a directive to break the repeated
|
|
@@ -12850,6 +13358,7 @@ export class AgentSession {
|
|
|
12850
13358
|
onChunk,
|
|
12851
13359
|
signal: abortController.signal,
|
|
12852
13360
|
sessionKey: this.sessionId,
|
|
13361
|
+
cwd,
|
|
12853
13362
|
timeout: clampTimeout("bash") * 1000,
|
|
12854
13363
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
12855
13364
|
useUserShell: options?.useUserShell,
|
|
@@ -13109,46 +13618,53 @@ export class AgentSession {
|
|
|
13109
13618
|
// =========================================================================
|
|
13110
13619
|
|
|
13111
13620
|
/**
|
|
13112
|
-
* Surfaces (and consumes) IRC incoming
|
|
13113
|
-
*
|
|
13621
|
+
* Surfaces (and consumes) pending IRC incoming records before the next model
|
|
13622
|
+
* step can inject them automatically.
|
|
13114
13623
|
*
|
|
13115
13624
|
* The inbox tool injects the formatted body into the tool result, so the
|
|
13116
|
-
* model sees it once via the result. Leaving the record in
|
|
13117
|
-
*
|
|
13118
|
-
*
|
|
13119
|
-
* also drains here.
|
|
13625
|
+
* model sees it once via the result. Leaving the record in either pending
|
|
13626
|
+
* IRC queue would deliver it a second time at the next step boundary —
|
|
13627
|
+
* including on `peek`, which is why peek also drains here.
|
|
13120
13628
|
*/
|
|
13121
13629
|
drainPendingIrcInboxMessages(agentId: string): IrcMessage[] {
|
|
13122
13630
|
const messages: IrcMessage[] = [];
|
|
13123
|
-
const
|
|
13124
|
-
|
|
13125
|
-
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13631
|
+
const remainingInterrupts: CustomMessage[] = [];
|
|
13632
|
+
const remainingAsides: CustomMessage[] = [];
|
|
13633
|
+
const queues = [
|
|
13634
|
+
{ records: this.#pendingIrcInterrupts, remaining: remainingInterrupts },
|
|
13635
|
+
{ records: this.#pendingIrcAsides, remaining: remainingAsides },
|
|
13636
|
+
];
|
|
13637
|
+
for (const queue of queues) {
|
|
13638
|
+
for (const record of queue.records) {
|
|
13639
|
+
if (record.customType !== "irc:incoming") {
|
|
13640
|
+
queue.remaining.push(record);
|
|
13641
|
+
continue;
|
|
13642
|
+
}
|
|
13643
|
+
const details = record.details;
|
|
13644
|
+
if (!details || typeof details !== "object") {
|
|
13645
|
+
queue.remaining.push(record);
|
|
13646
|
+
continue;
|
|
13647
|
+
}
|
|
13648
|
+
const id = Reflect.get(details, "id");
|
|
13649
|
+
const from = Reflect.get(details, "from");
|
|
13650
|
+
const body = Reflect.get(details, "message");
|
|
13651
|
+
const replyTo = Reflect.get(details, "replyTo");
|
|
13652
|
+
if (typeof id !== "string" || typeof from !== "string" || typeof body !== "string") {
|
|
13653
|
+
queue.remaining.push(record);
|
|
13654
|
+
continue;
|
|
13655
|
+
}
|
|
13656
|
+
messages.push({
|
|
13657
|
+
id,
|
|
13658
|
+
from,
|
|
13659
|
+
to: agentId,
|
|
13660
|
+
body,
|
|
13661
|
+
ts: record.timestamp,
|
|
13662
|
+
...(typeof replyTo === "string" ? { replyTo } : {}),
|
|
13663
|
+
});
|
|
13141
13664
|
}
|
|
13142
|
-
messages.push({
|
|
13143
|
-
id,
|
|
13144
|
-
from,
|
|
13145
|
-
to: agentId,
|
|
13146
|
-
body,
|
|
13147
|
-
ts: record.timestamp,
|
|
13148
|
-
...(typeof replyTo === "string" ? { replyTo } : {}),
|
|
13149
|
-
});
|
|
13150
13665
|
}
|
|
13151
|
-
this.#
|
|
13666
|
+
this.#pendingIrcInterrupts = remainingInterrupts;
|
|
13667
|
+
this.#pendingIrcAsides = remainingAsides;
|
|
13152
13668
|
return messages;
|
|
13153
13669
|
}
|
|
13154
13670
|
|
|
@@ -13186,6 +13702,7 @@ export class AgentSession {
|
|
|
13186
13702
|
message: msg.body,
|
|
13187
13703
|
replyTo: msg.replyTo ?? "",
|
|
13188
13704
|
autoReplied: autoReply,
|
|
13705
|
+
interrupting: this.isStreaming,
|
|
13189
13706
|
}),
|
|
13190
13707
|
display: true,
|
|
13191
13708
|
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
@@ -13194,7 +13711,18 @@ export class AgentSession {
|
|
|
13194
13711
|
};
|
|
13195
13712
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
13196
13713
|
if (this.isStreaming) {
|
|
13197
|
-
|
|
13714
|
+
const recipientParentId = AgentRegistry.global().get(msg.to)?.parentId;
|
|
13715
|
+
if (recipientParentId === msg.from) {
|
|
13716
|
+
this.agent.steer({
|
|
13717
|
+
role: "user",
|
|
13718
|
+
content: prompt.render(parentIrcSteerTemplate, { from: msg.from, message: msg.body }),
|
|
13719
|
+
attribution: "agent",
|
|
13720
|
+
timestamp: msg.ts,
|
|
13721
|
+
steering: true,
|
|
13722
|
+
});
|
|
13723
|
+
} else {
|
|
13724
|
+
this.#pendingIrcInterrupts.push(record);
|
|
13725
|
+
}
|
|
13198
13726
|
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
13199
13727
|
return "injected";
|
|
13200
13728
|
}
|
|
@@ -13405,8 +13933,9 @@ export class AgentSession {
|
|
|
13405
13933
|
* of the next prompt so the model still sees them.
|
|
13406
13934
|
*/
|
|
13407
13935
|
#flushPendingIrcAsides(): void {
|
|
13408
|
-
if (this.#pendingIrcAsides.length === 0) return;
|
|
13409
|
-
const records = this.#pendingIrcAsides;
|
|
13936
|
+
if (this.#pendingIrcInterrupts.length === 0 && this.#pendingIrcAsides.length === 0) return;
|
|
13937
|
+
const records = [...this.#pendingIrcInterrupts, ...this.#pendingIrcAsides];
|
|
13938
|
+
this.#pendingIrcInterrupts = [];
|
|
13410
13939
|
this.#pendingIrcAsides = [];
|
|
13411
13940
|
for (const record of records) {
|
|
13412
13941
|
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
@@ -13495,6 +14024,15 @@ export class AgentSession {
|
|
|
13495
14024
|
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
13496
14025
|
: undefined;
|
|
13497
14026
|
|
|
14027
|
+
// Snapshot the full checkpoint runtime state: the success path calls
|
|
14028
|
+
// #rehydrateCheckpointRewindState(), which clears and rebuilds all four
|
|
14029
|
+
// fields from the target branch. On rollback every one must be restored,
|
|
14030
|
+
// or a failed switch leaks the target session's checkpoint state.
|
|
14031
|
+
const previousCheckpointState = this.#checkpointState;
|
|
14032
|
+
const previousPendingRewindReport = this.#pendingRewindReport;
|
|
14033
|
+
const previousLastCompletedRewind = this.#lastCompletedRewind;
|
|
14034
|
+
const previousRewoundToolResultIds = new Set(this.#rewoundToolResultIds);
|
|
14035
|
+
|
|
13498
14036
|
this.agent.clearAllQueues();
|
|
13499
14037
|
this.#pendingNextTurnMessages = [];
|
|
13500
14038
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -13514,6 +14052,7 @@ export class AgentSession {
|
|
|
13514
14052
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
13515
14053
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
13516
14054
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
14055
|
+
this.#rehydrateCheckpointRewindState();
|
|
13517
14056
|
|
|
13518
14057
|
// Emit session_switch event to hooks
|
|
13519
14058
|
if (this.#extensionRunner) {
|
|
@@ -13655,6 +14194,10 @@ export class AgentSession {
|
|
|
13655
14194
|
this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
|
|
13656
14195
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
13657
14196
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
14197
|
+
this.#checkpointState = previousCheckpointState;
|
|
14198
|
+
this.#pendingRewindReport = previousPendingRewindReport;
|
|
14199
|
+
this.#lastCompletedRewind = previousLastCompletedRewind;
|
|
14200
|
+
this.#rewoundToolResultIds = previousRewoundToolResultIds;
|
|
13658
14201
|
if (previousModel) {
|
|
13659
14202
|
this.agent.setModel(previousModel);
|
|
13660
14203
|
}
|
|
@@ -13723,6 +14266,7 @@ export class AgentSession {
|
|
|
13723
14266
|
} else {
|
|
13724
14267
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
13725
14268
|
}
|
|
14269
|
+
this.#rehydrateCheckpointRewindState();
|
|
13726
14270
|
this.#syncTodoPhasesFromBranch();
|
|
13727
14271
|
this.#freshProviderSessionId = undefined;
|
|
13728
14272
|
this.#syncAgentSessionId();
|
|
@@ -13809,6 +14353,7 @@ export class AgentSession {
|
|
|
13809
14353
|
this.#cancelOwnAsyncJobs();
|
|
13810
14354
|
|
|
13811
14355
|
this.sessionManager.createBranchedSession(leafId);
|
|
14356
|
+
this.#rehydrateCheckpointRewindState();
|
|
13812
14357
|
this.sessionManager.appendMessage({
|
|
13813
14358
|
role: "user",
|
|
13814
14359
|
content: [{ type: "text", text: question }],
|
|
@@ -14005,6 +14550,7 @@ export class AgentSession {
|
|
|
14005
14550
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
14006
14551
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
14007
14552
|
this.agent.replaceMessages(displayContext.messages);
|
|
14553
|
+
this.#rehydrateCheckpointRewindState();
|
|
14008
14554
|
this.#resetAdvisorSessionState();
|
|
14009
14555
|
this.#syncTodoPhasesFromBranch();
|
|
14010
14556
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|