@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3
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 +162 -40
- package/dist/cli.js +4559 -4543
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
|
@@ -155,6 +155,7 @@ import {
|
|
|
155
155
|
type AdvisorNote,
|
|
156
156
|
AdvisorOutputQuarantinedError,
|
|
157
157
|
AdvisorRuntime,
|
|
158
|
+
type AdvisorRuntimeStatus,
|
|
158
159
|
type AdvisorSeverity,
|
|
159
160
|
AdvisorTranscriptRecorder,
|
|
160
161
|
advisorTranscriptFilename,
|
|
@@ -197,6 +198,7 @@ import {
|
|
|
197
198
|
onModelRolesChanged,
|
|
198
199
|
validateProviderMaxInFlightRequests,
|
|
199
200
|
} from "../config/settings";
|
|
201
|
+
import { CursorExecHandlers } from "../cursor";
|
|
200
202
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
201
203
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
202
204
|
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
@@ -233,6 +235,7 @@ import type {
|
|
|
233
235
|
TurnEndEvent,
|
|
234
236
|
TurnStartEvent,
|
|
235
237
|
} from "../extensibility/extensions";
|
|
238
|
+
import { ManagedTimers } from "../extensibility/extensions/managed-timers";
|
|
236
239
|
import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
|
|
237
240
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
238
241
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
@@ -252,7 +255,11 @@ import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
|
252
255
|
import { theme } from "../modes/theme/theme";
|
|
253
256
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
254
257
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
255
|
-
import {
|
|
258
|
+
import {
|
|
259
|
+
computeNonMessageBreakdown,
|
|
260
|
+
computeNonMessageTokens,
|
|
261
|
+
estimateToolSchemaTokens,
|
|
262
|
+
} from "../modes/utils/context-usage";
|
|
256
263
|
import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
|
|
257
264
|
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
258
265
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
@@ -393,6 +400,34 @@ import { YieldQueue } from "./yield-queue";
|
|
|
393
400
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
394
401
|
const PLAN_MODE_REMINDER_MAX = 3;
|
|
395
402
|
|
|
403
|
+
type BashAppendDestination =
|
|
404
|
+
| { kind: "current"; manager: SessionManager }
|
|
405
|
+
| { kind: "detached"; manager: SessionManager }
|
|
406
|
+
| { kind: "branch"; manager: SessionManager; parentId: string | null };
|
|
407
|
+
|
|
408
|
+
interface BashSessionTarget {
|
|
409
|
+
sessionId: string;
|
|
410
|
+
refs: number;
|
|
411
|
+
destination?: BashAppendDestination;
|
|
412
|
+
pending?: Promise<BashAppendDestination>;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
interface PendingBashMessage {
|
|
416
|
+
target: BashSessionTarget;
|
|
417
|
+
message: BashExecutionMessage;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface BashSessionTransition {
|
|
421
|
+
oldTarget: BashSessionTarget;
|
|
422
|
+
newTarget: BashSessionTarget;
|
|
423
|
+
oldSessionId: string;
|
|
424
|
+
oldSessionFile: string | undefined;
|
|
425
|
+
oldLeafId: string | null;
|
|
426
|
+
detachedManager: SessionManager | undefined;
|
|
427
|
+
resolveOld: ((destination: BashAppendDestination) => void) | undefined;
|
|
428
|
+
resolveNew: (destination: BashAppendDestination) => void;
|
|
429
|
+
}
|
|
430
|
+
|
|
396
431
|
/**
|
|
397
432
|
* Mutating tool results (`bash`/`eval`/`edit`/`write`/`ast_edit`) without the
|
|
398
433
|
* agent touching the `todo` tool that trip the mid-run reconciliation nudge.
|
|
@@ -531,6 +566,43 @@ function reportFromRewindReportContent(content: string): string {
|
|
|
531
566
|
return report.trim();
|
|
532
567
|
}
|
|
533
568
|
|
|
569
|
+
type SemanticCheckpointToolName = "checkpoint" | "rewind";
|
|
570
|
+
|
|
571
|
+
type SemanticToolResult = {
|
|
572
|
+
toolName: SemanticCheckpointToolName;
|
|
573
|
+
details?: unknown;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Normalize checkpoint/rewind results across native calls and `write xd://`
|
|
578
|
+
* dispatches. Xdev keeps the wrapped tool's result details under `xdev.inner`,
|
|
579
|
+
* while direct calls put them on the result itself.
|
|
580
|
+
*/
|
|
581
|
+
function semanticToolResult(toolName: string | undefined, result: unknown): SemanticToolResult | undefined {
|
|
582
|
+
if (toolName === "checkpoint" || toolName === "rewind") {
|
|
583
|
+
const details = result && typeof result === "object" && "details" in result ? result.details : undefined;
|
|
584
|
+
return { toolName, details };
|
|
585
|
+
}
|
|
586
|
+
const dispatch = writeDeviceDispatch(toolName ?? "", result);
|
|
587
|
+
if (dispatch?.mode !== "execute" || (dispatch.tool !== "checkpoint" && dispatch.tool !== "rewind")) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
return { toolName: dispatch.tool, details: dispatch.inner };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isTodoPhase(value: unknown): value is TodoPhase {
|
|
594
|
+
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
595
|
+
return value.tasks.every(
|
|
596
|
+
task =>
|
|
597
|
+
isRecord(task) &&
|
|
598
|
+
typeof task.content === "string" &&
|
|
599
|
+
(task.status === "pending" ||
|
|
600
|
+
task.status === "in_progress" ||
|
|
601
|
+
task.status === "completed" ||
|
|
602
|
+
task.status === "abandoned"),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
534
606
|
function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | undefined {
|
|
535
607
|
if (entry.type !== "custom_message" || entry.customType !== "rewind-report") return undefined;
|
|
536
608
|
const details = entry.details;
|
|
@@ -543,21 +615,18 @@ function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | u
|
|
|
543
615
|
reportFromRewindReportContent(customMessageContentText(entry.content));
|
|
544
616
|
return report.length > 0 ? { report, startedAt, rewoundAt } : undefined;
|
|
545
617
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
entry.message.toolName === "checkpoint" &&
|
|
554
|
-
entry.message.isError !== true
|
|
555
|
-
);
|
|
618
|
+
function isSuccessfulCheckpointEntry(
|
|
619
|
+
entry: SessionEntry,
|
|
620
|
+
): entry is SessionEntry & { type: "message"; message: Extract<AgentMessage, { role: "toolResult" }> } {
|
|
621
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.isError === true) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
return semanticToolResult(entry.message.toolName, entry.message)?.toolName === "checkpoint";
|
|
556
625
|
}
|
|
557
626
|
|
|
558
627
|
function checkpointStartedAtFromEntry(entry: SessionEntry): string | undefined {
|
|
559
628
|
if (!isSuccessfulCheckpointEntry(entry)) return undefined;
|
|
560
|
-
const details = entry.message.details;
|
|
629
|
+
const details = semanticToolResult(entry.message.toolName, entry.message)?.details;
|
|
561
630
|
if (details && typeof details === "object") {
|
|
562
631
|
const startedAt = stringProperty(details, "startedAt");
|
|
563
632
|
if (startedAt) return startedAt;
|
|
@@ -671,7 +740,7 @@ const COMPACTION_CHECK_NONE: CompactionCheckResult = {
|
|
|
671
740
|
};
|
|
672
741
|
const COMPACTION_CHECK_DEFERRED_HANDOFF: CompactionCheckResult = {
|
|
673
742
|
deferredHandoff: true,
|
|
674
|
-
continuationScheduled:
|
|
743
|
+
continuationScheduled: false,
|
|
675
744
|
};
|
|
676
745
|
const COMPACTION_CHECK_CONTINUATION: CompactionCheckResult = {
|
|
677
746
|
deferredHandoff: false,
|
|
@@ -843,6 +912,8 @@ export interface AgentSessionConfig {
|
|
|
843
912
|
builtInToolNames?: Iterable<string>;
|
|
844
913
|
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
845
914
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
915
|
+
/** Register the write transport lazily when runtime xdev mounts first need it. */
|
|
916
|
+
ensureWriteRegistered?: () => Promise<boolean>;
|
|
846
917
|
/** Current session pre-LLM message transform pipeline */
|
|
847
918
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
848
919
|
/**
|
|
@@ -889,7 +960,8 @@ export interface AgentSessionConfig {
|
|
|
889
960
|
xdevRegistry?: XdevRegistry;
|
|
890
961
|
/** Discoverable tools mounted under `xd://` in the initial enabled set (startup partition in `sdk.ts`). */
|
|
891
962
|
initialMountedXdevToolNames?: string[];
|
|
892
|
-
|
|
963
|
+
/** Explicit/effective names pinned top-level during runtime repartitioning. */
|
|
964
|
+
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
893
965
|
/**
|
|
894
966
|
* Optional accessor for live MCP server instructions. Read by the session's
|
|
895
967
|
* `rebuildSystemPrompt`-skip optimization to detect server-side instruction
|
|
@@ -1117,15 +1189,20 @@ export interface AdvisorStats {
|
|
|
1117
1189
|
advisors: PerAdvisorStat[];
|
|
1118
1190
|
}
|
|
1119
1191
|
|
|
1120
|
-
/** One advisor's slice of {@link AdvisorStats}
|
|
1192
|
+
/** One advisor's slice of {@link AdvisorStats}. Active advisors carry full
|
|
1193
|
+
* token/cost data; disabled/no-model/quota-exhausted advisors appear with
|
|
1194
|
+
* just `name` + `status` so the status line can render a dot for every
|
|
1195
|
+
* configured advisor. */
|
|
1121
1196
|
export interface PerAdvisorStat {
|
|
1122
1197
|
name: string;
|
|
1123
|
-
|
|
1198
|
+
status: AdvisorRuntimeStatus;
|
|
1199
|
+
model?: Model;
|
|
1124
1200
|
contextWindow: number;
|
|
1125
1201
|
contextTokens: number;
|
|
1126
1202
|
tokens: AdvisorStats["tokens"];
|
|
1127
1203
|
cost: number;
|
|
1128
1204
|
messages: AdvisorStats["messages"];
|
|
1205
|
+
sessionId?: string;
|
|
1129
1206
|
}
|
|
1130
1207
|
|
|
1131
1208
|
/**
|
|
@@ -1134,6 +1211,13 @@ export interface PerAdvisorStat {
|
|
|
1134
1211
|
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
1135
1212
|
* channel) stays on the session.
|
|
1136
1213
|
*/
|
|
1214
|
+
interface AdvisorRetryFallbackState {
|
|
1215
|
+
role: string;
|
|
1216
|
+
originalSelector: string;
|
|
1217
|
+
originalThinkingLevel: ThinkingLevel;
|
|
1218
|
+
lastAppliedThinkingLevel: ThinkingLevel;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1137
1221
|
interface ActiveAdvisor {
|
|
1138
1222
|
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
1139
1223
|
name: string;
|
|
@@ -1150,10 +1234,23 @@ interface ActiveAdvisor {
|
|
|
1150
1234
|
agentUnsubscribe?: () => void;
|
|
1151
1235
|
model: Model;
|
|
1152
1236
|
thinkingLevel: ThinkingLevel;
|
|
1237
|
+
/** Provider credential/session identity retained across advisor model switches. */
|
|
1238
|
+
providerSessionId: string | undefined;
|
|
1239
|
+
/** Active chain state retained until the configured primary can be restored. */
|
|
1240
|
+
retryFallback?: AdvisorRetryFallbackState;
|
|
1241
|
+
/** A switched advisor model has not yet completed its first successful turn. */
|
|
1242
|
+
retryFallbackPendingSuccess: boolean;
|
|
1153
1243
|
/** Stable key for the resolved runtime inputs that require a rebuild to change. */
|
|
1154
1244
|
signature: string;
|
|
1155
1245
|
}
|
|
1156
1246
|
|
|
1247
|
+
/** Runtime-only advisor compaction metadata. It never enters the model-facing summary text. */
|
|
1248
|
+
interface AdvisorCompactionSummaryMessage extends CompactionSummaryMessage {
|
|
1249
|
+
firstKeptEntryId?: string;
|
|
1250
|
+
/** First message index eligible to anchor provider usage after this compaction. */
|
|
1251
|
+
advisorUsageAnchorStartIndex?: number;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1157
1254
|
/** Resolved advisor config ready to instantiate as an {@link ActiveAdvisor}. */
|
|
1158
1255
|
interface AdvisorRuntimeDescriptor {
|
|
1159
1256
|
config: AdvisorConfig;
|
|
@@ -1229,13 +1326,31 @@ function isRetryFallbackModelKey(key: string): boolean {
|
|
|
1229
1326
|
}
|
|
1230
1327
|
|
|
1231
1328
|
/**
|
|
1232
|
-
* A
|
|
1233
|
-
*
|
|
1329
|
+
* A wildcard fallback-chain key/entry: `provider/*` matches any model of that
|
|
1330
|
+
* provider; an id-prefixed `provider/prefix/*` (e.g. `openrouter/google/*`)
|
|
1331
|
+
* scopes it to ids under that prefix — aggregators namespace model ids by
|
|
1332
|
+
* upstream vendor.
|
|
1234
1333
|
*/
|
|
1235
1334
|
function isRetryFallbackWildcardKey(key: string): boolean {
|
|
1236
1335
|
return key.endsWith("/*");
|
|
1237
1336
|
}
|
|
1238
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* Split a `…/*` wildcard key/entry into its provider and optional id prefix
|
|
1340
|
+
* (`google-vertex/*` → provider only; `openrouter/google/*` → provider
|
|
1341
|
+
* `openrouter`, prefix `google`). A template that names a known provider in
|
|
1342
|
+
* full wins over the split, so provider ids containing `/` keep working.
|
|
1343
|
+
*/
|
|
1344
|
+
function parseRetryFallbackWildcard(
|
|
1345
|
+
key: string,
|
|
1346
|
+
isKnownProvider: (provider: string) => boolean,
|
|
1347
|
+
): { provider: string; idPrefix: string | undefined } {
|
|
1348
|
+
const template = key.slice(0, -2);
|
|
1349
|
+
const slash = template.indexOf("/");
|
|
1350
|
+
if (slash < 0 || isKnownProvider(template)) return { provider: template, idPrefix: undefined };
|
|
1351
|
+
return { provider: template.slice(0, slash), idPrefix: template.slice(slash + 1) };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1239
1354
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1240
1355
|
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
1241
1356
|
}
|
|
@@ -1797,6 +1912,11 @@ export class AgentSession {
|
|
|
1797
1912
|
#advisors: ActiveAdvisor[] = [];
|
|
1798
1913
|
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1799
1914
|
#advisorConfigs?: AdvisorConfig[];
|
|
1915
|
+
/** Per-advisor runtime status (slug → {name, status}). Tracks disabled/quota/states
|
|
1916
|
+
* for the configured roster even when the advisor has no live runtime. The name
|
|
1917
|
+
* is stored alongside the status so {@link getAdvisorStats} doesn't need to
|
|
1918
|
+
* recompute slugs or resolve config names. */
|
|
1919
|
+
#advisorStatuses: Map<string, { name: string; status: AdvisorRuntimeStatus }> = new Map();
|
|
1800
1920
|
/** Provider-facing UUIDv7 identities keyed by primary provider session and advisor slug. */
|
|
1801
1921
|
#advisorProviderSessionIds = new Map<string, string>();
|
|
1802
1922
|
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
@@ -1860,11 +1980,13 @@ export class AgentSession {
|
|
|
1860
1980
|
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
1861
1981
|
* the session cwd changes. */
|
|
1862
1982
|
#titleSystemPrompt: string | undefined;
|
|
1983
|
+
#titleGenerationAbortController = new AbortController();
|
|
1863
1984
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1864
1985
|
|
|
1865
1986
|
// Bash execution state
|
|
1866
1987
|
#bashAbortControllers = new Set<AbortController>();
|
|
1867
|
-
#pendingBashMessages:
|
|
1988
|
+
#pendingBashMessages: PendingBashMessage[] = [];
|
|
1989
|
+
#bashSessionTarget!: BashSessionTarget;
|
|
1868
1990
|
|
|
1869
1991
|
// Python execution state
|
|
1870
1992
|
#evalAbortControllers = new Set<AbortController>();
|
|
@@ -1898,9 +2020,17 @@ export class AgentSession {
|
|
|
1898
2020
|
#providerSessionId: string | undefined;
|
|
1899
2021
|
#freshProviderSessionId: string | undefined;
|
|
1900
2022
|
#inheritedProviderPromptCacheKey: string | undefined;
|
|
2023
|
+
#autolearnCaptureAbortController: AbortController | undefined;
|
|
2024
|
+
#autolearnCaptureTask: Promise<void> | undefined;
|
|
1901
2025
|
#isDisposed = false;
|
|
1902
2026
|
// Extension system
|
|
1903
2027
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
2028
|
+
/**
|
|
2029
|
+
* Backs `ctx.setInterval`/`setTimeout`/`clearTimer` for the runner-less
|
|
2030
|
+
* command-context fallback (SDK embeddings with no extension runner). Lazily
|
|
2031
|
+
* created; cleared on dispose alongside the runner's own timers (#5664).
|
|
2032
|
+
*/
|
|
2033
|
+
#fallbackExtensionTimers: ManagedTimers | undefined = undefined;
|
|
1904
2034
|
#turnIndex = 0;
|
|
1905
2035
|
#messageEndPersistenceTail: Promise<void> = Promise.resolve();
|
|
1906
2036
|
#pendingMessageEndPersistence = new Map<string, Promise<void>>();
|
|
@@ -1939,8 +2069,10 @@ export class AgentSession {
|
|
|
1939
2069
|
#getLocalCalendarDate: () => string;
|
|
1940
2070
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1941
2071
|
#setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
|
|
2072
|
+
#ensureWriteRegistered: (() => Promise<boolean>) | undefined;
|
|
1942
2073
|
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
|
1943
|
-
#
|
|
2074
|
+
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
2075
|
+
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
1944
2076
|
#baseSystemPrompt: string[];
|
|
1945
2077
|
#baseSystemPromptBeforeMemoryPromotion: string[] | undefined;
|
|
1946
2078
|
/**
|
|
@@ -1963,6 +2095,9 @@ export class AgentSession {
|
|
|
1963
2095
|
#xdevRegistry: XdevRegistry | undefined;
|
|
1964
2096
|
/** Names of discoverable tools currently mounted under `xd://` (dynamic mounts only, not built-in devices). */
|
|
1965
2097
|
#mountedXdevToolNames = new Set<string>();
|
|
2098
|
+
/** Coalesced xd:// mount delta not yet announced to the model; delivered as a
|
|
2099
|
+
* hidden notice alongside the next prompt (see {@link #notifyXdevMountDelta}). */
|
|
2100
|
+
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
1966
2101
|
|
|
1967
2102
|
// TTSR manager for time-traveling stream rules
|
|
1968
2103
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
@@ -2110,6 +2245,17 @@ export class AgentSession {
|
|
|
2110
2245
|
* queue was consumed normally or a new turn already started. */
|
|
2111
2246
|
#drainStrandedQueuedMessages(): void {
|
|
2112
2247
|
if (this.#abortInProgress) return;
|
|
2248
|
+
// Session transitions (newSession/`/new`, compact, model-switch, session-switch,
|
|
2249
|
+
// dispose) call #disconnectFromAgent() BEFORE `await abort()`, so abort's own
|
|
2250
|
+
// finally lands here with no listener attached. Auto-resuming now would snapshot
|
|
2251
|
+
// the still-old context (the transition hasn't reached agent.reset() yet), start a
|
|
2252
|
+
// stale provider turn that races the reset, and — once reconnected — append its
|
|
2253
|
+
// output to the fresh session (issue #5800). A disconnected session never owns the
|
|
2254
|
+
// queue: the transition does. newSession/switchSession drop the queue (reset /
|
|
2255
|
+
// clearAllQueues), so nothing survives; compaction preserves it and re-drains itself
|
|
2256
|
+
// after #reconnectToAgent (see compact()'s finally); an explicit prompt flushes it
|
|
2257
|
+
// in every case.
|
|
2258
|
+
if (this.#unsubscribeAgent === undefined) return;
|
|
2113
2259
|
// A concern steered into a resumed streaming run after a user interrupt can
|
|
2114
2260
|
// strand at the turn tail (steered past the loop's final boundary poll). While
|
|
2115
2261
|
// that interrupt's suppression is still in effect, reclaim such advisor steers
|
|
@@ -2431,12 +2577,17 @@ export class AgentSession {
|
|
|
2431
2577
|
readPlan: url => this.#readPlanYoloFile(url),
|
|
2432
2578
|
listPlanFiles: () => this.#listPlanYoloFiles(),
|
|
2433
2579
|
});
|
|
2580
|
+
this.setPlanModeState(undefined);
|
|
2434
2581
|
const previousTools = this.#planYoloPreviousTools;
|
|
2435
|
-
|
|
2436
|
-
|
|
2582
|
+
try {
|
|
2583
|
+
if (previousTools) {
|
|
2584
|
+
await this.setActiveToolsByName(previousTools);
|
|
2585
|
+
}
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
this.setPlanModeState(state);
|
|
2588
|
+
throw error;
|
|
2437
2589
|
}
|
|
2438
2590
|
this.setPlanProposalHandler(null);
|
|
2439
|
-
this.setPlanModeState(undefined);
|
|
2440
2591
|
this.#planYolo = undefined;
|
|
2441
2592
|
this.#planYoloPreviousTools = undefined;
|
|
2442
2593
|
await this.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
@@ -2494,6 +2645,11 @@ export class AgentSession {
|
|
|
2494
2645
|
constructor(config: AgentSessionConfig) {
|
|
2495
2646
|
this.agent = config.agent;
|
|
2496
2647
|
this.sessionManager = config.sessionManager;
|
|
2648
|
+
this.#bashSessionTarget = {
|
|
2649
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
2650
|
+
refs: 0,
|
|
2651
|
+
destination: { kind: "current", manager: this.sessionManager },
|
|
2652
|
+
};
|
|
2497
2653
|
this.settings = config.settings;
|
|
2498
2654
|
this.#autoApprove = config.autoApprove === true;
|
|
2499
2655
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
@@ -2544,7 +2700,8 @@ export class AgentSession {
|
|
|
2544
2700
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2545
2701
|
this.#createVibeTools = config.createVibeTools;
|
|
2546
2702
|
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
2547
|
-
this.#
|
|
2703
|
+
this.#presentationPinnedToolNames = config.presentationPinnedToolNames;
|
|
2704
|
+
this.#ensureWriteRegistered = config.ensureWriteRegistered;
|
|
2548
2705
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2549
2706
|
this.#transformProviderContext = config.transformProviderContext;
|
|
2550
2707
|
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
@@ -2596,7 +2753,18 @@ export class AgentSession {
|
|
|
2596
2753
|
this.#advisorPrimaryTurnsCompleted++;
|
|
2597
2754
|
if (this.#advisors.length > 0) {
|
|
2598
2755
|
for (const a of this.#advisors) {
|
|
2599
|
-
if (
|
|
2756
|
+
if (a.runtime.disposed) continue;
|
|
2757
|
+
try {
|
|
2758
|
+
a.runtime.onTurnEnd(messages, { willContinue: context?.willContinue });
|
|
2759
|
+
} catch (advisorErr) {
|
|
2760
|
+
// CRITICAL boundary: NOTHING an advisor does may abort the
|
|
2761
|
+
// primary agent's turn-end. A throwing advisor loses its
|
|
2762
|
+
// delta; the primary continues untouched.
|
|
2763
|
+
logger.warn("advisor onTurnEnd threw; delta dropped", {
|
|
2764
|
+
advisor: a.name,
|
|
2765
|
+
err: String(advisorErr),
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2600
2768
|
}
|
|
2601
2769
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
2602
2770
|
if (syncBacklog !== "off") {
|
|
@@ -2805,6 +2973,12 @@ export class AgentSession {
|
|
|
2805
2973
|
slug = candidate;
|
|
2806
2974
|
usedSlugs.add(slug);
|
|
2807
2975
|
}
|
|
2976
|
+
// Per-advisor toggle: skip disabled advisors but keep them in the
|
|
2977
|
+
// status map so they show `○` rather than disappearing.
|
|
2978
|
+
if (config.enabled === false) {
|
|
2979
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "paused" });
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2808
2982
|
|
|
2809
2983
|
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2810
2984
|
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
@@ -2815,6 +2989,7 @@ export class AgentSession {
|
|
|
2815
2989
|
model = resolved.model;
|
|
2816
2990
|
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
2817
2991
|
if (!model) {
|
|
2992
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2818
2993
|
if (emitWarnings) {
|
|
2819
2994
|
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2820
2995
|
}
|
|
@@ -2823,6 +2998,7 @@ export class AgentSession {
|
|
|
2823
2998
|
} else {
|
|
2824
2999
|
const sel = resolveAdvisorRoleSelection(this.settings, this.#modelRegistry.getAvailable());
|
|
2825
3000
|
if (!sel) {
|
|
3001
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2826
3002
|
if (emitWarnings) {
|
|
2827
3003
|
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2828
3004
|
advisor: config.name,
|
|
@@ -2846,6 +3022,11 @@ export class AgentSession {
|
|
|
2846
3022
|
const requestedLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2847
3023
|
const resolvedLevel = resolveThinkingLevelForModel(model, requestedLevel);
|
|
2848
3024
|
const advisorThinkingLevel: ThinkingLevel = resolvedLevel ?? ThinkingLevel.Inherit;
|
|
3025
|
+
// Record the status entry now (in roster order) so the Map's insertion
|
|
3026
|
+
// order matches the configured roster even when earlier advisors were
|
|
3027
|
+
// skipped as paused/no_model. The build loop overwrites this to "running"
|
|
3028
|
+
// without changing insertion order.
|
|
3029
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "running" });
|
|
2849
3030
|
descriptors.push({
|
|
2850
3031
|
config,
|
|
2851
3032
|
name: config.name,
|
|
@@ -2881,6 +3062,11 @@ export class AgentSession {
|
|
|
2881
3062
|
if (!this.#advisorEnabled) return false;
|
|
2882
3063
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
2883
3064
|
|
|
3065
|
+
// Rebuild the status map from scratch so removed/renamed advisors don't
|
|
3066
|
+
// leave stale entries. #resolveAdvisorRuntimeDescriptors populates every
|
|
3067
|
+
// entry (`paused`/`no_model`/`running`) in roster order; the build loop
|
|
3068
|
+
// below confirms `running` for successfully built advisors.
|
|
3069
|
+
this.#advisorStatuses.clear();
|
|
2884
3070
|
const descriptors = this.#resolveAdvisorRuntimeDescriptors(true);
|
|
2885
3071
|
|
|
2886
3072
|
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
@@ -2921,11 +3107,16 @@ export class AgentSession {
|
|
|
2921
3107
|
|
|
2922
3108
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2923
3109
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
3110
|
+
const advisorLoopTools: AgentTool<any>[] = [adviseTool, ...tools];
|
|
3111
|
+
const advisorToolMap = new Map<string, AgentTool>();
|
|
2924
3112
|
const availableAdvisorToolNames = new Set<string>();
|
|
2925
|
-
|
|
2926
|
-
for (const tool of tools) {
|
|
3113
|
+
for (const tool of advisorLoopTools) {
|
|
2927
3114
|
availableAdvisorToolNames.add(tool.name);
|
|
2928
|
-
|
|
3115
|
+
advisorToolMap.set(tool.name, tool);
|
|
3116
|
+
if (tool.customWireName !== undefined) {
|
|
3117
|
+
availableAdvisorToolNames.add(tool.customWireName);
|
|
3118
|
+
advisorToolMap.set(tool.customWireName, tool);
|
|
3119
|
+
}
|
|
2929
3120
|
}
|
|
2930
3121
|
let quarantinedAdvisorOutput: string | undefined;
|
|
2931
3122
|
let currentAdvisorInput = "";
|
|
@@ -2965,17 +3156,36 @@ export class AgentSession {
|
|
|
2965
3156
|
// Codex request identity remains UUID-shaped while local labels keep the
|
|
2966
3157
|
// `-advisor` suffix.
|
|
2967
3158
|
const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorProviderSessionId;
|
|
3159
|
+
// On the Cursor provider every tool runs server-side and is dispatched
|
|
3160
|
+
// back through `cursorExecHandlers`; without this bridge the advisor's
|
|
3161
|
+
// own tools (including the MCP `advise` tool) return `toolNotFound` and
|
|
3162
|
+
// no advice is ever routed (issue #5680). Mirrors the primary agent's
|
|
3163
|
+
// bridge (`sdk.ts`), scoped to this advisor's granted tool set.
|
|
3164
|
+
// Cursor's native `delete` frame removes files directly, bypassing the
|
|
3165
|
+
// tool map, so gate it on the advisor actually holding a file-mutating
|
|
3166
|
+
// tool. A default read-only advisor (advise/read/grep/glob) never gets
|
|
3167
|
+
// to delete workspace files it was never granted (issue #5680 review).
|
|
3168
|
+
const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
|
|
3169
|
+
if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
|
|
3170
|
+
const advisorCursorExecHandlers = new CursorExecHandlers({
|
|
3171
|
+
cwd: this.sessionManager.getCwd(),
|
|
3172
|
+
getCwd: () => this.sessionManager.getCwd(),
|
|
3173
|
+
tools: advisorToolMap,
|
|
3174
|
+
allowNativeDelete: advisorCanMutateFiles,
|
|
3175
|
+
});
|
|
2968
3176
|
const advisorAgent = new Agent({
|
|
2969
3177
|
initialState: {
|
|
2970
3178
|
systemPrompt,
|
|
2971
3179
|
model: advisorModel,
|
|
2972
3180
|
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2973
|
-
tools:
|
|
3181
|
+
tools: advisorLoopTools,
|
|
2974
3182
|
},
|
|
2975
3183
|
appendOnlyContext,
|
|
2976
3184
|
sessionId: advisorProviderSessionId,
|
|
2977
3185
|
promptCacheKey: advisorPromptCacheKey,
|
|
2978
3186
|
providerSessionState: this.#providerSessionState,
|
|
3187
|
+
cursorExecHandlers: advisorCursorExecHandlers,
|
|
3188
|
+
cwdResolver: () => this.sessionManager.getCwd(),
|
|
2979
3189
|
preferWebsockets: this.#preferWebsockets,
|
|
2980
3190
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
2981
3191
|
streamFn: this.#advisorStreamFn,
|
|
@@ -3046,33 +3256,30 @@ export class AgentSession {
|
|
|
3046
3256
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
3047
3257
|
obfuscator: this.#obfuscator,
|
|
3048
3258
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
3049
|
-
onTurnError:
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
advisorModel.provider,
|
|
3060
|
-
advisorProviderSessionId,
|
|
3061
|
-
{
|
|
3062
|
-
retryAfterMs: extractRetryHint(undefined, message),
|
|
3063
|
-
baseUrl: advisorModel.baseUrl,
|
|
3064
|
-
modelId: advisorModel.id,
|
|
3065
|
-
},
|
|
3066
|
-
);
|
|
3259
|
+
onTurnError: (error, failedMessages) => this.#recoverAdvisorTurn(advisorRef, error, failedMessages),
|
|
3260
|
+
onTurnSuccess: async () => {
|
|
3261
|
+
const fallback = advisorRef.retryFallback;
|
|
3262
|
+
if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
|
|
3263
|
+
advisorRef.retryFallbackPendingSuccess = false;
|
|
3264
|
+
await this.#emitSessionEvent({
|
|
3265
|
+
type: "retry_fallback_succeeded",
|
|
3266
|
+
model: formatRetryFallbackSelector(advisorRef.agent.state.model, advisorRef.thinkingLevel),
|
|
3267
|
+
role: fallback.role,
|
|
3268
|
+
});
|
|
3067
3269
|
},
|
|
3068
3270
|
notifyFailure: error => {
|
|
3271
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "error" });
|
|
3069
3272
|
const message = error instanceof Error ? error.message : String(error);
|
|
3070
3273
|
this.emitNotice(
|
|
3071
3274
|
"warning",
|
|
3072
|
-
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(
|
|
3275
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorAgent.state.model)}: ${message}`,
|
|
3073
3276
|
"advisor",
|
|
3074
3277
|
);
|
|
3075
3278
|
},
|
|
3279
|
+
notifyQuotaExhausted: () => {
|
|
3280
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "quota_exhausted" });
|
|
3281
|
+
this.emitNotice("warning", `Advisor "${advisorName}" quota exhausted — pausing until reset.`, "advisor");
|
|
3282
|
+
},
|
|
3076
3283
|
});
|
|
3077
3284
|
|
|
3078
3285
|
const advisorRef: ActiveAdvisor = {
|
|
@@ -3086,10 +3293,13 @@ export class AgentSession {
|
|
|
3086
3293
|
recorderClosed: Promise.resolve(),
|
|
3087
3294
|
model: advisorModel,
|
|
3088
3295
|
thinkingLevel: advisorThinkingLevel,
|
|
3296
|
+
providerSessionId: advisorProviderSessionId,
|
|
3297
|
+
retryFallbackPendingSuccess: false,
|
|
3089
3298
|
signature,
|
|
3090
3299
|
};
|
|
3091
3300
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
3092
3301
|
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
3302
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
3093
3303
|
this.#advisors.push(advisorRef);
|
|
3094
3304
|
}
|
|
3095
3305
|
|
|
@@ -3251,6 +3461,163 @@ export class AgentSession {
|
|
|
3251
3461
|
});
|
|
3252
3462
|
}
|
|
3253
3463
|
|
|
3464
|
+
/** Switch one advisor model while preserving its context and effort invariants. */
|
|
3465
|
+
#setAdvisorModel(advisor: ActiveAdvisor, model: Model, requestedThinkingLevel: ThinkingLevel): ThinkingLevel {
|
|
3466
|
+
const resolvedThinkingLevel = resolveThinkingLevelForModel(model, requestedThinkingLevel);
|
|
3467
|
+
const nextThinkingLevel = resolvedThinkingLevel ?? ThinkingLevel.Inherit;
|
|
3468
|
+
advisor.agent.setModel(model);
|
|
3469
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
3470
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(nextThinkingLevel));
|
|
3471
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3472
|
+
advisor.model = model;
|
|
3473
|
+
advisor.thinkingLevel = nextThinkingLevel;
|
|
3474
|
+
return nextThinkingLevel;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
/** Restore an advisor's configured primary once its fallback cooldown expires. */
|
|
3478
|
+
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
|
|
3479
|
+
const fallback = advisor.retryFallback;
|
|
3480
|
+
if (!fallback || this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
3481
|
+
|
|
3482
|
+
const originalSelector = parseRetryFallbackSelector(fallback.originalSelector, this.#modelRegistry);
|
|
3483
|
+
if (!originalSelector) {
|
|
3484
|
+
advisor.retryFallback = undefined;
|
|
3485
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
const currentSelector = formatRetryFallbackSelector(advisor.agent.state.model, advisor.thinkingLevel);
|
|
3489
|
+
if (currentSelector === originalSelector.raw) {
|
|
3490
|
+
if (!this.#isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
3491
|
+
advisor.retryFallback = undefined;
|
|
3492
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3493
|
+
}
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
3497
|
+
|
|
3498
|
+
const resolvedPrimary = resolveModelOverride([originalSelector.raw], this.#modelRegistry, this.settings);
|
|
3499
|
+
const primaryModel =
|
|
3500
|
+
resolvedPrimary.model ?? this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
3501
|
+
if (!primaryModel) return;
|
|
3502
|
+
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
|
|
3503
|
+
if (!apiKey) return;
|
|
3504
|
+
|
|
3505
|
+
const thinkingToApply =
|
|
3506
|
+
advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
|
|
3507
|
+
? fallback.originalThinkingLevel
|
|
3508
|
+
: advisor.thinkingLevel;
|
|
3509
|
+
this.#setAdvisorModel(advisor, primaryModel, thinkingToApply);
|
|
3510
|
+
this.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(primaryModel));
|
|
3511
|
+
advisor.retryFallback = undefined;
|
|
3512
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
/**
|
|
3516
|
+
* Apply the advisor's configured provider-failure fallback chain after
|
|
3517
|
+
* same-provider credential rotation has no usable sibling.
|
|
3518
|
+
*/
|
|
3519
|
+
async #recoverAdvisorTurn(
|
|
3520
|
+
advisor: ActiveAdvisor,
|
|
3521
|
+
error: unknown,
|
|
3522
|
+
failedMessages: readonly AgentMessage[],
|
|
3523
|
+
): Promise<boolean> {
|
|
3524
|
+
if (error instanceof AdvisorOutputQuarantinedError) return false;
|
|
3525
|
+
|
|
3526
|
+
const failedMessage = failedMessages.findLast(
|
|
3527
|
+
(message): message is AssistantMessage => message.role === "assistant",
|
|
3528
|
+
);
|
|
3529
|
+
if (failedMessage?.stopReason !== "error") {
|
|
3530
|
+
// Stream setup can reject before any assistant turn is recorded (e.g.
|
|
3531
|
+
// an HTTP 429 thrown from prompt()); classify the raw error so a
|
|
3532
|
+
// structural usage limit still marks the exhausted credential.
|
|
3533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3534
|
+
if (!AIError.isUsageLimit(error) && !isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
const currentModel = advisor.agent.state.model;
|
|
3538
|
+
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
3539
|
+
currentModel.provider,
|
|
3540
|
+
advisor.providerSessionId,
|
|
3541
|
+
{
|
|
3542
|
+
retryAfterMs: extractRetryHint(undefined, message),
|
|
3543
|
+
baseUrl: currentModel.baseUrl,
|
|
3544
|
+
modelId: currentModel.id,
|
|
3545
|
+
},
|
|
3546
|
+
);
|
|
3547
|
+
return outcome.switched;
|
|
3548
|
+
}
|
|
3549
|
+
if (failedMessage.content.some(block => block.type === "toolCall")) return false;
|
|
3550
|
+
|
|
3551
|
+
const currentModel = advisor.agent.state.model;
|
|
3552
|
+
const message = failedMessage.errorMessage ?? (error instanceof Error ? error.message : String(error));
|
|
3553
|
+
const errorId = AIError.classifyMessage({
|
|
3554
|
+
api: currentModel.api,
|
|
3555
|
+
errorId: failedMessage.errorId,
|
|
3556
|
+
errorMessage: message,
|
|
3557
|
+
errorStatus: failedMessage.errorStatus,
|
|
3558
|
+
});
|
|
3559
|
+
if (AIError.is(errorId, AIError.Flag.Abort) || AIError.is(errorId, AIError.Flag.UserInterrupt)) return false;
|
|
3560
|
+
if (AIError.isContextOverflow(failedMessage, currentModel.contextWindow ?? 0)) return false;
|
|
3561
|
+
|
|
3562
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, advisor.thinkingLevel);
|
|
3563
|
+
|
|
3564
|
+
const retryAfterMs = extractRetryHint(undefined, message);
|
|
3565
|
+
if (
|
|
3566
|
+
AIError.is(errorId, AIError.Flag.UsageLimit) ||
|
|
3567
|
+
isUsageLimitOutcome(extractHttpStatusFromError(error), message)
|
|
3568
|
+
) {
|
|
3569
|
+
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
3570
|
+
currentModel.provider,
|
|
3571
|
+
advisor.providerSessionId,
|
|
3572
|
+
{
|
|
3573
|
+
retryAfterMs,
|
|
3574
|
+
baseUrl: currentModel.baseUrl,
|
|
3575
|
+
modelId: currentModel.id,
|
|
3576
|
+
},
|
|
3577
|
+
);
|
|
3578
|
+
if (outcome.switched) return true;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
3582
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
3583
|
+
const role = advisor.retryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector, currentModel);
|
|
3584
|
+
if (!role || this.#findRetryFallbackCandidates(role, currentSelector, currentModel).length === 0) return false;
|
|
3585
|
+
|
|
3586
|
+
this.#noteRetryFallbackCooldown(currentSelector, retryAfterMs, message);
|
|
3587
|
+
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector, currentModel)) {
|
|
3588
|
+
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
3589
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
3590
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
3591
|
+
if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
|
|
3592
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisor.providerSessionId);
|
|
3593
|
+
if (!apiKey) continue;
|
|
3594
|
+
|
|
3595
|
+
const originalThinkingLevel = advisor.thinkingLevel;
|
|
3596
|
+
const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
|
|
3597
|
+
const nextThinkingLevel = this.#setAdvisorModel(advisor, candidate, requestedThinkingLevel);
|
|
3598
|
+
if (advisor.retryFallback) {
|
|
3599
|
+
advisor.retryFallback.lastAppliedThinkingLevel = nextThinkingLevel;
|
|
3600
|
+
} else {
|
|
3601
|
+
advisor.retryFallback = {
|
|
3602
|
+
role,
|
|
3603
|
+
originalSelector: currentSelector,
|
|
3604
|
+
originalThinkingLevel,
|
|
3605
|
+
lastAppliedThinkingLevel: nextThinkingLevel,
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
advisor.retryFallbackPendingSuccess = true;
|
|
3609
|
+
this.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(candidate));
|
|
3610
|
+
await this.#emitSessionEvent({
|
|
3611
|
+
type: "retry_fallback_applied",
|
|
3612
|
+
from: currentSelector,
|
|
3613
|
+
to: selector.raw,
|
|
3614
|
+
role,
|
|
3615
|
+
});
|
|
3616
|
+
return true;
|
|
3617
|
+
}
|
|
3618
|
+
return false;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3254
3621
|
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
3255
3622
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
3256
3623
|
if (!promotionSettings.enabled) return false;
|
|
@@ -3263,10 +3630,7 @@ export class AgentSession {
|
|
|
3263
3630
|
// keeps its suffix across a promotion); only the model changes.
|
|
3264
3631
|
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
3265
3632
|
try {
|
|
3266
|
-
advisor
|
|
3267
|
-
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
3268
|
-
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
3269
|
-
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3633
|
+
this.#setAdvisorModel(advisor, targetModel, advisorThinkingLevel);
|
|
3270
3634
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
3271
3635
|
advisor: advisor.name,
|
|
3272
3636
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
@@ -3285,6 +3649,7 @@ export class AgentSession {
|
|
|
3285
3649
|
}
|
|
3286
3650
|
|
|
3287
3651
|
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
3652
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor);
|
|
3288
3653
|
const agent = advisor.agent;
|
|
3289
3654
|
|
|
3290
3655
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -3296,10 +3661,23 @@ export class AgentSession {
|
|
|
3296
3661
|
if (contextWindow <= 0) return false;
|
|
3297
3662
|
|
|
3298
3663
|
const messages = agent.state.messages;
|
|
3299
|
-
|
|
3664
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
3665
|
+
let storedConversationTokens = 0;
|
|
3300
3666
|
for (const message of messages) {
|
|
3301
|
-
|
|
3302
|
-
}
|
|
3667
|
+
storedConversationTokens += estimateTokens(message, estimateOptions);
|
|
3668
|
+
}
|
|
3669
|
+
// Provider usage (including cache reads and generated output) is the
|
|
3670
|
+
// trustworthy anchor for accumulated context. Add only the trailing incoming
|
|
3671
|
+
// delta to that arm. Floor it by a full local estimate — fixed advisor system
|
|
3672
|
+
// prompt, tool schemas, stored messages, and incoming delta — so provider
|
|
3673
|
+
// under-reporting or payload transforms cannot suppress maintenance.
|
|
3674
|
+
const providerContextTokens = this.#estimateAdvisorContextTokens(messages) + incomingTokens;
|
|
3675
|
+
const localContextTokens =
|
|
3676
|
+
countTokens(agent.state.systemPrompt) +
|
|
3677
|
+
estimateToolSchemaTokens(agent.state.tools) +
|
|
3678
|
+
storedConversationTokens +
|
|
3679
|
+
incomingTokens;
|
|
3680
|
+
const contextTokens = compactionContextTokens(providerContextTokens, localContextTokens);
|
|
3303
3681
|
|
|
3304
3682
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
3305
3683
|
return false;
|
|
@@ -3323,6 +3701,7 @@ export class AgentSession {
|
|
|
3323
3701
|
const timestamp = String(message.timestamp || Date.now());
|
|
3324
3702
|
|
|
3325
3703
|
if (message.role === "compactionSummary") {
|
|
3704
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
3326
3705
|
return {
|
|
3327
3706
|
type: "compaction",
|
|
3328
3707
|
id,
|
|
@@ -3330,9 +3709,7 @@ export class AgentSession {
|
|
|
3330
3709
|
timestamp,
|
|
3331
3710
|
summary: message.summary,
|
|
3332
3711
|
shortSummary: message.shortSummary,
|
|
3333
|
-
firstKeptEntryId:
|
|
3334
|
-
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
3335
|
-
`msg-${i + 1}`,
|
|
3712
|
+
firstKeptEntryId: advisorSummary.firstKeptEntryId || `msg-${i + 1}`,
|
|
3336
3713
|
tokensBefore: message.tokensBefore,
|
|
3337
3714
|
} satisfies CompactionEntry;
|
|
3338
3715
|
}
|
|
@@ -3426,11 +3803,15 @@ export class AgentSession {
|
|
|
3426
3803
|
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
3427
3804
|
const tokensBefore = compactResult.tokensBefore;
|
|
3428
3805
|
|
|
3429
|
-
//
|
|
3806
|
+
// The retained messages still carry provider usage from before this
|
|
3807
|
+
// compaction. Record their exact array boundary on the in-memory summary so
|
|
3808
|
+
// only assistants appended afterward can become the next usage anchor.
|
|
3809
|
+
const advisorUsageAnchorStartIndex = preparation.recentMessages.length + 1;
|
|
3430
3810
|
const summaryMessage = {
|
|
3431
3811
|
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
3432
3812
|
firstKeptEntryId,
|
|
3433
|
-
|
|
3813
|
+
advisorUsageAnchorStartIndex,
|
|
3814
|
+
} satisfies AdvisorCompactionSummaryMessage;
|
|
3434
3815
|
|
|
3435
3816
|
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
3436
3817
|
return false;
|
|
@@ -3986,7 +4367,7 @@ export class AgentSession {
|
|
|
3986
4367
|
}
|
|
3987
4368
|
const skipPersistedRewindResult =
|
|
3988
4369
|
message.role === "toolResult" &&
|
|
3989
|
-
message.toolName === "rewind" &&
|
|
4370
|
+
semanticToolResult(message.toolName, message)?.toolName === "rewind" &&
|
|
3990
4371
|
this.#rewoundToolResultIds.delete(message.toolCallId);
|
|
3991
4372
|
if (!skipPersistedRewindResult) {
|
|
3992
4373
|
this.#appendSessionMessage(message);
|
|
@@ -4357,29 +4738,28 @@ export class AgentSession {
|
|
|
4357
4738
|
}
|
|
4358
4739
|
}
|
|
4359
4740
|
if (event.message.role === "toolResult") {
|
|
4360
|
-
const { toolName, toolCallId,
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
isError?: boolean;
|
|
4365
|
-
content?: Array<TextContent | ImageContent>;
|
|
4366
|
-
};
|
|
4741
|
+
const { toolName, toolCallId, isError, content } = event.message;
|
|
4742
|
+
const details = isRecord(event.message.details) ? event.message.details : undefined;
|
|
4743
|
+
const semanticResult = semanticToolResult(toolName, event.message);
|
|
4744
|
+
const semanticDetails = isRecord(semanticResult?.details) ? semanticResult.details : undefined;
|
|
4367
4745
|
// A tool actually ran. Clear the post-reminder suppression: the agent did
|
|
4368
4746
|
// productive work in response to the prior nudge, so the next text-only stop
|
|
4369
4747
|
// is allowed to escalate to the next reminder if todos remain incomplete.
|
|
4370
4748
|
this.#todoReminderAwaitingProgress = false;
|
|
4371
4749
|
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
4372
|
-
|
|
4373
|
-
|
|
4750
|
+
const editedPath = details ? getStringProperty(details, "path") : undefined;
|
|
4751
|
+
if (toolName === "edit" && editedPath) {
|
|
4752
|
+
this.#invalidateFileCacheForPath(editedPath);
|
|
4374
4753
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4754
|
+
const phases = details?.phases;
|
|
4755
|
+
if (toolName === "todo" && !isError && details && Array.isArray(phases) && phases.every(isTodoPhase)) {
|
|
4756
|
+
this.setTodoPhases(phases);
|
|
4377
4757
|
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
4378
4758
|
this.#scheduleReplanTitleRefresh();
|
|
4379
4759
|
}
|
|
4380
4760
|
}
|
|
4381
4761
|
if (toolName === "todo" && isError) {
|
|
4382
|
-
const errorText = content
|
|
4762
|
+
const errorText = content.find(part => part.type === "text")?.text;
|
|
4383
4763
|
const reminderText = [
|
|
4384
4764
|
"<system-reminder>",
|
|
4385
4765
|
"todo failed, so todo progress is not visible to the user.",
|
|
@@ -4397,18 +4777,19 @@ export class AgentSession {
|
|
|
4397
4777
|
{ deliverAs: "nextTurn" },
|
|
4398
4778
|
);
|
|
4399
4779
|
}
|
|
4400
|
-
if (toolName === "checkpoint" && !isError) {
|
|
4780
|
+
if (semanticResult?.toolName === "checkpoint" && !isError) {
|
|
4401
4781
|
const checkpointEntryId = this.sessionManager.getEntries().at(-1)?.id ?? null;
|
|
4402
4782
|
this.#checkpointState = {
|
|
4403
4783
|
checkpointMessageCount: this.agent.state.messages.length,
|
|
4404
4784
|
checkpointEntryId,
|
|
4405
|
-
startedAt:
|
|
4785
|
+
startedAt:
|
|
4786
|
+
(semanticDetails && stringProperty(semanticDetails, "startedAt")) ?? new Date().toISOString(),
|
|
4406
4787
|
};
|
|
4407
4788
|
this.#pendingRewindReport = undefined;
|
|
4408
4789
|
this.#lastCompletedRewind = undefined;
|
|
4409
4790
|
}
|
|
4410
|
-
if (toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4411
|
-
const detailReport =
|
|
4791
|
+
if (semanticResult?.toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4792
|
+
const detailReport = semanticDetails ? (stringProperty(semanticDetails, "report")?.trim() ?? "") : "";
|
|
4412
4793
|
const textReport = content?.find(part => part.type === "text")?.text?.trim() ?? "";
|
|
4413
4794
|
const report = detailReport || textReport;
|
|
4414
4795
|
if (report.length > 0) {
|
|
@@ -4421,8 +4802,11 @@ export class AgentSession {
|
|
|
4421
4802
|
// Check auto-retry and auto-compaction after agent completes
|
|
4422
4803
|
if (event.type === "agent_end") {
|
|
4423
4804
|
const settledMessages = this.agent.state.messages;
|
|
4424
|
-
|
|
4425
|
-
|
|
4805
|
+
// TTSR retry work runs concurrently and clears the live flag before
|
|
4806
|
+
// maintenance can emit agent_end, so preserve the state at settle entry.
|
|
4807
|
+
const ttsrAbortPendingAtAgentEnd = this.#ttsrAbortPending;
|
|
4808
|
+
const emitAgentEndNotification = async (options?: { willContinue?: boolean }) => {
|
|
4809
|
+
await this.#emitAgentEndNotification(settledMessages, options);
|
|
4426
4810
|
};
|
|
4427
4811
|
const usage = this.getSessionStats().tokens;
|
|
4428
4812
|
await this.#goalRuntime.onAgentEnd({
|
|
@@ -4524,7 +4908,7 @@ export class AgentSession {
|
|
|
4524
4908
|
// active-goal threshold pre-empt below.
|
|
4525
4909
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
4526
4910
|
maintenanceRoute("empty-stop-handled");
|
|
4527
|
-
await emitAgentEndNotification();
|
|
4911
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4528
4912
|
return;
|
|
4529
4913
|
}
|
|
4530
4914
|
|
|
@@ -4547,30 +4931,34 @@ export class AgentSession {
|
|
|
4547
4931
|
automaticContinuationBlocked: compactionResult.automaticContinuationBlocked === true,
|
|
4548
4932
|
});
|
|
4549
4933
|
this.#resolveRetry();
|
|
4550
|
-
await emitAgentEndNotification(
|
|
4934
|
+
await emitAgentEndNotification(
|
|
4935
|
+
compactionResult.continuationScheduled ? { willContinue: true } : undefined,
|
|
4936
|
+
);
|
|
4551
4937
|
return;
|
|
4552
4938
|
}
|
|
4553
4939
|
}
|
|
4554
4940
|
|
|
4555
4941
|
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
4556
4942
|
maintenanceRoute("unexpected-stop-handled");
|
|
4557
|
-
await emitAgentEndNotification();
|
|
4943
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4558
4944
|
return;
|
|
4559
4945
|
}
|
|
4560
4946
|
|
|
4561
4947
|
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
4562
4948
|
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
4563
4949
|
if (didRetry) {
|
|
4564
|
-
await emitAgentEndNotification();
|
|
4950
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4565
4951
|
return;
|
|
4566
4952
|
}
|
|
4567
4953
|
}
|
|
4568
4954
|
|
|
4569
|
-
// A deliberate abort should settle the current turn, not trigger queued
|
|
4955
|
+
// A deliberate abort should settle the current turn, not trigger queued
|
|
4956
|
+
// continuations — except TTSR self-repair, which already scheduled a
|
|
4957
|
+
// hidden retry while #ttsrAbortPending is still true.
|
|
4570
4958
|
if (msg.stopReason === "aborted") {
|
|
4571
4959
|
this.#resolveRetry();
|
|
4572
4960
|
this.#resetSessionStopContinuationState();
|
|
4573
|
-
await emitAgentEndNotification();
|
|
4961
|
+
await emitAgentEndNotification(ttsrAbortPendingAtAgentEnd ? { willContinue: true } : undefined);
|
|
4574
4962
|
return;
|
|
4575
4963
|
}
|
|
4576
4964
|
// Fireworks Fast variants degrade to their base model on a failed turn —
|
|
@@ -4579,14 +4967,14 @@ export class AgentSession {
|
|
|
4579
4967
|
if (this.#isFireworksFastFallbackEligible(msg)) {
|
|
4580
4968
|
const didRetry = await this.#handleRetryableError(msg, { fireworksFastFallback: true });
|
|
4581
4969
|
if (didRetry) {
|
|
4582
|
-
await emitAgentEndNotification();
|
|
4970
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4583
4971
|
return;
|
|
4584
4972
|
}
|
|
4585
4973
|
}
|
|
4586
4974
|
if (this.#isRetryableError(msg)) {
|
|
4587
4975
|
const didRetry = await this.#handleRetryableError(msg);
|
|
4588
4976
|
if (didRetry) {
|
|
4589
|
-
await emitAgentEndNotification();
|
|
4977
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4590
4978
|
return;
|
|
4591
4979
|
}
|
|
4592
4980
|
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
@@ -4597,7 +4985,7 @@ export class AgentSession {
|
|
|
4597
4985
|
// backoff-retry of the failing model) when no switch happens.
|
|
4598
4986
|
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4599
4987
|
if (didRetry) {
|
|
4600
|
-
await emitAgentEndNotification();
|
|
4988
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4601
4989
|
return;
|
|
4602
4990
|
}
|
|
4603
4991
|
}
|
|
@@ -4636,22 +5024,22 @@ export class AgentSession {
|
|
|
4636
5024
|
compactionResult.continuationScheduled ||
|
|
4637
5025
|
compactionResult.automaticContinuationBlocked
|
|
4638
5026
|
) {
|
|
4639
|
-
await emitAgentEndNotification();
|
|
5027
|
+
await emitAgentEndNotification(compactionResult.continuationScheduled ? { willContinue: true } : undefined);
|
|
4640
5028
|
return;
|
|
4641
5029
|
}
|
|
4642
5030
|
if (msg.stopReason !== "error") {
|
|
4643
5031
|
if (this.#enforceRewindBeforeYield()) {
|
|
4644
|
-
await emitAgentEndNotification();
|
|
5032
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4645
5033
|
return;
|
|
4646
5034
|
}
|
|
4647
5035
|
const planModeContinuationScheduled = await this.#enforcePlanModeDecisionAtSettle();
|
|
4648
5036
|
if (planModeContinuationScheduled) {
|
|
4649
|
-
await emitAgentEndNotification();
|
|
5037
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4650
5038
|
return;
|
|
4651
5039
|
}
|
|
4652
5040
|
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
4653
5041
|
if (todoContinuationScheduled) {
|
|
4654
|
-
await emitAgentEndNotification();
|
|
5042
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4655
5043
|
return;
|
|
4656
5044
|
}
|
|
4657
5045
|
}
|
|
@@ -4661,11 +5049,11 @@ export class AgentSession {
|
|
|
4661
5049
|
// the session is fully idle (the todo reminder above defers the same
|
|
4662
5050
|
// way inside #checkTodoCompletion).
|
|
4663
5051
|
if (this.#hasPendingAsyncWake()) {
|
|
4664
|
-
await emitAgentEndNotification();
|
|
5052
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4665
5053
|
return;
|
|
4666
5054
|
}
|
|
4667
|
-
await this.#emitSessionStopEvent(settledMessages, msg);
|
|
4668
|
-
await emitAgentEndNotification();
|
|
5055
|
+
const sessionStopWillContinue = await this.#emitSessionStopEvent(settledMessages, msg);
|
|
5056
|
+
await emitAgentEndNotification(sessionStopWillContinue ? { willContinue: true } : undefined);
|
|
4669
5057
|
}
|
|
4670
5058
|
};
|
|
4671
5059
|
|
|
@@ -4795,7 +5183,28 @@ export class AgentSession {
|
|
|
4795
5183
|
);
|
|
4796
5184
|
}
|
|
4797
5185
|
|
|
4798
|
-
#
|
|
5186
|
+
#scheduleCompactionContinuation(options: {
|
|
5187
|
+
generation: number;
|
|
5188
|
+
autoContinue: boolean;
|
|
5189
|
+
terminalTextAnswer: boolean;
|
|
5190
|
+
suppressContinuation: boolean;
|
|
5191
|
+
}): boolean {
|
|
5192
|
+
if (options.suppressContinuation) return false;
|
|
5193
|
+
if (this.agent.hasQueuedMessages()) {
|
|
5194
|
+
this.#scheduleAgentContinue({
|
|
5195
|
+
delayMs: 100,
|
|
5196
|
+
generation: options.generation,
|
|
5197
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
5198
|
+
});
|
|
5199
|
+
return true;
|
|
5200
|
+
}
|
|
5201
|
+
if (!options.autoContinue) return false;
|
|
5202
|
+
const activeGoal = this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active";
|
|
5203
|
+
if (options.terminalTextAnswer && !activeGoal) return false;
|
|
5204
|
+
return this.#scheduleAutoContinuePrompt(options.generation);
|
|
5205
|
+
}
|
|
5206
|
+
|
|
5207
|
+
#scheduleAutoContinuePrompt(generation: number): boolean {
|
|
4799
5208
|
const continuePrompt = async () => {
|
|
4800
5209
|
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
4801
5210
|
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
@@ -4820,10 +5229,18 @@ export class AgentSession {
|
|
|
4820
5229
|
async signal => {
|
|
4821
5230
|
await Promise.resolve();
|
|
4822
5231
|
if (signal.aborted) return;
|
|
5232
|
+
if (this.agent.hasQueuedMessages()) {
|
|
5233
|
+
this.#scheduleAgentContinue({
|
|
5234
|
+
generation,
|
|
5235
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
5236
|
+
});
|
|
5237
|
+
return;
|
|
5238
|
+
}
|
|
4823
5239
|
await continuePrompt();
|
|
4824
5240
|
},
|
|
4825
5241
|
{ generation },
|
|
4826
5242
|
);
|
|
5243
|
+
return true;
|
|
4827
5244
|
}
|
|
4828
5245
|
|
|
4829
5246
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -5884,15 +6301,22 @@ export class AgentSession {
|
|
|
5884
6301
|
return undefined;
|
|
5885
6302
|
}
|
|
5886
6303
|
|
|
5887
|
-
async #emitAgentEndNotification(messages: AgentMessage[]): Promise<void> {
|
|
5888
|
-
await this.#extensionRunner?.emit({
|
|
6304
|
+
async #emitAgentEndNotification(messages: AgentMessage[], options?: { willContinue?: boolean }): Promise<void> {
|
|
6305
|
+
await this.#extensionRunner?.emit({
|
|
6306
|
+
type: "agent_end",
|
|
6307
|
+
messages,
|
|
6308
|
+
willContinue: options?.willContinue,
|
|
6309
|
+
});
|
|
5889
6310
|
}
|
|
5890
6311
|
|
|
6312
|
+
/** @returns true when a hidden session_stop continuation turn was scheduled. */
|
|
5891
6313
|
async #emitSessionStopEvent(
|
|
5892
6314
|
messages: AgentMessage[],
|
|
5893
6315
|
lastAssistantMessage = this.getLastAssistantMessage(),
|
|
5894
|
-
): Promise<
|
|
5895
|
-
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop"))
|
|
6316
|
+
): Promise<boolean> {
|
|
6317
|
+
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop")) {
|
|
6318
|
+
return false;
|
|
6319
|
+
}
|
|
5896
6320
|
const generation = this.#promptGeneration;
|
|
5897
6321
|
const result = await this.#extensionRunner.emitSessionStop({
|
|
5898
6322
|
messages,
|
|
@@ -5904,12 +6328,12 @@ export class AgentSession {
|
|
|
5904
6328
|
});
|
|
5905
6329
|
if (this.#promptGeneration !== generation || this.#abortInProgress || this.#isDisposed) {
|
|
5906
6330
|
this.#resetSessionStopContinuationState();
|
|
5907
|
-
return;
|
|
6331
|
+
return false;
|
|
5908
6332
|
}
|
|
5909
6333
|
const additionalContext = this.#sessionStopContinuationContext(result);
|
|
5910
6334
|
if (!additionalContext) {
|
|
5911
6335
|
this.#resetSessionStopContinuationState();
|
|
5912
|
-
return;
|
|
6336
|
+
return false;
|
|
5913
6337
|
}
|
|
5914
6338
|
if (this.#sessionStopContinuationCount >= SESSION_STOP_CONTINUATION_CAP) {
|
|
5915
6339
|
logger.warn("session_stop continuation cap reached", {
|
|
@@ -5917,7 +6341,7 @@ export class AgentSession {
|
|
|
5917
6341
|
cap: SESSION_STOP_CONTINUATION_CAP,
|
|
5918
6342
|
});
|
|
5919
6343
|
this.#resetSessionStopContinuationState();
|
|
5920
|
-
return;
|
|
6344
|
+
return false;
|
|
5921
6345
|
}
|
|
5922
6346
|
this.#sessionStopContinuationCount++;
|
|
5923
6347
|
this.#sessionStopHookActive = true;
|
|
@@ -5932,6 +6356,7 @@ export class AgentSession {
|
|
|
5932
6356
|
},
|
|
5933
6357
|
true,
|
|
5934
6358
|
);
|
|
6359
|
+
return true;
|
|
5935
6360
|
}
|
|
5936
6361
|
|
|
5937
6362
|
/** Emit extension events based on session events */
|
|
@@ -6204,6 +6629,43 @@ export class AgentSession {
|
|
|
6204
6629
|
await this.refreshBaseSystemPrompt();
|
|
6205
6630
|
}
|
|
6206
6631
|
}
|
|
6632
|
+
/** Run one abortable auto-learn capture outside the primary agent loop. */
|
|
6633
|
+
async runAutolearnCapture(capture: (signal: AbortSignal) => Promise<void>): Promise<void> {
|
|
6634
|
+
if (this.#autolearnCaptureTask || this.#isDisposed) return;
|
|
6635
|
+
const controller = new AbortController();
|
|
6636
|
+
this.#autolearnCaptureAbortController = controller;
|
|
6637
|
+
const task = (async () => {
|
|
6638
|
+
try {
|
|
6639
|
+
await capture(controller.signal);
|
|
6640
|
+
} catch (error) {
|
|
6641
|
+
if (!controller.signal.aborted) throw error;
|
|
6642
|
+
} finally {
|
|
6643
|
+
if (this.#autolearnCaptureAbortController === controller) {
|
|
6644
|
+
this.#autolearnCaptureAbortController = undefined;
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
})();
|
|
6648
|
+
this.#autolearnCaptureTask = task;
|
|
6649
|
+
try {
|
|
6650
|
+
await task;
|
|
6651
|
+
} finally {
|
|
6652
|
+
if (this.#autolearnCaptureTask === task) this.#autolearnCaptureTask = undefined;
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
|
|
6656
|
+
#abortAutolearnCapture(): void {
|
|
6657
|
+
this.#autolearnCaptureAbortController?.abort();
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
async #drainAutolearnCapture(): Promise<void> {
|
|
6661
|
+
const task = this.#autolearnCaptureTask;
|
|
6662
|
+
if (!task) return;
|
|
6663
|
+
try {
|
|
6664
|
+
await withTimeout(task, 3_000, "Timed out draining auto-learn capture during dispose");
|
|
6665
|
+
} catch (error) {
|
|
6666
|
+
logger.warn("Auto-learn capture did not settle during dispose", { error: String(error) });
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6207
6669
|
|
|
6208
6670
|
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
6209
6671
|
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
@@ -6226,6 +6688,8 @@ export class AgentSession {
|
|
|
6226
6688
|
*/
|
|
6227
6689
|
beginDispose(): void {
|
|
6228
6690
|
this.#isDisposed = true;
|
|
6691
|
+
this.#titleGenerationAbortController.abort();
|
|
6692
|
+
this.#abortAutolearnCapture();
|
|
6229
6693
|
this.#flushPendingIrcAsides();
|
|
6230
6694
|
this.yieldQueue.clear();
|
|
6231
6695
|
this.agent.setAsideMessageProvider(undefined);
|
|
@@ -6262,6 +6726,12 @@ export class AgentSession {
|
|
|
6262
6726
|
} catch (error) {
|
|
6263
6727
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6264
6728
|
}
|
|
6729
|
+
// Clear any timers extensions scheduled via `ctx.setInterval`/`ctx.setTimeout`
|
|
6730
|
+
// so their background work does not outlive the session (issue #5664).
|
|
6731
|
+
// Optional-called: hosts and tests may inject partial runner facades that
|
|
6732
|
+
// implement only the dispatch surface.
|
|
6733
|
+
this.#extensionRunner?.clearManagedTimers?.();
|
|
6734
|
+
this.#fallbackExtensionTimers?.clearAll();
|
|
6265
6735
|
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
6266
6736
|
// deferred-handoff task that has already advanced into
|
|
6267
6737
|
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
@@ -6279,6 +6749,7 @@ export class AgentSession {
|
|
|
6279
6749
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
6280
6750
|
this.agent.abort();
|
|
6281
6751
|
await postPromptDrain;
|
|
6752
|
+
await this.#drainAutolearnCapture();
|
|
6282
6753
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
6283
6754
|
// leak its background bash/task work into the parent's manager. Only
|
|
6284
6755
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -6753,82 +7224,159 @@ export class AgentSession {
|
|
|
6753
7224
|
|
|
6754
7225
|
async #applyActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6755
7226
|
toolNames = normalizeToolNames(toolNames);
|
|
7227
|
+
const selectedTools = toolNames.flatMap(name => {
|
|
7228
|
+
const tool = this.#toolRegistry.get(name);
|
|
7229
|
+
return tool ? [{ name, tool }] : [];
|
|
7230
|
+
});
|
|
7231
|
+
const xdevReadAvailable = this.#builtInToolNames.has("read") && selectedTools.some(({ name }) => name === "read");
|
|
7232
|
+
const isPresentationPinned = (name: string): boolean =>
|
|
7233
|
+
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
7234
|
+
const mountCandidates = selectedTools.filter(
|
|
7235
|
+
({ name, tool }) =>
|
|
7236
|
+
this.#xdevRegistry !== undefined &&
|
|
7237
|
+
xdevReadAvailable &&
|
|
7238
|
+
!isPresentationPinned(name) &&
|
|
7239
|
+
isMountableUnderXdev(tool),
|
|
7240
|
+
);
|
|
7241
|
+
|
|
7242
|
+
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
7243
|
+
if (mountCandidates.length > 0 && !builtInWriteAvailable) {
|
|
7244
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
7245
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
7246
|
+
}
|
|
7247
|
+
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
6756
7248
|
const tools: AgentTool[] = [];
|
|
6757
7249
|
const validToolNames: string[] = [];
|
|
6758
7250
|
const mountedTools: AgentTool[] = [];
|
|
6759
|
-
for (const name of
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
// Discoverable tools are presented as `xd://` devices (kept out of the
|
|
6763
|
-
// top-level schema) when the transport is active; everything else stays
|
|
6764
|
-
// top-level. `loadMode` decides presentation only — selection is upstream.
|
|
6765
|
-
if (this.#xdevRegistry && isMountableUnderXdev(tool)) {
|
|
6766
|
-
mountedTools.push(tool);
|
|
7251
|
+
for (const { name, tool } of selectedTools) {
|
|
7252
|
+
if (mountNames.has(name)) {
|
|
7253
|
+
mountedTools.push(this.#wrapToolForAcpPermission(tool));
|
|
6767
7254
|
} else {
|
|
6768
7255
|
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
6769
7256
|
validToolNames.push(name);
|
|
6770
7257
|
}
|
|
6771
7258
|
}
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
7259
|
+
|
|
7260
|
+
const pinnedWrite = isPresentationPinned("write");
|
|
7261
|
+
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
7262
|
+
const transportNeeded = mountedTools.length > 0 || activeDeferrableTool || this.#planModeState?.enabled === true;
|
|
7263
|
+
if (transportNeeded && !builtInWriteAvailable) {
|
|
7264
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
7265
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
7266
|
+
}
|
|
7267
|
+
if (transportNeeded && builtInWriteAvailable) {
|
|
7268
|
+
const write = this.#toolRegistry.get("write");
|
|
7269
|
+
if (write && !validToolNames.includes("write")) {
|
|
7270
|
+
tools.push(this.#wrapToolForAcpPermission(write));
|
|
7271
|
+
validToolNames.push("write");
|
|
7272
|
+
}
|
|
7273
|
+
} else if (
|
|
7274
|
+
!pinnedWrite &&
|
|
7275
|
+
(this.#presentationPinnedToolNames !== undefined || this.#runtimeSelectedToolNames !== undefined)
|
|
7276
|
+
) {
|
|
7277
|
+
const writeNameIndex = validToolNames.indexOf("write");
|
|
7278
|
+
if (writeNameIndex >= 0 && this.#builtInToolNames.has("write")) validToolNames.splice(writeNameIndex, 1);
|
|
7279
|
+
const writeToolIndex = tools.findIndex(tool => tool.name === "write" && this.#builtInToolNames.has("write"));
|
|
7280
|
+
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
7281
|
+
}
|
|
7282
|
+
|
|
6775
7283
|
const previousMounted = this.#mountedXdevToolNames;
|
|
7284
|
+
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
7285
|
+
const tool = this.#xdevRegistry?.get(name);
|
|
7286
|
+
return tool ? [tool] : [];
|
|
7287
|
+
});
|
|
7288
|
+
const previousActiveToolNames = this.getActiveToolNames();
|
|
6776
7289
|
this.#mountedXdevToolNames = new Set(mountedTools.map(tool => tool.name));
|
|
6777
7290
|
this.#xdevRegistry?.reconcile(mountedTools);
|
|
6778
|
-
this.#notifyXdevMountDelta(previousMounted);
|
|
6779
7291
|
this.#setActiveToolNames?.(validToolNames);
|
|
6780
|
-
this.agent.setTools(tools);
|
|
6781
7292
|
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
this.#clearInheritedProviderPromptCacheKey();
|
|
7293
|
+
let rebuiltSystemPrompt: string[] | undefined;
|
|
7294
|
+
let rebuiltSignature: string | undefined;
|
|
7295
|
+
try {
|
|
7296
|
+
if (this.#rebuildSystemPrompt) {
|
|
7297
|
+
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
7298
|
+
if (signature !== this.#lastAppliedToolSignature) {
|
|
7299
|
+
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
7300
|
+
rebuiltSystemPrompt = built.systemPrompt;
|
|
7301
|
+
rebuiltSignature = signature;
|
|
6792
7302
|
}
|
|
6793
|
-
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
6794
|
-
this.#baseSystemPrompt = built.systemPrompt;
|
|
6795
|
-
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
6796
|
-
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
6797
|
-
this.#lastAppliedToolSignature = signature;
|
|
6798
|
-
this.#promptModelKey = this.#currentPromptModelKey();
|
|
6799
7303
|
}
|
|
7304
|
+
} catch (error) {
|
|
7305
|
+
this.#mountedXdevToolNames = previousMounted;
|
|
7306
|
+
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
7307
|
+
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
7308
|
+
throw error;
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
this.#notifyXdevMountDelta(previousMounted);
|
|
7312
|
+
this.agent.setTools(tools);
|
|
7313
|
+
if (rebuiltSystemPrompt && rebuiltSignature) {
|
|
7314
|
+
if (this.#lastAppliedToolSignature !== undefined) this.#clearInheritedProviderPromptCacheKey();
|
|
7315
|
+
this.#baseSystemPrompt = rebuiltSystemPrompt;
|
|
7316
|
+
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
7317
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
7318
|
+
this.#lastAppliedToolSignature = rebuiltSignature;
|
|
7319
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
6800
7320
|
}
|
|
6801
7321
|
}
|
|
6802
7322
|
|
|
6803
7323
|
/**
|
|
6804
|
-
*
|
|
6805
|
-
*
|
|
6806
|
-
*
|
|
6807
|
-
*
|
|
6808
|
-
*
|
|
6809
|
-
*
|
|
7324
|
+
* Record a mid-session `xd://` mount delta for the model without rewriting
|
|
7325
|
+
* the system prompt: the prompt (and its provider cache prefix) stays
|
|
7326
|
+
* byte-stable across MCP connects and disconnects. The delta is NOT steered
|
|
7327
|
+
* immediately — a steered notice landing at a run's stop boundary (or while
|
|
7328
|
+
* the session is idle) forces an unsolicited extra assistant turn — it is
|
|
7329
|
+
* coalesced into {@link #pendingXdevMountDelta} and rides along with the
|
|
7330
|
+
* next prompt (docs + schema stay one `read xd://<tool>` away). The full
|
|
7331
|
+
* docs join the system prompt opportunistically on the next unrelated
|
|
7332
|
+
* rebuild.
|
|
6810
7333
|
*/
|
|
6811
7334
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
6812
7335
|
const registry = this.#xdevRegistry;
|
|
6813
7336
|
if (!registry) return;
|
|
6814
7337
|
const current = this.#mountedXdevToolNames;
|
|
6815
7338
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
6816
|
-
const
|
|
6817
|
-
if (addedNames.length === 0 &&
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
7339
|
+
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
7340
|
+
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
7341
|
+
// Coalesce against the unannounced delta: an unmount cancels a pending
|
|
7342
|
+
// mount the model never learned about, and a remount cancels a pending
|
|
7343
|
+
// unmount.
|
|
7344
|
+
const pending = this.#pendingXdevMountDelta ?? { added: new Set<string>(), removed: new Set<string>() };
|
|
7345
|
+
for (const name of addedNames) {
|
|
7346
|
+
if (!pending.removed.delete(name)) pending.added.add(name);
|
|
7347
|
+
}
|
|
7348
|
+
for (const name of removedNames) {
|
|
7349
|
+
if (!pending.added.delete(name)) pending.removed.add(name);
|
|
7350
|
+
}
|
|
7351
|
+
this.#pendingXdevMountDelta = pending.added.size > 0 || pending.removed.size > 0 ? pending : undefined;
|
|
7352
|
+
if (this.settings.get("startup.quiet")) return;
|
|
7353
|
+
const parts: string[] = [];
|
|
7354
|
+
if (addedNames.length > 0) parts.push(`mounted ${addedNames.join(", ")}`);
|
|
7355
|
+
if (removedNames.length > 0) parts.push(`unmounted ${removedNames.join(", ")}`);
|
|
7356
|
+
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
/**
|
|
7360
|
+
* Render and consume the pending xd:// mount delta as a hidden notice, or
|
|
7361
|
+
* `undefined` when nothing unannounced is queued. Called from the prompt
|
|
7362
|
+
* paths so the notice rides along with user input instead of forcing its
|
|
7363
|
+
* own model turn.
|
|
7364
|
+
*/
|
|
7365
|
+
#takePendingXdevMountNotice(): CustomMessage | undefined {
|
|
7366
|
+
const pending = this.#pendingXdevMountDelta;
|
|
7367
|
+
if (!pending) return undefined;
|
|
7368
|
+
this.#pendingXdevMountDelta = undefined;
|
|
7369
|
+
const summaries = new Map(this.#xdevRegistry?.entries().map(entry => [entry.name, entry.summary]) ?? []);
|
|
7370
|
+
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
7371
|
+
const removed = [...pending.removed].map(name => ({ name }));
|
|
7372
|
+
return {
|
|
6821
7373
|
role: "custom",
|
|
6822
7374
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
6823
7375
|
content: prompt.render(xdevMountNoticePrompt, { added, removed }),
|
|
6824
7376
|
attribution: "agent",
|
|
6825
7377
|
display: false,
|
|
6826
7378
|
timestamp: Date.now(),
|
|
6827
|
-
}
|
|
6828
|
-
const parts: string[] = [];
|
|
6829
|
-
if (added.length > 0) parts.push(`mounted ${added.map(entry => entry.name).join(", ")}`);
|
|
6830
|
-
if (removed.length > 0) parts.push(`unmounted ${removed.map(entry => entry.name).join(", ")}`);
|
|
6831
|
-
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7379
|
+
};
|
|
6832
7380
|
}
|
|
6833
7381
|
|
|
6834
7382
|
/**
|
|
@@ -6866,7 +7414,24 @@ export class AgentSession {
|
|
|
6866
7414
|
* Changes take effect before the next model call.
|
|
6867
7415
|
*/
|
|
6868
7416
|
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6869
|
-
|
|
7417
|
+
const mounted = this.#mountedXdevToolNames;
|
|
7418
|
+
const normalized = normalizeToolNames(toolNames);
|
|
7419
|
+
const transportWriteActive =
|
|
7420
|
+
this.#builtInToolNames.has("write") &&
|
|
7421
|
+
this.getActiveToolNames().includes("write") &&
|
|
7422
|
+
this.#presentationPinnedToolNames?.has("write") !== true &&
|
|
7423
|
+
this.#runtimeSelectedToolNames?.has("write") !== true &&
|
|
7424
|
+
(mounted.size > 0 || this.#planModeState?.enabled === true);
|
|
7425
|
+
const previousRuntimeSelectedToolNames = this.#runtimeSelectedToolNames;
|
|
7426
|
+
this.#runtimeSelectedToolNames = new Set(
|
|
7427
|
+
normalized.filter(name => !mounted.has(name) && !(name === "write" && transportWriteActive)),
|
|
7428
|
+
);
|
|
7429
|
+
try {
|
|
7430
|
+
await this.#applyActiveToolsByName(normalized);
|
|
7431
|
+
} catch (error) {
|
|
7432
|
+
this.#runtimeSelectedToolNames = previousRuntimeSelectedToolNames;
|
|
7433
|
+
throw error;
|
|
7434
|
+
}
|
|
6870
7435
|
}
|
|
6871
7436
|
|
|
6872
7437
|
/** Rebuild the base system prompt using the current active tool set. */
|
|
@@ -7009,6 +7574,12 @@ export class AgentSession {
|
|
|
7009
7574
|
*/
|
|
7010
7575
|
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
7011
7576
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
7577
|
+
const previousMcpTools = new Map(
|
|
7578
|
+
existingNames.flatMap(name => {
|
|
7579
|
+
const tool = this.#toolRegistry.get(name);
|
|
7580
|
+
return isMCPToolName(name) && tool ? [[name, tool] as const] : [];
|
|
7581
|
+
}),
|
|
7582
|
+
);
|
|
7012
7583
|
for (const name of existingNames) {
|
|
7013
7584
|
if (isMCPToolName(name)) {
|
|
7014
7585
|
this.#toolRegistry.delete(name);
|
|
@@ -7036,10 +7607,18 @@ export class AgentSession {
|
|
|
7036
7607
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
7037
7608
|
}
|
|
7038
7609
|
|
|
7039
|
-
// Every connected MCP tool is
|
|
7040
|
-
//
|
|
7610
|
+
// Every connected MCP tool is selected; centralized repartitioning owns
|
|
7611
|
+
// presentation pins and write-transport activation/removal.
|
|
7041
7612
|
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...mcpTools.map(tool => tool.name)])];
|
|
7042
|
-
|
|
7613
|
+
try {
|
|
7614
|
+
await this.#applyActiveToolsByName(nextActive);
|
|
7615
|
+
} catch (error) {
|
|
7616
|
+
for (const name of this.#toolRegistry.keys()) {
|
|
7617
|
+
if (isMCPToolName(name)) this.#toolRegistry.delete(name);
|
|
7618
|
+
}
|
|
7619
|
+
for (const [name, tool] of previousMcpTools) this.#toolRegistry.set(name, tool);
|
|
7620
|
+
throw error;
|
|
7621
|
+
}
|
|
7043
7622
|
}
|
|
7044
7623
|
|
|
7045
7624
|
/**
|
|
@@ -7060,6 +7639,12 @@ export class AgentSession {
|
|
|
7060
7639
|
|
|
7061
7640
|
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
7062
7641
|
const previousActiveToolNames = this.getEnabledToolNames();
|
|
7642
|
+
const previousRpcHostTools = new Map(
|
|
7643
|
+
[...previousRpcHostToolNames].flatMap(name => {
|
|
7644
|
+
const tool = this.#toolRegistry.get(name);
|
|
7645
|
+
return tool ? [[name, tool] as const] : [];
|
|
7646
|
+
}),
|
|
7647
|
+
);
|
|
7063
7648
|
for (const name of previousRpcHostToolNames) {
|
|
7064
7649
|
this.#toolRegistry.delete(name);
|
|
7065
7650
|
}
|
|
@@ -7081,9 +7666,16 @@ export class AgentSession {
|
|
|
7081
7666
|
const autoActivatedRpcToolNames = rpcTools
|
|
7082
7667
|
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
7083
7668
|
.map(tool => tool.name);
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7669
|
+
try {
|
|
7670
|
+
await this.#applyActiveToolsByName(
|
|
7671
|
+
Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
|
|
7672
|
+
);
|
|
7673
|
+
} catch (error) {
|
|
7674
|
+
for (const name of this.#rpcHostToolNames) this.#toolRegistry.delete(name);
|
|
7675
|
+
this.#rpcHostToolNames = previousRpcHostToolNames;
|
|
7676
|
+
for (const [name, tool] of previousRpcHostTools) this.#toolRegistry.set(name, tool);
|
|
7677
|
+
throw error;
|
|
7678
|
+
}
|
|
7087
7679
|
}
|
|
7088
7680
|
|
|
7089
7681
|
/** Whether auto-compaction is currently running */
|
|
@@ -7411,6 +8003,11 @@ export class AgentSession {
|
|
|
7411
8003
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
7412
8004
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
7413
8005
|
this.agent.setTools(activeTools);
|
|
8006
|
+
const mountedTools = [...this.#mountedXdevToolNames]
|
|
8007
|
+
.map(name => this.#toolRegistry.get(name))
|
|
8008
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
8009
|
+
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
8010
|
+
this.#xdevRegistry?.reconcile(mountedTools);
|
|
7414
8011
|
}
|
|
7415
8012
|
|
|
7416
8013
|
#clearCheckpointRuntimeState(): void {
|
|
@@ -8072,7 +8669,7 @@ export class AgentSession {
|
|
|
8072
8669
|
const generation = this.#promptGeneration;
|
|
8073
8670
|
try {
|
|
8074
8671
|
// Flush any pending bash messages before the new prompt
|
|
8075
|
-
this.#flushPendingBashMessages();
|
|
8672
|
+
await this.#flushPendingBashMessages();
|
|
8076
8673
|
this.#flushPendingPythonMessages();
|
|
8077
8674
|
this.#flushPendingIrcAsides();
|
|
8078
8675
|
|
|
@@ -8137,14 +8734,19 @@ export class AgentSession {
|
|
|
8137
8734
|
messages.push(...options.prependMessages);
|
|
8138
8735
|
}
|
|
8139
8736
|
|
|
8140
|
-
messages.push(message);
|
|
8141
|
-
|
|
8142
8737
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
8143
8738
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
8144
8739
|
if (this.#promptGeneration !== generation) {
|
|
8145
8740
|
return;
|
|
8146
8741
|
}
|
|
8147
8742
|
|
|
8743
|
+
// A pending xd:// delta accompanies the next user-authored prompt,
|
|
8744
|
+
// never an agent-initiated continuation.
|
|
8745
|
+
const xdevMountNotice = isUserQueuedMessage(message) ? this.#takePendingXdevMountNotice() : undefined;
|
|
8746
|
+
if (xdevMountNotice) {
|
|
8747
|
+
messages.push(xdevMountNotice);
|
|
8748
|
+
}
|
|
8749
|
+
messages.push(message);
|
|
8148
8750
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
8149
8751
|
for (const msg of this.#pendingNextTurnMessages) {
|
|
8150
8752
|
messages.push(msg);
|
|
@@ -8346,9 +8948,20 @@ export class AgentSession {
|
|
|
8346
8948
|
await this.reload();
|
|
8347
8949
|
},
|
|
8348
8950
|
getSystemPrompt: () => this.systemPrompt,
|
|
8951
|
+
setInterval: (callback, ms, ...args) => this.#fallbackTimers().setInterval(callback, ms, ...args),
|
|
8952
|
+
setTimeout: (callback, ms, ...args) => this.#fallbackTimers().setTimeout(callback, ms, ...args),
|
|
8953
|
+
clearTimer: timer => this.#fallbackTimers().clear(timer),
|
|
8349
8954
|
};
|
|
8350
8955
|
}
|
|
8351
8956
|
|
|
8957
|
+
/** Lazily create the runner-less command-context timer registry (#5664). */
|
|
8958
|
+
#fallbackTimers(): ManagedTimers {
|
|
8959
|
+
this.#fallbackExtensionTimers ??= new ManagedTimers((event, error) =>
|
|
8960
|
+
logger.warn("Extension timer callback threw", { event, error }),
|
|
8961
|
+
);
|
|
8962
|
+
return this.#fallbackExtensionTimers;
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8352
8965
|
/**
|
|
8353
8966
|
* Try to execute a custom command. Returns the prompt string if found, null otherwise.
|
|
8354
8967
|
* If the command returns void, returns empty string to indicate it was handled.
|
|
@@ -8982,16 +9595,26 @@ export class AgentSession {
|
|
|
8982
9595
|
this.#replanTitleRefreshInFlight = refresh;
|
|
8983
9596
|
}
|
|
8984
9597
|
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
9598
|
+
/**
|
|
9599
|
+
* Generate an automatic session title tied to this session's lifecycle.
|
|
9600
|
+
* Input and replan callers share the signal so disposal cancels provider and
|
|
9601
|
+
* local-worker requests instead of leaving background inference alive.
|
|
9602
|
+
*/
|
|
9603
|
+
generateTitle(firstMessage: string): Promise<string | null> {
|
|
9604
|
+
return generateSessionTitle(
|
|
9605
|
+
firstMessage,
|
|
8988
9606
|
this.#modelRegistry,
|
|
8989
9607
|
this.settings,
|
|
8990
|
-
sessionId,
|
|
9608
|
+
this.sessionId,
|
|
8991
9609
|
this.model,
|
|
8992
9610
|
provider => this.agent.metadataForProvider(provider),
|
|
8993
9611
|
this.#titleSystemPrompt,
|
|
9612
|
+
this.#titleGenerationAbortController.signal,
|
|
8994
9613
|
);
|
|
9614
|
+
}
|
|
9615
|
+
|
|
9616
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
9617
|
+
const title = await this.generateTitle(context);
|
|
8995
9618
|
if (!title) return;
|
|
8996
9619
|
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
8997
9620
|
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
@@ -9059,6 +9682,7 @@ export class AgentSession {
|
|
|
9059
9682
|
// auto-starting a fresh turn during cleanup.
|
|
9060
9683
|
this.#abortInProgress = true;
|
|
9061
9684
|
try {
|
|
9685
|
+
this.#abortAutolearnCapture();
|
|
9062
9686
|
this.abortRetry();
|
|
9063
9687
|
this.#promptGeneration++;
|
|
9064
9688
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -9080,6 +9704,7 @@ export class AgentSession {
|
|
|
9080
9704
|
this.agent.abort(options?.reason);
|
|
9081
9705
|
await postPromptDrain;
|
|
9082
9706
|
await this.agent.waitForIdle();
|
|
9707
|
+
await this.#drainAutolearnCapture();
|
|
9083
9708
|
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
9084
9709
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
9085
9710
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
@@ -9139,27 +9764,36 @@ export class AgentSession {
|
|
|
9139
9764
|
await this.abort();
|
|
9140
9765
|
this.#cancelOwnAsyncJobs();
|
|
9141
9766
|
this.#closeAllProviderSessions("new session");
|
|
9142
|
-
this
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9767
|
+
await this.#flushPendingBashMessages();
|
|
9768
|
+
const bashTransition = this.#beginBashSessionTransition({ persistDetached: options?.drop !== true });
|
|
9769
|
+
let sessionTransitioned = false;
|
|
9770
|
+
try {
|
|
9771
|
+
this.agent.reset();
|
|
9772
|
+
if (options?.drop && previousSessionFile) {
|
|
9773
|
+
// Detach the advisor recorder feed and drain its writer BEFORE deleting the
|
|
9774
|
+
// old artifacts dir: `await this.abort()` only stops the primary, so a still-
|
|
9775
|
+
// running advisor turn could otherwise finish, emit `message_end`, and recreate
|
|
9776
|
+
// `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
|
|
9777
|
+
// the advisor and re-attaches the feed at the new session's path.
|
|
9778
|
+
for (const a of this.#advisors) {
|
|
9779
|
+
a.agentUnsubscribe?.();
|
|
9780
|
+
a.agentUnsubscribe = undefined;
|
|
9781
|
+
await a.recorder.close();
|
|
9782
|
+
}
|
|
9783
|
+
try {
|
|
9784
|
+
await this.sessionManager.dropSession(previousSessionFile);
|
|
9785
|
+
} catch (err) {
|
|
9786
|
+
logger.error("Failed to delete session during /drop", { err });
|
|
9787
|
+
}
|
|
9788
|
+
} else {
|
|
9789
|
+
await this.sessionManager.flush();
|
|
9158
9790
|
}
|
|
9159
|
-
|
|
9160
|
-
|
|
9791
|
+
await this.sessionManager.newSession(options);
|
|
9792
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9793
|
+
sessionTransitioned = true;
|
|
9794
|
+
} finally {
|
|
9795
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
9161
9796
|
}
|
|
9162
|
-
await this.sessionManager.newSession(options);
|
|
9163
9797
|
|
|
9164
9798
|
this.#clearCheckpointRuntimeState();
|
|
9165
9799
|
this.setTodoPhases([]);
|
|
@@ -9225,14 +9859,25 @@ export class AgentSession {
|
|
|
9225
9859
|
}
|
|
9226
9860
|
}
|
|
9227
9861
|
|
|
9862
|
+
await this.#flushPendingBashMessages();
|
|
9228
9863
|
// Flush current session to ensure all entries are written
|
|
9229
9864
|
await this.sessionManager.flush();
|
|
9865
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
9230
9866
|
|
|
9231
9867
|
// Fork the session (creates new session file with same entries)
|
|
9232
|
-
|
|
9868
|
+
let forkResult: { oldSessionFile: string; newSessionFile: string } | undefined;
|
|
9869
|
+
try {
|
|
9870
|
+
forkResult = await this.sessionManager.fork();
|
|
9871
|
+
} catch (error) {
|
|
9872
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
9873
|
+
throw error;
|
|
9874
|
+
}
|
|
9233
9875
|
if (!forkResult) {
|
|
9876
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
9234
9877
|
return false;
|
|
9235
9878
|
}
|
|
9879
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9880
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
9236
9881
|
|
|
9237
9882
|
// Copy artifacts directory if it exists
|
|
9238
9883
|
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
|
@@ -10415,6 +11060,13 @@ export class AgentSession {
|
|
|
10415
11060
|
this.#compactionAbortController = undefined;
|
|
10416
11061
|
}
|
|
10417
11062
|
this.#reconnectToAgent();
|
|
11063
|
+
// Compaction disconnected before `await abort()`, so abort's finally drain
|
|
11064
|
+
// (and any steer/follow-up that arrived mid-compaction — async IRC, an
|
|
11065
|
+
// `xd://` mount notice, an SDK/RPC steer) was suppressed while disconnected
|
|
11066
|
+
// (issue #5800). Unlike `/new`/switchSession, compaction preserves the agent
|
|
11067
|
+
// queues, so nothing else resumes them: re-drain now that the listener is back
|
|
11068
|
+
// and `isCompacting` is false, or the queued turn hangs until the next prompt.
|
|
11069
|
+
this.#drainStrandedQueuedMessages();
|
|
10418
11070
|
}
|
|
10419
11071
|
}
|
|
10420
11072
|
|
|
@@ -10608,9 +11260,20 @@ export class AgentSession {
|
|
|
10608
11260
|
return undefined;
|
|
10609
11261
|
}
|
|
10610
11262
|
}
|
|
11263
|
+
await this.#flushPendingBashMessages();
|
|
10611
11264
|
await this.sessionManager.flush();
|
|
11265
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
10612
11266
|
this.#cancelOwnAsyncJobs();
|
|
10613
|
-
|
|
11267
|
+
let sessionTransitioned = false;
|
|
11268
|
+
try {
|
|
11269
|
+
await this.sessionManager.newSession(
|
|
11270
|
+
previousSessionFile ? { parentSession: previousSessionFile } : undefined,
|
|
11271
|
+
);
|
|
11272
|
+
this.#markBashSessionTransition(bashTransition);
|
|
11273
|
+
sessionTransitioned = true;
|
|
11274
|
+
} finally {
|
|
11275
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
11276
|
+
}
|
|
10614
11277
|
|
|
10615
11278
|
this.#clearCheckpointRuntimeState();
|
|
10616
11279
|
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
@@ -11070,6 +11733,7 @@ export class AgentSession {
|
|
|
11070
11733
|
autoContinue,
|
|
11071
11734
|
triggerContextTokens: postMaintenanceContextTokens,
|
|
11072
11735
|
phase: "pre_turn",
|
|
11736
|
+
terminalTextAnswer: isTerminalTextAssistantAnswer(assistantMessage),
|
|
11073
11737
|
});
|
|
11074
11738
|
}
|
|
11075
11739
|
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
@@ -11301,11 +11965,12 @@ export class AgentSession {
|
|
|
11301
11965
|
#isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
|
|
11302
11966
|
switch (assistantMessage.stopReason) {
|
|
11303
11967
|
case "stop":
|
|
11304
|
-
//
|
|
11305
|
-
//
|
|
11968
|
+
// Unsigned thinking alone is not actionable, but a signature is
|
|
11969
|
+
// provider-authenticated content and makes the stop terminal.
|
|
11306
11970
|
for (const content of assistantMessage.content) {
|
|
11307
11971
|
if (content.type === "toolCall") return false;
|
|
11308
11972
|
if (content.type === "text" && hasNonWhitespace(content.text)) return false;
|
|
11973
|
+
if (content.type === "thinking" && hasNonWhitespace(content.thinkingSignature ?? "")) return false;
|
|
11309
11974
|
}
|
|
11310
11975
|
return true;
|
|
11311
11976
|
case "toolUse":
|
|
@@ -11503,11 +12168,13 @@ export class AgentSession {
|
|
|
11503
12168
|
|
|
11504
12169
|
if (!branchEntry) return;
|
|
11505
12170
|
const targetParentId = prunePrompt ? parentEntry.parentId : branchEntry.parentId;
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
12171
|
+
this.#withBashBranchTransition(() => {
|
|
12172
|
+
if (targetParentId === null) {
|
|
12173
|
+
this.sessionManager.resetLeaf();
|
|
12174
|
+
} else {
|
|
12175
|
+
this.sessionManager.branch(targetParentId);
|
|
12176
|
+
}
|
|
12177
|
+
});
|
|
11511
12178
|
this.sessionManager.appendCustomEntry("accepted-terminal-empty-stop");
|
|
11512
12179
|
}
|
|
11513
12180
|
|
|
@@ -11534,11 +12201,13 @@ export class AgentSession {
|
|
|
11534
12201
|
if (!branchEntry) {
|
|
11535
12202
|
return;
|
|
11536
12203
|
}
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
12204
|
+
this.#withBashBranchTransition(() => {
|
|
12205
|
+
if (branchEntry.parentId === null) {
|
|
12206
|
+
this.sessionManager.resetLeaf();
|
|
12207
|
+
} else {
|
|
12208
|
+
this.sessionManager.branch(branchEntry.parentId);
|
|
12209
|
+
}
|
|
12210
|
+
});
|
|
11542
12211
|
}
|
|
11543
12212
|
|
|
11544
12213
|
#isSameAssistantMessage(left: AssistantMessage, right: AssistantMessage): boolean {
|
|
@@ -11575,8 +12244,10 @@ export class AgentSession {
|
|
|
11575
12244
|
if (this.#pendingRewindReport) return this.#pendingRewindReport;
|
|
11576
12245
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11577
12246
|
const message = messages[i];
|
|
11578
|
-
if (message?.role !== "toolResult" || message.
|
|
11579
|
-
const
|
|
12247
|
+
if (message?.role !== "toolResult" || message.isError) continue;
|
|
12248
|
+
const semanticResult = semanticToolResult(message.toolName, message);
|
|
12249
|
+
if (semanticResult?.toolName !== "rewind") continue;
|
|
12250
|
+
const details = semanticResult.details;
|
|
11580
12251
|
const detailReport =
|
|
11581
12252
|
details && typeof details === "object" && "report" in details && typeof details.report === "string"
|
|
11582
12253
|
? details.report.trim()
|
|
@@ -11593,16 +12264,18 @@ export class AgentSession {
|
|
|
11593
12264
|
if (!checkpointState) {
|
|
11594
12265
|
return;
|
|
11595
12266
|
}
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
12267
|
+
this.#withBashBranchTransition(() => {
|
|
12268
|
+
try {
|
|
12269
|
+
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
12270
|
+
startedAt: checkpointState.startedAt,
|
|
12271
|
+
});
|
|
12272
|
+
} catch (error) {
|
|
12273
|
+
logger.warn("Rewind branch checkpoint missing, falling back to root", {
|
|
12274
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12275
|
+
});
|
|
12276
|
+
this.sessionManager.branchWithSummary(null, report, { startedAt: checkpointState.startedAt });
|
|
12277
|
+
}
|
|
12278
|
+
});
|
|
11606
12279
|
|
|
11607
12280
|
const rewoundAt = new Date().toISOString();
|
|
11608
12281
|
const details = { report, startedAt: checkpointState.startedAt, rewoundAt };
|
|
@@ -11617,7 +12290,7 @@ export class AgentSession {
|
|
|
11617
12290
|
|
|
11618
12291
|
if (activeMessages) {
|
|
11619
12292
|
for (const message of activeMessages) {
|
|
11620
|
-
if (message.role === "toolResult" && message.toolName === "rewind") {
|
|
12293
|
+
if (message.role === "toolResult" && semanticToolResult(message.toolName, message)?.toolName === "rewind") {
|
|
11621
12294
|
this.#rewoundToolResultIds.add(message.toolCallId);
|
|
11622
12295
|
}
|
|
11623
12296
|
}
|
|
@@ -12916,12 +13589,15 @@ export class AgentSession {
|
|
|
12916
13589
|
suppressContinuation?: boolean;
|
|
12917
13590
|
suppressHandoff?: boolean;
|
|
12918
13591
|
phase?: CodexCompactionContext["phase"];
|
|
13592
|
+
terminalTextAnswer?: boolean;
|
|
12919
13593
|
} = {},
|
|
12920
13594
|
): Promise<CompactionCheckResult> {
|
|
12921
13595
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
12922
13596
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
12923
13597
|
if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
|
|
12924
13598
|
const generation = this.#promptGeneration;
|
|
13599
|
+
const terminalTextAnswer =
|
|
13600
|
+
options.terminalTextAnswer ?? isTerminalTextAssistantAnswer(this.#findLastAssistantMessage());
|
|
12925
13601
|
const suppressContinuation = options.suppressContinuation === true;
|
|
12926
13602
|
const shouldAutoContinue =
|
|
12927
13603
|
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
@@ -12936,6 +13612,7 @@ export class AgentSession {
|
|
|
12936
13612
|
willRetry,
|
|
12937
13613
|
generation,
|
|
12938
13614
|
shouldAutoContinue,
|
|
13615
|
+
terminalTextAnswer,
|
|
12939
13616
|
options.triggerContextTokens,
|
|
12940
13617
|
suppressContinuation,
|
|
12941
13618
|
);
|
|
@@ -12958,11 +13635,17 @@ export class AgentSession {
|
|
|
12958
13635
|
async signal => {
|
|
12959
13636
|
await Promise.resolve();
|
|
12960
13637
|
if (signal.aborted) return;
|
|
12961
|
-
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13638
|
+
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13639
|
+
...options,
|
|
13640
|
+
terminalTextAnswer,
|
|
13641
|
+
});
|
|
12962
13642
|
},
|
|
12963
13643
|
{ generation },
|
|
12964
13644
|
);
|
|
12965
|
-
return
|
|
13645
|
+
return {
|
|
13646
|
+
...COMPACTION_CHECK_DEFERRED_HANDOFF,
|
|
13647
|
+
continuationScheduled: shouldAutoContinue,
|
|
13648
|
+
};
|
|
12966
13649
|
}
|
|
12967
13650
|
|
|
12968
13651
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
@@ -13029,10 +13712,14 @@ export class AgentSession {
|
|
|
13029
13712
|
aborted: false,
|
|
13030
13713
|
willRetry: false,
|
|
13031
13714
|
});
|
|
13032
|
-
const continuationScheduled =
|
|
13033
|
-
|
|
13034
|
-
this.#
|
|
13035
|
-
|
|
13715
|
+
const continuationScheduled =
|
|
13716
|
+
!autoCompactionSignal.aborted &&
|
|
13717
|
+
this.#scheduleCompactionContinuation({
|
|
13718
|
+
generation,
|
|
13719
|
+
autoContinue: reason !== "idle" && shouldAutoContinue,
|
|
13720
|
+
terminalTextAnswer,
|
|
13721
|
+
suppressContinuation,
|
|
13722
|
+
});
|
|
13036
13723
|
return {
|
|
13037
13724
|
...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
|
|
13038
13725
|
historyRewritten: true,
|
|
@@ -13521,20 +14208,13 @@ export class AgentSession {
|
|
|
13521
14208
|
if (retryFits) {
|
|
13522
14209
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13523
14210
|
continuationScheduled = true;
|
|
13524
|
-
} else
|
|
13525
|
-
this.#
|
|
13526
|
-
continuationScheduled = true;
|
|
13527
|
-
}
|
|
13528
|
-
if (!continuationScheduled && !suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
13529
|
-
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
13530
|
-
// Kick the loop so queued messages are actually delivered. This remains separate
|
|
13531
|
-
// from the no-progress warning: pausing maintenance must not strand user input.
|
|
13532
|
-
this.#scheduleAgentContinue({
|
|
13533
|
-
delayMs: 100,
|
|
14211
|
+
} else {
|
|
14212
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13534
14213
|
generation,
|
|
13535
|
-
|
|
14214
|
+
autoContinue: hasHeadroom && shouldAutoContinue,
|
|
14215
|
+
terminalTextAnswer,
|
|
14216
|
+
suppressContinuation,
|
|
13536
14217
|
});
|
|
13537
|
-
continuationScheduled = true;
|
|
13538
14218
|
}
|
|
13539
14219
|
|
|
13540
14220
|
if (deadEndWarning) {
|
|
@@ -13590,6 +14270,7 @@ export class AgentSession {
|
|
|
13590
14270
|
willRetry: boolean,
|
|
13591
14271
|
generation: number,
|
|
13592
14272
|
autoContinue: boolean,
|
|
14273
|
+
terminalTextAnswer: boolean,
|
|
13593
14274
|
triggerContextTokens?: number,
|
|
13594
14275
|
suppressContinuation = false,
|
|
13595
14276
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
@@ -13672,10 +14353,6 @@ export class AgentSession {
|
|
|
13672
14353
|
});
|
|
13673
14354
|
|
|
13674
14355
|
let continuationScheduled = false;
|
|
13675
|
-
if (!willRetry && reason !== "idle" && autoContinue) {
|
|
13676
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13677
|
-
continuationScheduled = true;
|
|
13678
|
-
}
|
|
13679
14356
|
if (willRetry) {
|
|
13680
14357
|
// The shake rebuild replays every entry, so a trailing error/length
|
|
13681
14358
|
// assistant from the failed turn re-enters agent state — drop it before
|
|
@@ -13691,13 +14368,13 @@ export class AgentSession {
|
|
|
13691
14368
|
}
|
|
13692
14369
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13693
14370
|
continuationScheduled = true;
|
|
13694
|
-
} else
|
|
13695
|
-
this.#
|
|
13696
|
-
delayMs: 100,
|
|
14371
|
+
} else {
|
|
14372
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13697
14373
|
generation,
|
|
13698
|
-
|
|
14374
|
+
autoContinue: reason !== "idle" && autoContinue,
|
|
14375
|
+
terminalTextAnswer,
|
|
14376
|
+
suppressContinuation,
|
|
13699
14377
|
});
|
|
13700
|
-
continuationScheduled = true;
|
|
13701
14378
|
}
|
|
13702
14379
|
if (!reclaimed) {
|
|
13703
14380
|
return willRetry && continuationScheduled
|
|
@@ -13851,6 +14528,11 @@ export class AgentSession {
|
|
|
13851
14528
|
return stopType === "refusal" || stopType === "sensitive";
|
|
13852
14529
|
}
|
|
13853
14530
|
|
|
14531
|
+
/** True when any registered model belongs to `provider`. */
|
|
14532
|
+
#hasProviderModels(provider: string): boolean {
|
|
14533
|
+
return this.#modelRegistry.getAll().some(model => model.provider === provider);
|
|
14534
|
+
}
|
|
14535
|
+
|
|
13854
14536
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
13855
14537
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
13856
14538
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -13881,8 +14563,8 @@ export class AgentSession {
|
|
|
13881
14563
|
const keyKind = isRetryFallbackModelKey(key) ? "model" : "role";
|
|
13882
14564
|
if (keyKind === "model") {
|
|
13883
14565
|
if (isRetryFallbackWildcardKey(key)) {
|
|
13884
|
-
const provider = key
|
|
13885
|
-
if (!this.#
|
|
14566
|
+
const { provider } = parseRetryFallbackWildcard(key, p => this.#hasProviderModels(p));
|
|
14567
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13886
14568
|
const msg = `retry.fallbackChains wildcard key references unknown provider: ${key}`;
|
|
13887
14569
|
logger.warn(msg);
|
|
13888
14570
|
this.configWarnings.push(msg);
|
|
@@ -13914,8 +14596,8 @@ export class AgentSession {
|
|
|
13914
14596
|
continue;
|
|
13915
14597
|
}
|
|
13916
14598
|
if (isRetryFallbackWildcardKey(selectorStr)) {
|
|
13917
|
-
const provider = selectorStr
|
|
13918
|
-
if (!this.#
|
|
14599
|
+
const { provider } = parseRetryFallbackWildcard(selectorStr, p => this.#hasProviderModels(p));
|
|
14600
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13919
14601
|
const msg = `Fallback chain for ${keyKind} '${key}' references unknown provider: ${selectorStr}`;
|
|
13920
14602
|
logger.warn(msg);
|
|
13921
14603
|
this.configWarnings.push(msg);
|
|
@@ -13974,13 +14656,16 @@ export class AgentSession {
|
|
|
13974
14656
|
* Model-oriented keys win over roles so a chain follows the model across
|
|
13975
14657
|
* role reassignments.
|
|
13976
14658
|
*/
|
|
13977
|
-
#resolveRetryFallbackRole(
|
|
14659
|
+
#resolveRetryFallbackRole(
|
|
14660
|
+
currentSelector: string,
|
|
14661
|
+
currentModel: Model | null | undefined = this.model,
|
|
14662
|
+
): string | undefined {
|
|
13978
14663
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13979
14664
|
if (!parsedCurrent) return undefined;
|
|
13980
14665
|
const chains = this.#getRetryFallbackChains();
|
|
13981
14666
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
13982
|
-
const currentPlainSelector =
|
|
13983
|
-
? formatModelSelectorValue(formatModelString(
|
|
14667
|
+
const currentPlainSelector = currentModel
|
|
14668
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
13984
14669
|
: undefined;
|
|
13985
14670
|
const currentPlainBaseSelector =
|
|
13986
14671
|
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -14006,9 +14691,22 @@ export class AgentSession {
|
|
|
14006
14691
|
for (const key of exactModelKeys) {
|
|
14007
14692
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
14008
14693
|
}
|
|
14009
|
-
// 2. Provider
|
|
14010
|
-
|
|
14011
|
-
|
|
14694
|
+
// 2. Provider wildcards — an id-prefixed key (`openrouter/google/*`)
|
|
14695
|
+
// beats the plain `provider/*` key for ids under its prefix.
|
|
14696
|
+
let wildcardMatch: string | undefined;
|
|
14697
|
+
let wildcardPrefixLength = -1;
|
|
14698
|
+
for (const key in chains) {
|
|
14699
|
+
if (!isRetryFallbackWildcardKey(key) || !Array.isArray(chains[key])) continue;
|
|
14700
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(key, p => this.#hasProviderModels(p));
|
|
14701
|
+
if (provider !== parsedCurrent.provider) continue;
|
|
14702
|
+
if (idPrefix !== undefined && !parsedCurrent.id.startsWith(`${idPrefix}/`)) continue;
|
|
14703
|
+
const prefixLength = idPrefix === undefined ? 0 : idPrefix.length;
|
|
14704
|
+
if (prefixLength > wildcardPrefixLength) {
|
|
14705
|
+
wildcardMatch = key;
|
|
14706
|
+
wildcardPrefixLength = prefixLength;
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14709
|
+
if (wildcardMatch) return wildcardMatch;
|
|
14012
14710
|
// 3. Role keys — matched by the role's currently-assigned model.
|
|
14013
14711
|
for (const key of roleKeys) {
|
|
14014
14712
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
@@ -14027,9 +14725,11 @@ export class AgentSession {
|
|
|
14027
14725
|
|
|
14028
14726
|
/**
|
|
14029
14727
|
* Parse one configured chain entry. A `provider/*` entry keeps the failing
|
|
14030
|
-
* model's id and swaps the provider (google-antigravity/x → google/x);
|
|
14031
|
-
*
|
|
14032
|
-
*
|
|
14728
|
+
* model's id and swaps the provider (google-antigravity/x → google/x); an
|
|
14729
|
+
* id-prefixed `provider/prefix/*` entry re-prefixes the failing model's
|
|
14730
|
+
* bare id instead (openrouter/google/* : google-antigravity/x →
|
|
14731
|
+
* openrouter/google/x). Ids the target provider lacks are skipped by the
|
|
14732
|
+
* candidate loop's registry lookup.
|
|
14033
14733
|
*/
|
|
14034
14734
|
#parseRetryFallbackChainEntry(
|
|
14035
14735
|
entry: string,
|
|
@@ -14037,8 +14737,23 @@ export class AgentSession {
|
|
|
14037
14737
|
): RetryFallbackSelector | undefined {
|
|
14038
14738
|
if (isRetryFallbackWildcardKey(entry)) {
|
|
14039
14739
|
if (!current) return undefined;
|
|
14040
|
-
const provider = entry
|
|
14041
|
-
|
|
14740
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(entry, p => this.#hasProviderModels(p));
|
|
14741
|
+
const bareId = current.id.slice(current.id.lastIndexOf("/") + 1);
|
|
14742
|
+
let id: string;
|
|
14743
|
+
if (idPrefix !== undefined) {
|
|
14744
|
+
id = `${idPrefix}/${bareId}`;
|
|
14745
|
+
} else if (
|
|
14746
|
+
bareId !== current.id &&
|
|
14747
|
+
!this.#modelRegistry.find(provider, current.id) &&
|
|
14748
|
+
this.#modelRegistry.find(provider, bareId)
|
|
14749
|
+
) {
|
|
14750
|
+
// Aggregator → direct: the failing id carries a vendor prefix the
|
|
14751
|
+
// target provider does not use (openrouter/google/x → google-vertex/x).
|
|
14752
|
+
id = bareId;
|
|
14753
|
+
} else {
|
|
14754
|
+
id = current.id;
|
|
14755
|
+
}
|
|
14756
|
+
return { raw: `${provider}/${id}`, provider, id, thinkingLevel: undefined };
|
|
14042
14757
|
}
|
|
14043
14758
|
return parseRetryFallbackSelector(entry, this.#modelRegistry);
|
|
14044
14759
|
}
|
|
@@ -14071,7 +14786,11 @@ export class AgentSession {
|
|
|
14071
14786
|
return chain;
|
|
14072
14787
|
}
|
|
14073
14788
|
|
|
14074
|
-
#findRetryFallbackCandidates(
|
|
14789
|
+
#findRetryFallbackCandidates(
|
|
14790
|
+
role: string,
|
|
14791
|
+
currentSelector: string,
|
|
14792
|
+
currentModel: Model | null | undefined = this.model,
|
|
14793
|
+
): RetryFallbackSelector[] {
|
|
14075
14794
|
let chain = this.#getRetryFallbackEffectiveChain(role, currentSelector);
|
|
14076
14795
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
14077
14796
|
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
@@ -14095,8 +14814,8 @@ export class AgentSession {
|
|
|
14095
14814
|
if (chain.length <= 1) return [];
|
|
14096
14815
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
14097
14816
|
const currentPlainSelector =
|
|
14098
|
-
|
|
14099
|
-
? formatModelSelectorValue(formatModelString(
|
|
14817
|
+
currentModel && parsedCurrent
|
|
14818
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
14100
14819
|
: undefined;
|
|
14101
14820
|
const currentPlainBaseSelector =
|
|
14102
14821
|
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -14706,17 +15425,187 @@ export class AgentSession {
|
|
|
14706
15425
|
// Bash Execution
|
|
14707
15426
|
// =========================================================================
|
|
14708
15427
|
|
|
14709
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
|
|
15428
|
+
async #saveBashOriginalArtifact(target: BashSessionTarget, originalText: string): Promise<string | undefined> {
|
|
14710
15429
|
try {
|
|
14711
|
-
|
|
15430
|
+
const destination = target.destination ?? (await target.pending);
|
|
15431
|
+
return await destination?.manager.saveArtifact(originalText, "bash-original");
|
|
14712
15432
|
} catch {
|
|
14713
15433
|
return undefined;
|
|
14714
15434
|
}
|
|
14715
15435
|
}
|
|
14716
15436
|
|
|
15437
|
+
#createBashMessage(
|
|
15438
|
+
command: string,
|
|
15439
|
+
result: BashResult,
|
|
15440
|
+
options?: { excludeFromContext?: boolean },
|
|
15441
|
+
): BashExecutionMessage {
|
|
15442
|
+
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
15443
|
+
return {
|
|
15444
|
+
role: "bashExecution",
|
|
15445
|
+
command,
|
|
15446
|
+
output: result.output,
|
|
15447
|
+
exitCode: result.exitCode,
|
|
15448
|
+
cancelled: result.cancelled,
|
|
15449
|
+
truncated: result.truncated,
|
|
15450
|
+
meta,
|
|
15451
|
+
timestamp: Date.now(),
|
|
15452
|
+
excludeFromContext: options?.excludeFromContext,
|
|
15453
|
+
};
|
|
15454
|
+
}
|
|
15455
|
+
|
|
15456
|
+
#captureBashSessionTarget(): BashSessionTarget {
|
|
15457
|
+
this.#bashSessionTarget.refs++;
|
|
15458
|
+
return this.#bashSessionTarget;
|
|
15459
|
+
}
|
|
15460
|
+
|
|
15461
|
+
async #releaseBashSessionTarget(target: BashSessionTarget): Promise<void> {
|
|
15462
|
+
if (target.refs <= 0) throw new Error("Bash session target released more than once");
|
|
15463
|
+
target.refs--;
|
|
15464
|
+
if (target.refs === 0 && target.destination?.kind === "detached") {
|
|
15465
|
+
await target.destination.manager.close();
|
|
15466
|
+
}
|
|
15467
|
+
}
|
|
15468
|
+
|
|
15469
|
+
#appendBashMessage(destination: BashAppendDestination, message: BashExecutionMessage): void {
|
|
15470
|
+
switch (destination.kind) {
|
|
15471
|
+
case "current":
|
|
15472
|
+
this.agent.appendMessage(message);
|
|
15473
|
+
destination.manager.appendMessage(message);
|
|
15474
|
+
break;
|
|
15475
|
+
case "detached":
|
|
15476
|
+
destination.manager.appendMessage(message);
|
|
15477
|
+
break;
|
|
15478
|
+
case "branch":
|
|
15479
|
+
destination.parentId = destination.manager.appendMessageToBranch(message, destination.parentId);
|
|
15480
|
+
break;
|
|
15481
|
+
}
|
|
15482
|
+
}
|
|
15483
|
+
|
|
15484
|
+
async #appendOwnedBashMessage(target: BashSessionTarget, message: BashExecutionMessage): Promise<void> {
|
|
15485
|
+
try {
|
|
15486
|
+
const destination = target.destination ?? (await target.pending);
|
|
15487
|
+
if (!destination) throw new Error("Bash session target has no append destination");
|
|
15488
|
+
this.#appendBashMessage(destination, message);
|
|
15489
|
+
} finally {
|
|
15490
|
+
await this.#releaseBashSessionTarget(target);
|
|
15491
|
+
}
|
|
15492
|
+
}
|
|
15493
|
+
|
|
15494
|
+
async #recordBashResultForTarget(
|
|
15495
|
+
target: BashSessionTarget,
|
|
15496
|
+
command: string,
|
|
15497
|
+
result: BashResult,
|
|
15498
|
+
options?: { excludeFromContext?: boolean },
|
|
15499
|
+
): Promise<void> {
|
|
15500
|
+
const message = this.#createBashMessage(command, result, options);
|
|
15501
|
+
if (this.isStreaming && target === this.#bashSessionTarget) {
|
|
15502
|
+
this.#pendingBashMessages.push({ target, message });
|
|
15503
|
+
return;
|
|
15504
|
+
}
|
|
15505
|
+
await this.#appendOwnedBashMessage(target, message);
|
|
15506
|
+
}
|
|
15507
|
+
|
|
15508
|
+
/** Run a leaf rewrite while retaining any in-flight bash on its originating branch. */
|
|
15509
|
+
#withBashBranchTransition<T>(mutate: () => T): T {
|
|
15510
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15511
|
+
let branchTransitioned = false;
|
|
15512
|
+
try {
|
|
15513
|
+
const result = mutate();
|
|
15514
|
+
this.#markBashSessionTransition(bashTransition);
|
|
15515
|
+
branchTransitioned = true;
|
|
15516
|
+
return result;
|
|
15517
|
+
} finally {
|
|
15518
|
+
this.#finishBashSessionTransition(bashTransition, branchTransitioned);
|
|
15519
|
+
}
|
|
15520
|
+
}
|
|
15521
|
+
|
|
14717
15522
|
/**
|
|
14718
|
-
*
|
|
14719
|
-
*
|
|
15523
|
+
* Snapshot the session/branch that owns any in-flight bash before a transition.
|
|
15524
|
+
* When an owner is still active, its target is detached to a clone so a failed
|
|
15525
|
+
* or intentionally dropped transition never redirects the late result.
|
|
15526
|
+
*/
|
|
15527
|
+
#beginBashSessionTransition(options?: { persistDetached?: boolean }): BashSessionTransition {
|
|
15528
|
+
const oldTarget = this.#bashSessionTarget;
|
|
15529
|
+
let detachedManager: SessionManager | undefined;
|
|
15530
|
+
let resolveOld: ((destination: BashAppendDestination) => void) | undefined;
|
|
15531
|
+
if (oldTarget.refs > 0) {
|
|
15532
|
+
detachedManager = this.sessionManager.cloneCurrentSession({ persist: options?.persistDetached });
|
|
15533
|
+
const pendingOld = Promise.withResolvers<BashAppendDestination>();
|
|
15534
|
+
oldTarget.destination = undefined;
|
|
15535
|
+
oldTarget.pending = pendingOld.promise;
|
|
15536
|
+
resolveOld = pendingOld.resolve;
|
|
15537
|
+
}
|
|
15538
|
+
|
|
15539
|
+
const pendingNew = Promise.withResolvers<BashAppendDestination>();
|
|
15540
|
+
return {
|
|
15541
|
+
oldTarget,
|
|
15542
|
+
newTarget: {
|
|
15543
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
15544
|
+
refs: 0,
|
|
15545
|
+
pending: pendingNew.promise,
|
|
15546
|
+
},
|
|
15547
|
+
oldSessionId: this.sessionManager.getSessionId(),
|
|
15548
|
+
oldSessionFile: this.sessionManager.getSessionFile(),
|
|
15549
|
+
oldLeafId: this.sessionManager.getLeafId(),
|
|
15550
|
+
detachedManager,
|
|
15551
|
+
resolveOld,
|
|
15552
|
+
resolveNew: pendingNew.resolve,
|
|
15553
|
+
};
|
|
15554
|
+
}
|
|
15555
|
+
|
|
15556
|
+
/** Adopt the transition's new target as the live bash owner. */
|
|
15557
|
+
#markBashSessionTransition(transition: BashSessionTransition): void {
|
|
15558
|
+
transition.newTarget.sessionId = this.sessionManager.getSessionId();
|
|
15559
|
+
this.#bashSessionTarget = transition.newTarget;
|
|
15560
|
+
}
|
|
15561
|
+
|
|
15562
|
+
/**
|
|
15563
|
+
* Resolve the pending append destinations opened by {@link #beginBashSessionTransition}.
|
|
15564
|
+
* On success the old owner keeps its original session/branch (same file → current or
|
|
15565
|
+
* branch destination; different file → detached clone); on failure both fall back to
|
|
15566
|
+
* the still-current manager and the clone is discarded.
|
|
15567
|
+
*/
|
|
15568
|
+
#finishBashSessionTransition(transition: BashSessionTransition, success: boolean): void {
|
|
15569
|
+
const currentDestination: BashAppendDestination = { kind: "current", manager: this.sessionManager };
|
|
15570
|
+
let oldDestination: BashAppendDestination = currentDestination;
|
|
15571
|
+
if (success && transition.resolveOld) {
|
|
15572
|
+
const currentFile = this.sessionManager.getSessionFile();
|
|
15573
|
+
const sameFile =
|
|
15574
|
+
transition.oldSessionFile === currentFile ||
|
|
15575
|
+
(transition.oldSessionFile !== undefined &&
|
|
15576
|
+
currentFile !== undefined &&
|
|
15577
|
+
path.resolve(transition.oldSessionFile) === path.resolve(currentFile));
|
|
15578
|
+
const sameSession = transition.oldSessionId === this.sessionManager.getSessionId() && sameFile;
|
|
15579
|
+
if (sameSession) {
|
|
15580
|
+
oldDestination =
|
|
15581
|
+
transition.oldLeafId === this.sessionManager.getLeafId()
|
|
15582
|
+
? currentDestination
|
|
15583
|
+
: { kind: "branch", manager: this.sessionManager, parentId: transition.oldLeafId };
|
|
15584
|
+
} else if (transition.detachedManager) {
|
|
15585
|
+
oldDestination = { kind: "detached", manager: transition.detachedManager };
|
|
15586
|
+
}
|
|
15587
|
+
}
|
|
15588
|
+
|
|
15589
|
+
if (transition.resolveOld) {
|
|
15590
|
+
transition.oldTarget.pending = undefined;
|
|
15591
|
+
transition.oldTarget.destination = oldDestination;
|
|
15592
|
+
transition.resolveOld(oldDestination);
|
|
15593
|
+
}
|
|
15594
|
+
|
|
15595
|
+
transition.newTarget.pending = undefined;
|
|
15596
|
+
transition.newTarget.destination = currentDestination;
|
|
15597
|
+
if (!success) transition.newTarget.sessionId = this.sessionManager.getSessionId();
|
|
15598
|
+
transition.resolveNew(currentDestination);
|
|
15599
|
+
|
|
15600
|
+
if (transition.detachedManager && (oldDestination.kind !== "detached" || transition.oldTarget.refs === 0)) {
|
|
15601
|
+
void transition.detachedManager.close().catch(error => {
|
|
15602
|
+
logger.warn("Failed to close detached bash session writer", { error: String(error) });
|
|
15603
|
+
});
|
|
15604
|
+
}
|
|
15605
|
+
}
|
|
15606
|
+
|
|
15607
|
+
/**
|
|
15608
|
+
* Execute a bash command and retain the session/branch that owned its start.
|
|
14720
15609
|
* @param command The bash command to execute
|
|
14721
15610
|
* @param onChunk Optional streaming callback for output
|
|
14722
15611
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
@@ -14727,72 +15616,72 @@ export class AgentSession {
|
|
|
14727
15616
|
onChunk?: (chunk: string) => void,
|
|
14728
15617
|
options?: { excludeFromContext?: boolean; useUserShell?: boolean },
|
|
14729
15618
|
): Promise<BashResult> {
|
|
15619
|
+
const target = this.#captureBashSessionTarget();
|
|
15620
|
+
let targetTransferred = false;
|
|
14730
15621
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
14731
15622
|
const cwd = this.sessionManager.getCwd();
|
|
14732
15623
|
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
15624
|
+
try {
|
|
15625
|
+
if (this.#extensionRunner?.hasHandlers("user_bash")) {
|
|
15626
|
+
const hookResult = await this.#extensionRunner.emitUserBash({
|
|
15627
|
+
type: "user_bash",
|
|
15628
|
+
command,
|
|
15629
|
+
excludeFromContext,
|
|
15630
|
+
cwd,
|
|
15631
|
+
});
|
|
15632
|
+
if (hookResult?.result) {
|
|
15633
|
+
targetTransferred = true;
|
|
15634
|
+
await this.#recordBashResultForTarget(target, command, hookResult.result, options);
|
|
15635
|
+
return hookResult.result;
|
|
15636
|
+
}
|
|
14743
15637
|
}
|
|
14744
|
-
}
|
|
14745
15638
|
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
15639
|
+
const abortController = new AbortController();
|
|
15640
|
+
this.#bashAbortControllers.add(abortController);
|
|
15641
|
+
let result: BashResult;
|
|
15642
|
+
try {
|
|
15643
|
+
result = await executeBashCommand(command, {
|
|
15644
|
+
onChunk,
|
|
15645
|
+
signal: abortController.signal,
|
|
15646
|
+
sessionKey: target.sessionId,
|
|
15647
|
+
cwd,
|
|
15648
|
+
timeout: clampTimeout("bash") * 1000,
|
|
15649
|
+
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(target, originalText),
|
|
15650
|
+
useUserShell: options?.useUserShell,
|
|
15651
|
+
});
|
|
15652
|
+
} finally {
|
|
15653
|
+
this.#bashAbortControllers.delete(abortController);
|
|
15654
|
+
}
|
|
14759
15655
|
|
|
14760
|
-
|
|
15656
|
+
targetTransferred = true;
|
|
15657
|
+
await this.#recordBashResultForTarget(target, command, result, options);
|
|
14761
15658
|
return result;
|
|
14762
15659
|
} finally {
|
|
14763
|
-
this.#
|
|
15660
|
+
if (!targetTransferred) await this.#releaseBashSessionTarget(target);
|
|
14764
15661
|
}
|
|
14765
15662
|
}
|
|
14766
15663
|
|
|
14767
|
-
/**
|
|
14768
|
-
* Record a bash execution result in session history.
|
|
14769
|
-
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
14770
|
-
*/
|
|
15664
|
+
/** Record a bash result supplied outside executeBash in the current ownership scope. */
|
|
14771
15665
|
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {
|
|
14772
|
-
const
|
|
14773
|
-
const
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
cancelled: result.cancelled,
|
|
14779
|
-
truncated: result.truncated,
|
|
14780
|
-
meta,
|
|
14781
|
-
timestamp: Date.now(),
|
|
14782
|
-
excludeFromContext: options?.excludeFromContext,
|
|
14783
|
-
};
|
|
14784
|
-
|
|
14785
|
-
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
14786
|
-
if (this.isStreaming) {
|
|
14787
|
-
// Queue for later - will be flushed on agent_end
|
|
14788
|
-
this.#pendingBashMessages.push(bashMessage);
|
|
14789
|
-
} else {
|
|
14790
|
-
// Add to agent state immediately
|
|
14791
|
-
this.agent.appendMessage(bashMessage);
|
|
15666
|
+
const target = this.#captureBashSessionTarget();
|
|
15667
|
+
const message = this.#createBashMessage(command, result, options);
|
|
15668
|
+
if (this.isStreaming && target === this.#bashSessionTarget) {
|
|
15669
|
+
this.#pendingBashMessages.push({ target, message });
|
|
15670
|
+
return;
|
|
15671
|
+
}
|
|
14792
15672
|
|
|
14793
|
-
|
|
14794
|
-
|
|
15673
|
+
if (target.destination) {
|
|
15674
|
+
try {
|
|
15675
|
+
this.#appendBashMessage(target.destination, message);
|
|
15676
|
+
} finally {
|
|
15677
|
+
void this.#releaseBashSessionTarget(target);
|
|
15678
|
+
}
|
|
15679
|
+
return;
|
|
14795
15680
|
}
|
|
15681
|
+
|
|
15682
|
+
void this.#appendOwnedBashMessage(target, message).catch(error => {
|
|
15683
|
+
logger.error("Failed to record bash result in its owning session", { error: String(error) });
|
|
15684
|
+
});
|
|
14796
15685
|
}
|
|
14797
15686
|
|
|
14798
15687
|
/**
|
|
@@ -14814,22 +15703,14 @@ export class AgentSession {
|
|
|
14814
15703
|
return this.#pendingBashMessages.length > 0;
|
|
14815
15704
|
}
|
|
14816
15705
|
|
|
14817
|
-
/**
|
|
14818
|
-
|
|
14819
|
-
* Called after agent turn completes to maintain proper message ordering.
|
|
14820
|
-
*/
|
|
14821
|
-
#flushPendingBashMessages(): void {
|
|
15706
|
+
/** Flush pending bash messages after the active turn without changing their ownership. */
|
|
15707
|
+
async #flushPendingBashMessages(): Promise<void> {
|
|
14822
15708
|
if (this.#pendingBashMessages.length === 0) return;
|
|
14823
|
-
|
|
14824
|
-
for (const bashMessage of this.#pendingBashMessages) {
|
|
14825
|
-
// Add to agent state
|
|
14826
|
-
this.agent.appendMessage(bashMessage);
|
|
14827
|
-
|
|
14828
|
-
// Save to session
|
|
14829
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
14830
|
-
}
|
|
14831
|
-
|
|
15709
|
+
const pending = this.#pendingBashMessages;
|
|
14832
15710
|
this.#pendingBashMessages = [];
|
|
15711
|
+
for (const { target, message } of pending) {
|
|
15712
|
+
await this.#appendOwnedBashMessage(target, message);
|
|
15713
|
+
}
|
|
14833
15714
|
}
|
|
14834
15715
|
|
|
14835
15716
|
// =========================================================================
|
|
@@ -15422,9 +16303,11 @@ export class AgentSession {
|
|
|
15422
16303
|
this.#disconnectFromAgent();
|
|
15423
16304
|
await this.abort({ goalReason: "internal" });
|
|
15424
16305
|
|
|
16306
|
+
await this.#flushPendingBashMessages();
|
|
15425
16307
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
15426
16308
|
await this.sessionManager.flush();
|
|
15427
16309
|
const previousSessionState = this.sessionManager.captureState();
|
|
16310
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15428
16311
|
// Only same-session reloads compare against the prior context to detect
|
|
15429
16312
|
// rollback edits (`#didSessionMessagesChange` below). Building it for a
|
|
15430
16313
|
// different-session switch is a pure waste — and on huge pre-fix sessions
|
|
@@ -15469,6 +16352,7 @@ export class AgentSession {
|
|
|
15469
16352
|
|
|
15470
16353
|
try {
|
|
15471
16354
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
16355
|
+
this.#markBashSessionTransition(bashTransition);
|
|
15472
16356
|
if (switchingToDifferentSession) {
|
|
15473
16357
|
this.#freshProviderSessionId = undefined;
|
|
15474
16358
|
this.#clearInheritedProviderPromptCacheKey();
|
|
@@ -15602,6 +16486,7 @@ export class AgentSession {
|
|
|
15602
16486
|
error: String(error),
|
|
15603
16487
|
});
|
|
15604
16488
|
}
|
|
16489
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
15605
16490
|
return true;
|
|
15606
16491
|
} catch (error) {
|
|
15607
16492
|
this.sessionManager.restoreState(previousSessionState);
|
|
@@ -15633,6 +16518,7 @@ export class AgentSession {
|
|
|
15633
16518
|
this.#syncTodoPhasesFromBranch();
|
|
15634
16519
|
this.#resetAllAdvisorRuntimes();
|
|
15635
16520
|
this.#reconnectToAgent();
|
|
16521
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
15636
16522
|
throw error;
|
|
15637
16523
|
}
|
|
15638
16524
|
}
|
|
@@ -15678,14 +16564,25 @@ export class AgentSession {
|
|
|
15678
16564
|
this.#pendingNextTurnMessages = [];
|
|
15679
16565
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
15680
16566
|
|
|
16567
|
+
await this.#flushPendingBashMessages();
|
|
15681
16568
|
// Flush pending writes before branching
|
|
15682
16569
|
await this.sessionManager.flush();
|
|
16570
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15683
16571
|
this.#cancelOwnAsyncJobs();
|
|
16572
|
+
this.#abortAutolearnCapture();
|
|
16573
|
+
await this.#drainAutolearnCapture();
|
|
15684
16574
|
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15688
|
-
|
|
16575
|
+
let sessionTransitioned = false;
|
|
16576
|
+
try {
|
|
16577
|
+
if (!selectedEntry.parentId) {
|
|
16578
|
+
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
16579
|
+
} else {
|
|
16580
|
+
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
16581
|
+
}
|
|
16582
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16583
|
+
sessionTransitioned = true;
|
|
16584
|
+
} finally {
|
|
16585
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
15689
16586
|
}
|
|
15690
16587
|
this.#rehydrateCheckpointRewindState();
|
|
15691
16588
|
this.#syncTodoPhasesFromBranch();
|
|
@@ -15769,10 +16666,21 @@ export class AgentSession {
|
|
|
15769
16666
|
await this.abort({ goalReason: "internal", reason: "branching /btw" });
|
|
15770
16667
|
this.agent.replaceQueues([], []);
|
|
15771
16668
|
}
|
|
16669
|
+
await this.#flushPendingBashMessages();
|
|
15772
16670
|
await this.sessionManager.flush();
|
|
16671
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15773
16672
|
this.#cancelOwnAsyncJobs();
|
|
16673
|
+
this.#abortAutolearnCapture();
|
|
16674
|
+
await this.#drainAutolearnCapture();
|
|
15774
16675
|
|
|
15775
|
-
|
|
16676
|
+
let sessionTransitioned = false;
|
|
16677
|
+
try {
|
|
16678
|
+
this.sessionManager.createBranchedSession(leafId);
|
|
16679
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16680
|
+
sessionTransitioned = true;
|
|
16681
|
+
} finally {
|
|
16682
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
16683
|
+
}
|
|
15776
16684
|
|
|
15777
16685
|
this.#rehydrateCheckpointRewindState();
|
|
15778
16686
|
this.sessionManager.appendMessage({
|
|
@@ -15828,6 +16736,7 @@ export class AgentSession {
|
|
|
15828
16736
|
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
15829
16737
|
sessionContext?: SessionContext;
|
|
15830
16738
|
}> {
|
|
16739
|
+
await this.#flushPendingBashMessages();
|
|
15831
16740
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
15832
16741
|
|
|
15833
16742
|
// No-op if already at target
|
|
@@ -15955,18 +16864,28 @@ export class AgentSession {
|
|
|
15955
16864
|
|
|
15956
16865
|
// Switch leaf (with or without summary)
|
|
15957
16866
|
// Summary is attached at the navigation target position (newLeafId), not the old branch
|
|
16867
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15958
16868
|
let summaryEntry: BranchSummaryEntry | undefined;
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
16869
|
+
let branchTransitioned = false;
|
|
16870
|
+
try {
|
|
16871
|
+
if (summaryText) {
|
|
16872
|
+
// Create summary at target position (can be null for root)
|
|
16873
|
+
const summaryId = this.sessionManager.branchWithSummary(
|
|
16874
|
+
newLeafId,
|
|
16875
|
+
summaryText,
|
|
16876
|
+
summaryDetails,
|
|
16877
|
+
fromExtension,
|
|
16878
|
+
);
|
|
16879
|
+
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
|
|
16880
|
+
} else if (newLeafId === null) {
|
|
16881
|
+
this.sessionManager.resetLeaf();
|
|
16882
|
+
} else {
|
|
16883
|
+
this.sessionManager.branch(newLeafId);
|
|
16884
|
+
}
|
|
16885
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16886
|
+
branchTransitioned = true;
|
|
16887
|
+
} finally {
|
|
16888
|
+
this.#finishBashSessionTransition(bashTransition, branchTransitioned);
|
|
15970
16889
|
}
|
|
15971
16890
|
|
|
15972
16891
|
// Update agent state — build display context to populate agent messages.
|
|
@@ -16709,29 +17628,73 @@ export class AgentSession {
|
|
|
16709
17628
|
return this.#advisors[0]?.agent;
|
|
16710
17629
|
}
|
|
16711
17630
|
|
|
17631
|
+
/**
|
|
17632
|
+
* Lightweight advisor status for the status line: returns just the configured
|
|
17633
|
+
* flag and per-advisor name/status without computing token/cost breakdowns.
|
|
17634
|
+
* Avoids re-tokenizing the advisor transcript on every render frame.
|
|
17635
|
+
*/
|
|
17636
|
+
getAdvisorStatusOverview(): { configured: boolean; advisors: { name: string; status: AdvisorRuntimeStatus }[] } {
|
|
17637
|
+
// Override stale map entries with live runtime status: failureNotified/quotaExhausted
|
|
17638
|
+
// clear on reset() but #advisorStatuses lags until the next build.
|
|
17639
|
+
const liveStatusBySlug = new Map<string, AdvisorRuntimeStatus>();
|
|
17640
|
+
for (const a of this.#advisors) {
|
|
17641
|
+
liveStatusBySlug.set(
|
|
17642
|
+
a.slug,
|
|
17643
|
+
a.runtime.quotaExhausted ? "quota_exhausted" : a.runtime.failureNotified ? "error" : "running",
|
|
17644
|
+
);
|
|
17645
|
+
}
|
|
17646
|
+
const advisors = [...this.#advisorStatuses.entries()].map(([slug, { name, status }]) => ({
|
|
17647
|
+
name,
|
|
17648
|
+
status: liveStatusBySlug.get(slug) ?? status,
|
|
17649
|
+
}));
|
|
17650
|
+
return { configured: this.#advisorEnabled, advisors };
|
|
17651
|
+
}
|
|
16712
17652
|
/**
|
|
16713
17653
|
* Return structured advisor stats for the status command and TUI panel.
|
|
16714
17654
|
*/
|
|
16715
17655
|
getAdvisorStats(): AdvisorStats {
|
|
16716
17656
|
const configured = this.#advisorEnabled;
|
|
16717
|
-
const
|
|
16718
|
-
|
|
17657
|
+
const liveAdvisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
17658
|
+
// Build the complete roster from #advisorStatuses, which already has the
|
|
17659
|
+
// correct de-duped slugs as keys. Live advisors (from #advisors) carry full
|
|
17660
|
+
// token/cost data; disabled/no-model/quota-exhausted advisors appear as
|
|
17661
|
+
// skeleton entries with just name + status so the status line renders a dot.
|
|
17662
|
+
const liveStatBySlug = new Map(this.#advisors.map((a, i) => [a.slug, liveAdvisors[i]]));
|
|
17663
|
+
const roster: PerAdvisorStat[] = [];
|
|
17664
|
+
for (const [slug, entry] of this.#advisorStatuses) {
|
|
17665
|
+
const live = liveStatBySlug.get(slug);
|
|
17666
|
+
if (live) {
|
|
17667
|
+
roster.push(live);
|
|
17668
|
+
} else {
|
|
17669
|
+
roster.push({
|
|
17670
|
+
name: entry.name,
|
|
17671
|
+
status: entry.status,
|
|
17672
|
+
contextWindow: 0,
|
|
17673
|
+
contextTokens: 0,
|
|
17674
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
17675
|
+
cost: 0,
|
|
17676
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
17677
|
+
});
|
|
17678
|
+
}
|
|
17679
|
+
}
|
|
17680
|
+
const active = liveAdvisors.length > 0;
|
|
17681
|
+
if (liveAdvisors.length === 0) {
|
|
16719
17682
|
return {
|
|
16720
17683
|
configured,
|
|
16721
|
-
active
|
|
17684
|
+
active,
|
|
16722
17685
|
contextWindow: 0,
|
|
16723
17686
|
contextTokens: 0,
|
|
16724
17687
|
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
16725
17688
|
cost: 0,
|
|
16726
17689
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
16727
|
-
advisors:
|
|
17690
|
+
advisors: roster,
|
|
16728
17691
|
};
|
|
16729
17692
|
}
|
|
16730
17693
|
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
16731
17694
|
const messages = { user: 0, assistant: 0, total: 0 };
|
|
16732
17695
|
let cost = 0;
|
|
16733
17696
|
let contextTokens = 0;
|
|
16734
|
-
for (const a of
|
|
17697
|
+
for (const a of liveAdvisors) {
|
|
16735
17698
|
tokens.input += a.tokens.input;
|
|
16736
17699
|
tokens.output += a.tokens.output;
|
|
16737
17700
|
tokens.reasoning += a.tokens.reasoning;
|
|
@@ -16748,14 +17711,14 @@ export class AgentSession {
|
|
|
16748
17711
|
// first advisor's so the legacy status line stays byte-identical.
|
|
16749
17712
|
return {
|
|
16750
17713
|
configured,
|
|
16751
|
-
active
|
|
16752
|
-
model:
|
|
16753
|
-
contextWindow:
|
|
17714
|
+
active,
|
|
17715
|
+
model: liveAdvisors[0].model,
|
|
17716
|
+
contextWindow: liveAdvisors[0].contextWindow,
|
|
16754
17717
|
contextTokens,
|
|
16755
17718
|
tokens,
|
|
16756
17719
|
cost,
|
|
16757
17720
|
messages,
|
|
16758
|
-
advisors,
|
|
17721
|
+
advisors: roster,
|
|
16759
17722
|
};
|
|
16760
17723
|
}
|
|
16761
17724
|
|
|
@@ -16789,12 +17752,18 @@ export class AgentSession {
|
|
|
16789
17752
|
}
|
|
16790
17753
|
return {
|
|
16791
17754
|
name: advisor.name,
|
|
17755
|
+
status: advisor.runtime.quotaExhausted
|
|
17756
|
+
? "quota_exhausted"
|
|
17757
|
+
: advisor.runtime.failureNotified
|
|
17758
|
+
? "error"
|
|
17759
|
+
: "running",
|
|
16792
17760
|
model,
|
|
16793
17761
|
contextWindow: model.contextWindow ?? 0,
|
|
16794
17762
|
contextTokens,
|
|
16795
17763
|
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
|
|
16796
17764
|
cost,
|
|
16797
17765
|
messages: { user, assistant, total: messages.length },
|
|
17766
|
+
sessionId: advisor.agent.sessionId,
|
|
16798
17767
|
};
|
|
16799
17768
|
}
|
|
16800
17769
|
|
|
@@ -16803,13 +17772,18 @@ export class AgentSession {
|
|
|
16803
17772
|
*/
|
|
16804
17773
|
formatAdvisorStatus(): string {
|
|
16805
17774
|
const stats = this.getAdvisorStats();
|
|
16806
|
-
if (!stats.active) {
|
|
17775
|
+
if (!stats.active && stats.advisors.length === 0) {
|
|
16807
17776
|
return stats.configured
|
|
16808
17777
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
16809
17778
|
: "Advisor is disabled.";
|
|
16810
17779
|
}
|
|
16811
17780
|
if (stats.advisors.length <= 1) {
|
|
16812
17781
|
const s = stats.advisors[0];
|
|
17782
|
+
if (s && s.status === "no_model") {
|
|
17783
|
+
return stats.configured
|
|
17784
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
17785
|
+
: "Advisor is disabled.";
|
|
17786
|
+
}
|
|
16813
17787
|
const contextLine =
|
|
16814
17788
|
s.contextWindow > 0
|
|
16815
17789
|
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
@@ -16818,6 +17792,7 @@ export class AgentSession {
|
|
|
16818
17792
|
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
16819
17793
|
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
16820
17794
|
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
17795
|
+
if (!s.model || s.status !== "running") return `Advisor "${s.name}" is ${s.status.replace("_", " ")}.`;
|
|
16821
17796
|
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
16822
17797
|
}
|
|
16823
17798
|
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
@@ -16826,7 +17801,9 @@ export class AgentSession {
|
|
|
16826
17801
|
s.contextWindow > 0
|
|
16827
17802
|
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
16828
17803
|
: `${s.contextTokens.toLocaleString()}`;
|
|
16829
|
-
lines.push(
|
|
17804
|
+
lines.push(
|
|
17805
|
+
` • ${s.name}${s.model && s.status === "running" ? ` (${s.model.provider}/${s.model.id})` : ` [${s.status}]`} — context ${ctx} tokens, $${s.cost.toFixed(4)}`,
|
|
17806
|
+
);
|
|
16830
17807
|
}
|
|
16831
17808
|
lines.push(
|
|
16832
17809
|
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
@@ -16835,37 +17812,51 @@ export class AgentSession {
|
|
|
16835
17812
|
}
|
|
16836
17813
|
|
|
16837
17814
|
/**
|
|
16838
|
-
* Estimate the advisor's current context tokens.
|
|
16839
|
-
*
|
|
16840
|
-
*
|
|
16841
|
-
*
|
|
17815
|
+
* Estimate the advisor's current context tokens. A successful provider usage
|
|
17816
|
+
* after the latest advisor compaction is ground truth for the prompt plus its
|
|
17817
|
+
* generated output; only messages after that anchor are estimated. Usage from
|
|
17818
|
+
* retained pre-compaction messages is stale and must not immediately retrigger
|
|
17819
|
+
* maintenance on the newly compacted context.
|
|
16842
17820
|
*/
|
|
16843
17821
|
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
16844
|
-
let
|
|
16845
|
-
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
17822
|
+
let usageAnchorStartIndex = 0;
|
|
16846
17823
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16847
|
-
const
|
|
16848
|
-
if (
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
17824
|
+
const message = messages[i];
|
|
17825
|
+
if (message.role !== "compactionSummary") continue;
|
|
17826
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
17827
|
+
// Advisor summaries created before this runtime-only boundary existed have
|
|
17828
|
+
// no trustworthy way to distinguish retained from newly appended messages.
|
|
17829
|
+
// Conservatively ignore every current assistant until the next compaction.
|
|
17830
|
+
usageAnchorStartIndex = advisorSummary.advisorUsageAnchorStartIndex ?? messages.length;
|
|
17831
|
+
break;
|
|
17832
|
+
}
|
|
17833
|
+
|
|
17834
|
+
let lastUsageIndex: number | undefined;
|
|
17835
|
+
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
17836
|
+
for (let i = messages.length - 1; i >= usageAnchorStartIndex; i--) {
|
|
17837
|
+
const message = messages[i];
|
|
17838
|
+
if (message.role !== "assistant") continue;
|
|
17839
|
+
const assistant = message as AssistantMessage;
|
|
17840
|
+
if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error" && assistant.usage) {
|
|
17841
|
+
lastUsage = assistant.usage;
|
|
17842
|
+
lastUsageIndex = i;
|
|
17843
|
+
break;
|
|
16855
17844
|
}
|
|
16856
17845
|
}
|
|
16857
|
-
|
|
17846
|
+
|
|
17847
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
17848
|
+
if (!lastUsage || lastUsageIndex === undefined) {
|
|
16858
17849
|
let estimated = 0;
|
|
16859
17850
|
for (const message of messages) {
|
|
16860
|
-
estimated += estimateTokens(message);
|
|
17851
|
+
estimated += estimateTokens(message, estimateOptions);
|
|
16861
17852
|
}
|
|
16862
17853
|
return estimated;
|
|
16863
17854
|
}
|
|
16864
17855
|
let trailingTokens = 0;
|
|
16865
17856
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
16866
|
-
trailingTokens += estimateTokens(messages[i]);
|
|
17857
|
+
trailingTokens += estimateTokens(messages[i], estimateOptions);
|
|
16867
17858
|
}
|
|
16868
|
-
return
|
|
17859
|
+
return calculateContextTokens(lastUsage) + trailingTokens;
|
|
16869
17860
|
}
|
|
16870
17861
|
|
|
16871
17862
|
/**
|