@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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 +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- 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/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- 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/discovery/substitute-plugin-root.d.ts +20 -7
- 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 +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -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/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- 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/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- 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/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- 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 +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- 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 +96 -1
- 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/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- 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/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -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 +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- 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/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- 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,
|
|
@@ -170,6 +171,7 @@ import {
|
|
|
170
171
|
} from "../advisor";
|
|
171
172
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
172
173
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
174
|
+
import { reset as resetCapabilities } from "../capability";
|
|
173
175
|
import type { Rule } from "../capability/rule";
|
|
174
176
|
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
175
177
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -196,6 +198,7 @@ import {
|
|
|
196
198
|
onModelRolesChanged,
|
|
197
199
|
validateProviderMaxInFlightRequests,
|
|
198
200
|
} from "../config/settings";
|
|
201
|
+
import { CursorExecHandlers } from "../cursor";
|
|
199
202
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
200
203
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
201
204
|
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
@@ -232,12 +235,13 @@ import type {
|
|
|
232
235
|
TurnEndEvent,
|
|
233
236
|
TurnStartEvent,
|
|
234
237
|
} from "../extensibility/extensions";
|
|
238
|
+
import { ManagedTimers } from "../extensibility/extensions/managed-timers";
|
|
235
239
|
import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
|
|
236
240
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
237
241
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
238
242
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
239
243
|
import type { RecoveredRetryError } from "../extensibility/shared-events";
|
|
240
|
-
import type
|
|
244
|
+
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
241
245
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
242
246
|
import { GoalRuntime } from "../goals/runtime";
|
|
243
247
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
@@ -251,7 +255,11 @@ import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
|
251
255
|
import { theme } from "../modes/theme/theme";
|
|
252
256
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
253
257
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
254
|
-
import {
|
|
258
|
+
import {
|
|
259
|
+
computeNonMessageBreakdown,
|
|
260
|
+
computeNonMessageTokens,
|
|
261
|
+
estimateToolSchemaTokens,
|
|
262
|
+
} from "../modes/utils/context-usage";
|
|
255
263
|
import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
|
|
256
264
|
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
257
265
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
@@ -314,7 +322,7 @@ import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
|
314
322
|
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
315
323
|
import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
|
|
316
324
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
317
|
-
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
325
|
+
import { isInternalUrlPath, normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
318
326
|
import {
|
|
319
327
|
buildResolveReminderMessage,
|
|
320
328
|
isPreviewResolutionToolCall,
|
|
@@ -392,6 +400,34 @@ import { YieldQueue } from "./yield-queue";
|
|
|
392
400
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
393
401
|
const PLAN_MODE_REMINDER_MAX = 3;
|
|
394
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
|
+
|
|
395
431
|
/**
|
|
396
432
|
* Mutating tool results (`bash`/`eval`/`edit`/`write`/`ast_edit`) without the
|
|
397
433
|
* agent touching the `todo` tool that trip the mid-run reconciliation nudge.
|
|
@@ -530,6 +566,43 @@ function reportFromRewindReportContent(content: string): string {
|
|
|
530
566
|
return report.trim();
|
|
531
567
|
}
|
|
532
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
|
+
|
|
533
606
|
function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | undefined {
|
|
534
607
|
if (entry.type !== "custom_message" || entry.customType !== "rewind-report") return undefined;
|
|
535
608
|
const details = entry.details;
|
|
@@ -542,21 +615,18 @@ function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | u
|
|
|
542
615
|
reportFromRewindReportContent(customMessageContentText(entry.content));
|
|
543
616
|
return report.length > 0 ? { report, startedAt, rewoundAt } : undefined;
|
|
544
617
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
entry.message.toolName === "checkpoint" &&
|
|
553
|
-
entry.message.isError !== true
|
|
554
|
-
);
|
|
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";
|
|
555
625
|
}
|
|
556
626
|
|
|
557
627
|
function checkpointStartedAtFromEntry(entry: SessionEntry): string | undefined {
|
|
558
628
|
if (!isSuccessfulCheckpointEntry(entry)) return undefined;
|
|
559
|
-
const details = entry.message.details;
|
|
629
|
+
const details = semanticToolResult(entry.message.toolName, entry.message)?.details;
|
|
560
630
|
if (details && typeof details === "object") {
|
|
561
631
|
const startedAt = stringProperty(details, "startedAt");
|
|
562
632
|
if (startedAt) return startedAt;
|
|
@@ -670,7 +740,7 @@ const COMPACTION_CHECK_NONE: CompactionCheckResult = {
|
|
|
670
740
|
};
|
|
671
741
|
const COMPACTION_CHECK_DEFERRED_HANDOFF: CompactionCheckResult = {
|
|
672
742
|
deferredHandoff: true,
|
|
673
|
-
continuationScheduled:
|
|
743
|
+
continuationScheduled: false,
|
|
674
744
|
};
|
|
675
745
|
const COMPACTION_CHECK_CONTINUATION: CompactionCheckResult = {
|
|
676
746
|
deferredHandoff: false,
|
|
@@ -827,6 +897,8 @@ export interface AgentSessionConfig {
|
|
|
827
897
|
skills?: Skill[];
|
|
828
898
|
/** Skill loading warnings (already captured by SDK) */
|
|
829
899
|
skillWarnings?: SkillWarning[];
|
|
900
|
+
/** Whether runtime reloads may rediscover disk-backed skills for this session. */
|
|
901
|
+
skillsReloadable?: boolean;
|
|
830
902
|
/** Custom commands (TypeScript slash commands) */
|
|
831
903
|
customCommands?: LoadedCustomCommand[];
|
|
832
904
|
skillsSettings?: SkillsSettings;
|
|
@@ -840,6 +912,8 @@ export interface AgentSessionConfig {
|
|
|
840
912
|
builtInToolNames?: Iterable<string>;
|
|
841
913
|
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
842
914
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
915
|
+
/** Register the write transport lazily when runtime xdev mounts first need it. */
|
|
916
|
+
ensureWriteRegistered?: () => Promise<boolean>;
|
|
843
917
|
/** Current session pre-LLM message transform pipeline */
|
|
844
918
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
845
919
|
/**
|
|
@@ -886,7 +960,8 @@ export interface AgentSessionConfig {
|
|
|
886
960
|
xdevRegistry?: XdevRegistry;
|
|
887
961
|
/** Discoverable tools mounted under `xd://` in the initial enabled set (startup partition in `sdk.ts`). */
|
|
888
962
|
initialMountedXdevToolNames?: string[];
|
|
889
|
-
|
|
963
|
+
/** Explicit/effective names pinned top-level during runtime repartitioning. */
|
|
964
|
+
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
890
965
|
/**
|
|
891
966
|
* Optional accessor for live MCP server instructions. Read by the session's
|
|
892
967
|
* `rebuildSystemPrompt`-skip optimization to detect server-side instruction
|
|
@@ -1114,15 +1189,20 @@ export interface AdvisorStats {
|
|
|
1114
1189
|
advisors: PerAdvisorStat[];
|
|
1115
1190
|
}
|
|
1116
1191
|
|
|
1117
|
-
/** 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. */
|
|
1118
1196
|
export interface PerAdvisorStat {
|
|
1119
1197
|
name: string;
|
|
1120
|
-
|
|
1198
|
+
status: AdvisorRuntimeStatus;
|
|
1199
|
+
model?: Model;
|
|
1121
1200
|
contextWindow: number;
|
|
1122
1201
|
contextTokens: number;
|
|
1123
1202
|
tokens: AdvisorStats["tokens"];
|
|
1124
1203
|
cost: number;
|
|
1125
1204
|
messages: AdvisorStats["messages"];
|
|
1205
|
+
sessionId?: string;
|
|
1126
1206
|
}
|
|
1127
1207
|
|
|
1128
1208
|
/**
|
|
@@ -1131,6 +1211,13 @@ export interface PerAdvisorStat {
|
|
|
1131
1211
|
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
1132
1212
|
* channel) stays on the session.
|
|
1133
1213
|
*/
|
|
1214
|
+
interface AdvisorRetryFallbackState {
|
|
1215
|
+
role: string;
|
|
1216
|
+
originalSelector: string;
|
|
1217
|
+
originalThinkingLevel: ThinkingLevel;
|
|
1218
|
+
lastAppliedThinkingLevel: ThinkingLevel;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1134
1221
|
interface ActiveAdvisor {
|
|
1135
1222
|
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
1136
1223
|
name: string;
|
|
@@ -1147,10 +1234,23 @@ interface ActiveAdvisor {
|
|
|
1147
1234
|
agentUnsubscribe?: () => void;
|
|
1148
1235
|
model: Model;
|
|
1149
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;
|
|
1150
1243
|
/** Stable key for the resolved runtime inputs that require a rebuild to change. */
|
|
1151
1244
|
signature: string;
|
|
1152
1245
|
}
|
|
1153
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
|
+
|
|
1154
1254
|
/** Resolved advisor config ready to instantiate as an {@link ActiveAdvisor}. */
|
|
1155
1255
|
interface AdvisorRuntimeDescriptor {
|
|
1156
1256
|
config: AdvisorConfig;
|
|
@@ -1226,13 +1326,31 @@ function isRetryFallbackModelKey(key: string): boolean {
|
|
|
1226
1326
|
}
|
|
1227
1327
|
|
|
1228
1328
|
/**
|
|
1229
|
-
* A
|
|
1230
|
-
*
|
|
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.
|
|
1231
1333
|
*/
|
|
1232
1334
|
function isRetryFallbackWildcardKey(key: string): boolean {
|
|
1233
1335
|
return key.endsWith("/*");
|
|
1234
1336
|
}
|
|
1235
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
|
+
|
|
1236
1354
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1237
1355
|
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
1238
1356
|
}
|
|
@@ -1536,15 +1654,20 @@ function queuedTextContent(message: AgentMessage): string | undefined {
|
|
|
1536
1654
|
if (!("content" in message)) return undefined;
|
|
1537
1655
|
const content = message.content;
|
|
1538
1656
|
if (typeof content === "string") return content;
|
|
1539
|
-
|
|
1657
|
+
for (const part of content) {
|
|
1658
|
+
if (part.type === "text") return part.text;
|
|
1659
|
+
}
|
|
1660
|
+
return undefined;
|
|
1540
1661
|
}
|
|
1541
1662
|
|
|
1542
1663
|
function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
|
|
1543
1664
|
if (!("content" in message) || typeof message.content === "string") return undefined;
|
|
1544
|
-
const images =
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1665
|
+
const images: ImageContent[] = [];
|
|
1666
|
+
for (const part of message.content) {
|
|
1667
|
+
if (part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string") {
|
|
1668
|
+
images.push(part);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1548
1671
|
return images.length > 0 ? images : undefined;
|
|
1549
1672
|
}
|
|
1550
1673
|
|
|
@@ -1789,6 +1912,11 @@ export class AgentSession {
|
|
|
1789
1912
|
#advisors: ActiveAdvisor[] = [];
|
|
1790
1913
|
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1791
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();
|
|
1792
1920
|
/** Provider-facing UUIDv7 identities keyed by primary provider session and advisor slug. */
|
|
1793
1921
|
#advisorProviderSessionIds = new Map<string, string>();
|
|
1794
1922
|
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
@@ -1852,11 +1980,13 @@ export class AgentSession {
|
|
|
1852
1980
|
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
1853
1981
|
* the session cwd changes. */
|
|
1854
1982
|
#titleSystemPrompt: string | undefined;
|
|
1983
|
+
#titleGenerationAbortController = new AbortController();
|
|
1855
1984
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1856
1985
|
|
|
1857
1986
|
// Bash execution state
|
|
1858
1987
|
#bashAbortControllers = new Set<AbortController>();
|
|
1859
|
-
#pendingBashMessages:
|
|
1988
|
+
#pendingBashMessages: PendingBashMessage[] = [];
|
|
1989
|
+
#bashSessionTarget!: BashSessionTarget;
|
|
1860
1990
|
|
|
1861
1991
|
// Python execution state
|
|
1862
1992
|
#evalAbortControllers = new Set<AbortController>();
|
|
@@ -1890,9 +2020,17 @@ export class AgentSession {
|
|
|
1890
2020
|
#providerSessionId: string | undefined;
|
|
1891
2021
|
#freshProviderSessionId: string | undefined;
|
|
1892
2022
|
#inheritedProviderPromptCacheKey: string | undefined;
|
|
2023
|
+
#autolearnCaptureAbortController: AbortController | undefined;
|
|
2024
|
+
#autolearnCaptureTask: Promise<void> | undefined;
|
|
1893
2025
|
#isDisposed = false;
|
|
1894
2026
|
// Extension system
|
|
1895
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;
|
|
1896
2034
|
#turnIndex = 0;
|
|
1897
2035
|
#messageEndPersistenceTail: Promise<void> = Promise.resolve();
|
|
1898
2036
|
#pendingMessageEndPersistence = new Map<string, Promise<void>>();
|
|
@@ -1907,6 +2045,7 @@ export class AgentSession {
|
|
|
1907
2045
|
#mcpPromptCommands: LoadedCustomCommand[] = [];
|
|
1908
2046
|
|
|
1909
2047
|
#skillsSettings: SkillsSettings | undefined;
|
|
2048
|
+
#skillsReloadable: boolean;
|
|
1910
2049
|
|
|
1911
2050
|
// Model registry for API key resolution
|
|
1912
2051
|
#modelRegistry: ModelRegistry;
|
|
@@ -1930,8 +2069,10 @@ export class AgentSession {
|
|
|
1930
2069
|
#getLocalCalendarDate: () => string;
|
|
1931
2070
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1932
2071
|
#setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
|
|
2072
|
+
#ensureWriteRegistered: (() => Promise<boolean>) | undefined;
|
|
1933
2073
|
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
|
1934
|
-
#
|
|
2074
|
+
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
2075
|
+
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
1935
2076
|
#baseSystemPrompt: string[];
|
|
1936
2077
|
#baseSystemPromptBeforeMemoryPromotion: string[] | undefined;
|
|
1937
2078
|
/**
|
|
@@ -2422,12 +2563,17 @@ export class AgentSession {
|
|
|
2422
2563
|
readPlan: url => this.#readPlanYoloFile(url),
|
|
2423
2564
|
listPlanFiles: () => this.#listPlanYoloFiles(),
|
|
2424
2565
|
});
|
|
2566
|
+
this.setPlanModeState(undefined);
|
|
2425
2567
|
const previousTools = this.#planYoloPreviousTools;
|
|
2426
|
-
|
|
2427
|
-
|
|
2568
|
+
try {
|
|
2569
|
+
if (previousTools) {
|
|
2570
|
+
await this.setActiveToolsByName(previousTools);
|
|
2571
|
+
}
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
this.setPlanModeState(state);
|
|
2574
|
+
throw error;
|
|
2428
2575
|
}
|
|
2429
2576
|
this.setPlanProposalHandler(null);
|
|
2430
|
-
this.setPlanModeState(undefined);
|
|
2431
2577
|
this.#planYolo = undefined;
|
|
2432
2578
|
this.#planYoloPreviousTools = undefined;
|
|
2433
2579
|
await this.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
@@ -2485,6 +2631,11 @@ export class AgentSession {
|
|
|
2485
2631
|
constructor(config: AgentSessionConfig) {
|
|
2486
2632
|
this.agent = config.agent;
|
|
2487
2633
|
this.sessionManager = config.sessionManager;
|
|
2634
|
+
this.#bashSessionTarget = {
|
|
2635
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
2636
|
+
refs: 0,
|
|
2637
|
+
destination: { kind: "current", manager: this.sessionManager },
|
|
2638
|
+
};
|
|
2488
2639
|
this.settings = config.settings;
|
|
2489
2640
|
this.#autoApprove = config.autoApprove === true;
|
|
2490
2641
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
@@ -2516,6 +2667,7 @@ export class AgentSession {
|
|
|
2516
2667
|
this.#skills = config.skills ?? [];
|
|
2517
2668
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2518
2669
|
this.#customCommands = config.customCommands ?? [];
|
|
2670
|
+
this.#skillsReloadable = config.skillsReloadable ?? true;
|
|
2519
2671
|
this.#skillsSettings = config.skillsSettings;
|
|
2520
2672
|
this.#modelRegistry = config.modelRegistry;
|
|
2521
2673
|
// Resolve the wire service-tier per request so the Fireworks Priority
|
|
@@ -2534,7 +2686,8 @@ export class AgentSession {
|
|
|
2534
2686
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2535
2687
|
this.#createVibeTools = config.createVibeTools;
|
|
2536
2688
|
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
2537
|
-
this.#
|
|
2689
|
+
this.#presentationPinnedToolNames = config.presentationPinnedToolNames;
|
|
2690
|
+
this.#ensureWriteRegistered = config.ensureWriteRegistered;
|
|
2538
2691
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2539
2692
|
this.#transformProviderContext = config.transformProviderContext;
|
|
2540
2693
|
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
@@ -2586,7 +2739,18 @@ export class AgentSession {
|
|
|
2586
2739
|
this.#advisorPrimaryTurnsCompleted++;
|
|
2587
2740
|
if (this.#advisors.length > 0) {
|
|
2588
2741
|
for (const a of this.#advisors) {
|
|
2589
|
-
if (
|
|
2742
|
+
if (a.runtime.disposed) continue;
|
|
2743
|
+
try {
|
|
2744
|
+
a.runtime.onTurnEnd(messages, { willContinue: context?.willContinue });
|
|
2745
|
+
} catch (advisorErr) {
|
|
2746
|
+
// CRITICAL boundary: NOTHING an advisor does may abort the
|
|
2747
|
+
// primary agent's turn-end. A throwing advisor loses its
|
|
2748
|
+
// delta; the primary continues untouched.
|
|
2749
|
+
logger.warn("advisor onTurnEnd threw; delta dropped", {
|
|
2750
|
+
advisor: a.name,
|
|
2751
|
+
err: String(advisorErr),
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2590
2754
|
}
|
|
2591
2755
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
2592
2756
|
if (syncBacklog !== "off") {
|
|
@@ -2795,6 +2959,12 @@ export class AgentSession {
|
|
|
2795
2959
|
slug = candidate;
|
|
2796
2960
|
usedSlugs.add(slug);
|
|
2797
2961
|
}
|
|
2962
|
+
// Per-advisor toggle: skip disabled advisors but keep them in the
|
|
2963
|
+
// status map so they show `○` rather than disappearing.
|
|
2964
|
+
if (config.enabled === false) {
|
|
2965
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "paused" });
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2798
2968
|
|
|
2799
2969
|
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2800
2970
|
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
@@ -2805,6 +2975,7 @@ export class AgentSession {
|
|
|
2805
2975
|
model = resolved.model;
|
|
2806
2976
|
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
2807
2977
|
if (!model) {
|
|
2978
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2808
2979
|
if (emitWarnings) {
|
|
2809
2980
|
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2810
2981
|
}
|
|
@@ -2813,6 +2984,7 @@ export class AgentSession {
|
|
|
2813
2984
|
} else {
|
|
2814
2985
|
const sel = resolveAdvisorRoleSelection(this.settings, this.#modelRegistry.getAvailable());
|
|
2815
2986
|
if (!sel) {
|
|
2987
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2816
2988
|
if (emitWarnings) {
|
|
2817
2989
|
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2818
2990
|
advisor: config.name,
|
|
@@ -2836,6 +3008,11 @@ export class AgentSession {
|
|
|
2836
3008
|
const requestedLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2837
3009
|
const resolvedLevel = resolveThinkingLevelForModel(model, requestedLevel);
|
|
2838
3010
|
const advisorThinkingLevel: ThinkingLevel = resolvedLevel ?? ThinkingLevel.Inherit;
|
|
3011
|
+
// Record the status entry now (in roster order) so the Map's insertion
|
|
3012
|
+
// order matches the configured roster even when earlier advisors were
|
|
3013
|
+
// skipped as paused/no_model. The build loop overwrites this to "running"
|
|
3014
|
+
// without changing insertion order.
|
|
3015
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "running" });
|
|
2839
3016
|
descriptors.push({
|
|
2840
3017
|
config,
|
|
2841
3018
|
name: config.name,
|
|
@@ -2871,6 +3048,11 @@ export class AgentSession {
|
|
|
2871
3048
|
if (!this.#advisorEnabled) return false;
|
|
2872
3049
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
2873
3050
|
|
|
3051
|
+
// Rebuild the status map from scratch so removed/renamed advisors don't
|
|
3052
|
+
// leave stale entries. #resolveAdvisorRuntimeDescriptors populates every
|
|
3053
|
+
// entry (`paused`/`no_model`/`running`) in roster order; the build loop
|
|
3054
|
+
// below confirms `running` for successfully built advisors.
|
|
3055
|
+
this.#advisorStatuses.clear();
|
|
2874
3056
|
const descriptors = this.#resolveAdvisorRuntimeDescriptors(true);
|
|
2875
3057
|
|
|
2876
3058
|
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
@@ -2911,11 +3093,16 @@ export class AgentSession {
|
|
|
2911
3093
|
|
|
2912
3094
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2913
3095
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
3096
|
+
const advisorLoopTools: AgentTool<any>[] = [adviseTool, ...tools];
|
|
3097
|
+
const advisorToolMap = new Map<string, AgentTool>();
|
|
2914
3098
|
const availableAdvisorToolNames = new Set<string>();
|
|
2915
|
-
|
|
2916
|
-
for (const tool of tools) {
|
|
3099
|
+
for (const tool of advisorLoopTools) {
|
|
2917
3100
|
availableAdvisorToolNames.add(tool.name);
|
|
2918
|
-
|
|
3101
|
+
advisorToolMap.set(tool.name, tool);
|
|
3102
|
+
if (tool.customWireName !== undefined) {
|
|
3103
|
+
availableAdvisorToolNames.add(tool.customWireName);
|
|
3104
|
+
advisorToolMap.set(tool.customWireName, tool);
|
|
3105
|
+
}
|
|
2919
3106
|
}
|
|
2920
3107
|
let quarantinedAdvisorOutput: string | undefined;
|
|
2921
3108
|
let currentAdvisorInput = "";
|
|
@@ -2955,17 +3142,36 @@ export class AgentSession {
|
|
|
2955
3142
|
// Codex request identity remains UUID-shaped while local labels keep the
|
|
2956
3143
|
// `-advisor` suffix.
|
|
2957
3144
|
const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorProviderSessionId;
|
|
3145
|
+
// On the Cursor provider every tool runs server-side and is dispatched
|
|
3146
|
+
// back through `cursorExecHandlers`; without this bridge the advisor's
|
|
3147
|
+
// own tools (including the MCP `advise` tool) return `toolNotFound` and
|
|
3148
|
+
// no advice is ever routed (issue #5680). Mirrors the primary agent's
|
|
3149
|
+
// bridge (`sdk.ts`), scoped to this advisor's granted tool set.
|
|
3150
|
+
// Cursor's native `delete` frame removes files directly, bypassing the
|
|
3151
|
+
// tool map, so gate it on the advisor actually holding a file-mutating
|
|
3152
|
+
// tool. A default read-only advisor (advise/read/grep/glob) never gets
|
|
3153
|
+
// to delete workspace files it was never granted (issue #5680 review).
|
|
3154
|
+
const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
|
|
3155
|
+
if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
|
|
3156
|
+
const advisorCursorExecHandlers = new CursorExecHandlers({
|
|
3157
|
+
cwd: this.sessionManager.getCwd(),
|
|
3158
|
+
getCwd: () => this.sessionManager.getCwd(),
|
|
3159
|
+
tools: advisorToolMap,
|
|
3160
|
+
allowNativeDelete: advisorCanMutateFiles,
|
|
3161
|
+
});
|
|
2958
3162
|
const advisorAgent = new Agent({
|
|
2959
3163
|
initialState: {
|
|
2960
3164
|
systemPrompt,
|
|
2961
3165
|
model: advisorModel,
|
|
2962
3166
|
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2963
|
-
tools:
|
|
3167
|
+
tools: advisorLoopTools,
|
|
2964
3168
|
},
|
|
2965
3169
|
appendOnlyContext,
|
|
2966
3170
|
sessionId: advisorProviderSessionId,
|
|
2967
3171
|
promptCacheKey: advisorPromptCacheKey,
|
|
2968
3172
|
providerSessionState: this.#providerSessionState,
|
|
3173
|
+
cursorExecHandlers: advisorCursorExecHandlers,
|
|
3174
|
+
cwdResolver: () => this.sessionManager.getCwd(),
|
|
2969
3175
|
preferWebsockets: this.#preferWebsockets,
|
|
2970
3176
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
2971
3177
|
streamFn: this.#advisorStreamFn,
|
|
@@ -3036,33 +3242,30 @@ export class AgentSession {
|
|
|
3036
3242
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
3037
3243
|
obfuscator: this.#obfuscator,
|
|
3038
3244
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
3039
|
-
onTurnError:
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
advisorModel.provider,
|
|
3050
|
-
advisorProviderSessionId,
|
|
3051
|
-
{
|
|
3052
|
-
retryAfterMs: extractRetryHint(undefined, message),
|
|
3053
|
-
baseUrl: advisorModel.baseUrl,
|
|
3054
|
-
modelId: advisorModel.id,
|
|
3055
|
-
},
|
|
3056
|
-
);
|
|
3245
|
+
onTurnError: (error, failedMessages) => this.#recoverAdvisorTurn(advisorRef, error, failedMessages),
|
|
3246
|
+
onTurnSuccess: async () => {
|
|
3247
|
+
const fallback = advisorRef.retryFallback;
|
|
3248
|
+
if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
|
|
3249
|
+
advisorRef.retryFallbackPendingSuccess = false;
|
|
3250
|
+
await this.#emitSessionEvent({
|
|
3251
|
+
type: "retry_fallback_succeeded",
|
|
3252
|
+
model: formatRetryFallbackSelector(advisorRef.agent.state.model, advisorRef.thinkingLevel),
|
|
3253
|
+
role: fallback.role,
|
|
3254
|
+
});
|
|
3057
3255
|
},
|
|
3058
3256
|
notifyFailure: error => {
|
|
3257
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "error" });
|
|
3059
3258
|
const message = error instanceof Error ? error.message : String(error);
|
|
3060
3259
|
this.emitNotice(
|
|
3061
3260
|
"warning",
|
|
3062
|
-
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(
|
|
3261
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorAgent.state.model)}: ${message}`,
|
|
3063
3262
|
"advisor",
|
|
3064
3263
|
);
|
|
3065
3264
|
},
|
|
3265
|
+
notifyQuotaExhausted: () => {
|
|
3266
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "quota_exhausted" });
|
|
3267
|
+
this.emitNotice("warning", `Advisor "${advisorName}" quota exhausted — pausing until reset.`, "advisor");
|
|
3268
|
+
},
|
|
3066
3269
|
});
|
|
3067
3270
|
|
|
3068
3271
|
const advisorRef: ActiveAdvisor = {
|
|
@@ -3076,10 +3279,13 @@ export class AgentSession {
|
|
|
3076
3279
|
recorderClosed: Promise.resolve(),
|
|
3077
3280
|
model: advisorModel,
|
|
3078
3281
|
thinkingLevel: advisorThinkingLevel,
|
|
3282
|
+
providerSessionId: advisorProviderSessionId,
|
|
3283
|
+
retryFallbackPendingSuccess: false,
|
|
3079
3284
|
signature,
|
|
3080
3285
|
};
|
|
3081
3286
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
3082
3287
|
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
3288
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
3083
3289
|
this.#advisors.push(advisorRef);
|
|
3084
3290
|
}
|
|
3085
3291
|
|
|
@@ -3110,11 +3316,11 @@ export class AgentSession {
|
|
|
3110
3316
|
/**
|
|
3111
3317
|
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
3112
3318
|
* blocker interrupt the running agent through the steering channel; once the
|
|
3113
|
-
* loop has yielded, `triggerTurn` resumes it.
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3116
|
-
* user interrupt auto-resume is suppressed while idle/unwinding
|
|
3117
|
-
* becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
3319
|
+
* loop has yielded, `triggerTurn` resumes it. After a terminal text answer with
|
|
3320
|
+
* no queued work, a concern is preserved as a visible advisor card, while a
|
|
3321
|
+
* blocker wakes the primary to acknowledge work it handed off incorrectly.
|
|
3322
|
+
* After a deliberate user interrupt auto-resume is suppressed while idle/unwinding
|
|
3323
|
+
* (the note becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
3118
3324
|
* steered in directly. A plain nit always rides the non-interrupting YieldQueue
|
|
3119
3325
|
* aside. Suppression by the per-advisor emission guard drops the note silently —
|
|
3120
3326
|
* the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
|
|
@@ -3171,10 +3377,19 @@ export class AgentSession {
|
|
|
3171
3377
|
});
|
|
3172
3378
|
return;
|
|
3173
3379
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3380
|
+
// A steered interrupting note only continues the run when the session can
|
|
3381
|
+
// actually start (or is already running) a turn. Two idle cases cannot, so
|
|
3382
|
+
// `sendCustomMessage({ triggerTurn: true })` would silently bury the card in
|
|
3383
|
+
// `#pendingNextTurnMessages` until the next user prompt — strictly worse than
|
|
3384
|
+
// the visible preserved card. Preserve instead:
|
|
3385
|
+
// - Plan mode: only user-driven turns converge on ask/resolve.
|
|
3386
|
+
// - ACP bridges with `deferAgentInitiatedTurns`: the client cannot show an
|
|
3387
|
+
// agent-initiated turn as busy, so idle triggers are refused (#5628 review).
|
|
3388
|
+
const cannotAutoTrigger =
|
|
3389
|
+
!this.agent.state.isStreaming &&
|
|
3390
|
+
this.#clientBridge?.deferAgentInitiatedTurns === true &&
|
|
3391
|
+
!this.#allowAcpAgentInitiatedTurns;
|
|
3392
|
+
if (this.#planModeState?.enabled || cannotAutoTrigger) {
|
|
3178
3393
|
this.#preserveAdvisorCard({
|
|
3179
3394
|
role: "custom",
|
|
3180
3395
|
customType: "advisor",
|
|
@@ -3186,6 +3401,11 @@ export class AgentSession {
|
|
|
3186
3401
|
});
|
|
3187
3402
|
return;
|
|
3188
3403
|
}
|
|
3404
|
+
// Arm the post-interrupt immune window only now that a turn is actually
|
|
3405
|
+
// being steered/triggered. A merely preserved card never interrupts, so
|
|
3406
|
+
// arming earlier would downgrade the next `advisor.immuneTurns` worth of
|
|
3407
|
+
// real concerns/blockers to skip-idle-flush asides (#5628 review).
|
|
3408
|
+
this.#recordAdvisorInterruptDelivered();
|
|
3189
3409
|
void this.sendCustomMessage(
|
|
3190
3410
|
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
3191
3411
|
{ deliverAs: "steer", triggerTurn: true },
|
|
@@ -3227,6 +3447,163 @@ export class AgentSession {
|
|
|
3227
3447
|
});
|
|
3228
3448
|
}
|
|
3229
3449
|
|
|
3450
|
+
/** Switch one advisor model while preserving its context and effort invariants. */
|
|
3451
|
+
#setAdvisorModel(advisor: ActiveAdvisor, model: Model, requestedThinkingLevel: ThinkingLevel): ThinkingLevel {
|
|
3452
|
+
const resolvedThinkingLevel = resolveThinkingLevelForModel(model, requestedThinkingLevel);
|
|
3453
|
+
const nextThinkingLevel = resolvedThinkingLevel ?? ThinkingLevel.Inherit;
|
|
3454
|
+
advisor.agent.setModel(model);
|
|
3455
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
3456
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(nextThinkingLevel));
|
|
3457
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3458
|
+
advisor.model = model;
|
|
3459
|
+
advisor.thinkingLevel = nextThinkingLevel;
|
|
3460
|
+
return nextThinkingLevel;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
/** Restore an advisor's configured primary once its fallback cooldown expires. */
|
|
3464
|
+
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
|
|
3465
|
+
const fallback = advisor.retryFallback;
|
|
3466
|
+
if (!fallback || this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
3467
|
+
|
|
3468
|
+
const originalSelector = parseRetryFallbackSelector(fallback.originalSelector, this.#modelRegistry);
|
|
3469
|
+
if (!originalSelector) {
|
|
3470
|
+
advisor.retryFallback = undefined;
|
|
3471
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
const currentSelector = formatRetryFallbackSelector(advisor.agent.state.model, advisor.thinkingLevel);
|
|
3475
|
+
if (currentSelector === originalSelector.raw) {
|
|
3476
|
+
if (!this.#isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
3477
|
+
advisor.retryFallback = undefined;
|
|
3478
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3479
|
+
}
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
3483
|
+
|
|
3484
|
+
const resolvedPrimary = resolveModelOverride([originalSelector.raw], this.#modelRegistry, this.settings);
|
|
3485
|
+
const primaryModel =
|
|
3486
|
+
resolvedPrimary.model ?? this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
3487
|
+
if (!primaryModel) return;
|
|
3488
|
+
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
|
|
3489
|
+
if (!apiKey) return;
|
|
3490
|
+
|
|
3491
|
+
const thinkingToApply =
|
|
3492
|
+
advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
|
|
3493
|
+
? fallback.originalThinkingLevel
|
|
3494
|
+
: advisor.thinkingLevel;
|
|
3495
|
+
this.#setAdvisorModel(advisor, primaryModel, thinkingToApply);
|
|
3496
|
+
this.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(primaryModel));
|
|
3497
|
+
advisor.retryFallback = undefined;
|
|
3498
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
/**
|
|
3502
|
+
* Apply the advisor's configured provider-failure fallback chain after
|
|
3503
|
+
* same-provider credential rotation has no usable sibling.
|
|
3504
|
+
*/
|
|
3505
|
+
async #recoverAdvisorTurn(
|
|
3506
|
+
advisor: ActiveAdvisor,
|
|
3507
|
+
error: unknown,
|
|
3508
|
+
failedMessages: readonly AgentMessage[],
|
|
3509
|
+
): Promise<boolean> {
|
|
3510
|
+
if (error instanceof AdvisorOutputQuarantinedError) return false;
|
|
3511
|
+
|
|
3512
|
+
const failedMessage = failedMessages.findLast(
|
|
3513
|
+
(message): message is AssistantMessage => message.role === "assistant",
|
|
3514
|
+
);
|
|
3515
|
+
if (failedMessage?.stopReason !== "error") {
|
|
3516
|
+
// Stream setup can reject before any assistant turn is recorded (e.g.
|
|
3517
|
+
// an HTTP 429 thrown from prompt()); classify the raw error so a
|
|
3518
|
+
// structural usage limit still marks the exhausted credential.
|
|
3519
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3520
|
+
if (!AIError.isUsageLimit(error) && !isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
|
|
3521
|
+
return false;
|
|
3522
|
+
}
|
|
3523
|
+
const currentModel = advisor.agent.state.model;
|
|
3524
|
+
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
3525
|
+
currentModel.provider,
|
|
3526
|
+
advisor.providerSessionId,
|
|
3527
|
+
{
|
|
3528
|
+
retryAfterMs: extractRetryHint(undefined, message),
|
|
3529
|
+
baseUrl: currentModel.baseUrl,
|
|
3530
|
+
modelId: currentModel.id,
|
|
3531
|
+
},
|
|
3532
|
+
);
|
|
3533
|
+
return outcome.switched;
|
|
3534
|
+
}
|
|
3535
|
+
if (failedMessage.content.some(block => block.type === "toolCall")) return false;
|
|
3536
|
+
|
|
3537
|
+
const currentModel = advisor.agent.state.model;
|
|
3538
|
+
const message = failedMessage.errorMessage ?? (error instanceof Error ? error.message : String(error));
|
|
3539
|
+
const errorId = AIError.classifyMessage({
|
|
3540
|
+
api: currentModel.api,
|
|
3541
|
+
errorId: failedMessage.errorId,
|
|
3542
|
+
errorMessage: message,
|
|
3543
|
+
errorStatus: failedMessage.errorStatus,
|
|
3544
|
+
});
|
|
3545
|
+
if (AIError.is(errorId, AIError.Flag.Abort) || AIError.is(errorId, AIError.Flag.UserInterrupt)) return false;
|
|
3546
|
+
if (AIError.isContextOverflow(failedMessage, currentModel.contextWindow ?? 0)) return false;
|
|
3547
|
+
|
|
3548
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, advisor.thinkingLevel);
|
|
3549
|
+
|
|
3550
|
+
const retryAfterMs = extractRetryHint(undefined, message);
|
|
3551
|
+
if (
|
|
3552
|
+
AIError.is(errorId, AIError.Flag.UsageLimit) ||
|
|
3553
|
+
isUsageLimitOutcome(extractHttpStatusFromError(error), message)
|
|
3554
|
+
) {
|
|
3555
|
+
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
3556
|
+
currentModel.provider,
|
|
3557
|
+
advisor.providerSessionId,
|
|
3558
|
+
{
|
|
3559
|
+
retryAfterMs,
|
|
3560
|
+
baseUrl: currentModel.baseUrl,
|
|
3561
|
+
modelId: currentModel.id,
|
|
3562
|
+
},
|
|
3563
|
+
);
|
|
3564
|
+
if (outcome.switched) return true;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
3568
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
3569
|
+
const role = advisor.retryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector, currentModel);
|
|
3570
|
+
if (!role || this.#findRetryFallbackCandidates(role, currentSelector, currentModel).length === 0) return false;
|
|
3571
|
+
|
|
3572
|
+
this.#noteRetryFallbackCooldown(currentSelector, retryAfterMs, message);
|
|
3573
|
+
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector, currentModel)) {
|
|
3574
|
+
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
3575
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
3576
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
3577
|
+
if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
|
|
3578
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, advisor.providerSessionId);
|
|
3579
|
+
if (!apiKey) continue;
|
|
3580
|
+
|
|
3581
|
+
const originalThinkingLevel = advisor.thinkingLevel;
|
|
3582
|
+
const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
|
|
3583
|
+
const nextThinkingLevel = this.#setAdvisorModel(advisor, candidate, requestedThinkingLevel);
|
|
3584
|
+
if (advisor.retryFallback) {
|
|
3585
|
+
advisor.retryFallback.lastAppliedThinkingLevel = nextThinkingLevel;
|
|
3586
|
+
} else {
|
|
3587
|
+
advisor.retryFallback = {
|
|
3588
|
+
role,
|
|
3589
|
+
originalSelector: currentSelector,
|
|
3590
|
+
originalThinkingLevel,
|
|
3591
|
+
lastAppliedThinkingLevel: nextThinkingLevel,
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
advisor.retryFallbackPendingSuccess = true;
|
|
3595
|
+
this.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(candidate));
|
|
3596
|
+
await this.#emitSessionEvent({
|
|
3597
|
+
type: "retry_fallback_applied",
|
|
3598
|
+
from: currentSelector,
|
|
3599
|
+
to: selector.raw,
|
|
3600
|
+
role,
|
|
3601
|
+
});
|
|
3602
|
+
return true;
|
|
3603
|
+
}
|
|
3604
|
+
return false;
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3230
3607
|
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
3231
3608
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
3232
3609
|
if (!promotionSettings.enabled) return false;
|
|
@@ -3239,10 +3616,7 @@ export class AgentSession {
|
|
|
3239
3616
|
// keeps its suffix across a promotion); only the model changes.
|
|
3240
3617
|
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
3241
3618
|
try {
|
|
3242
|
-
advisor
|
|
3243
|
-
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
3244
|
-
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
3245
|
-
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3619
|
+
this.#setAdvisorModel(advisor, targetModel, advisorThinkingLevel);
|
|
3246
3620
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
3247
3621
|
advisor: advisor.name,
|
|
3248
3622
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
@@ -3261,6 +3635,7 @@ export class AgentSession {
|
|
|
3261
3635
|
}
|
|
3262
3636
|
|
|
3263
3637
|
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
3638
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor);
|
|
3264
3639
|
const agent = advisor.agent;
|
|
3265
3640
|
|
|
3266
3641
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -3272,10 +3647,23 @@ export class AgentSession {
|
|
|
3272
3647
|
if (contextWindow <= 0) return false;
|
|
3273
3648
|
|
|
3274
3649
|
const messages = agent.state.messages;
|
|
3275
|
-
|
|
3650
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
3651
|
+
let storedConversationTokens = 0;
|
|
3276
3652
|
for (const message of messages) {
|
|
3277
|
-
|
|
3278
|
-
}
|
|
3653
|
+
storedConversationTokens += estimateTokens(message, estimateOptions);
|
|
3654
|
+
}
|
|
3655
|
+
// Provider usage (including cache reads and generated output) is the
|
|
3656
|
+
// trustworthy anchor for accumulated context. Add only the trailing incoming
|
|
3657
|
+
// delta to that arm. Floor it by a full local estimate — fixed advisor system
|
|
3658
|
+
// prompt, tool schemas, stored messages, and incoming delta — so provider
|
|
3659
|
+
// under-reporting or payload transforms cannot suppress maintenance.
|
|
3660
|
+
const providerContextTokens = this.#estimateAdvisorContextTokens(messages) + incomingTokens;
|
|
3661
|
+
const localContextTokens =
|
|
3662
|
+
countTokens(agent.state.systemPrompt) +
|
|
3663
|
+
estimateToolSchemaTokens(agent.state.tools) +
|
|
3664
|
+
storedConversationTokens +
|
|
3665
|
+
incomingTokens;
|
|
3666
|
+
const contextTokens = compactionContextTokens(providerContextTokens, localContextTokens);
|
|
3279
3667
|
|
|
3280
3668
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
3281
3669
|
return false;
|
|
@@ -3299,6 +3687,7 @@ export class AgentSession {
|
|
|
3299
3687
|
const timestamp = String(message.timestamp || Date.now());
|
|
3300
3688
|
|
|
3301
3689
|
if (message.role === "compactionSummary") {
|
|
3690
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
3302
3691
|
return {
|
|
3303
3692
|
type: "compaction",
|
|
3304
3693
|
id,
|
|
@@ -3306,9 +3695,7 @@ export class AgentSession {
|
|
|
3306
3695
|
timestamp,
|
|
3307
3696
|
summary: message.summary,
|
|
3308
3697
|
shortSummary: message.shortSummary,
|
|
3309
|
-
firstKeptEntryId:
|
|
3310
|
-
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
3311
|
-
`msg-${i + 1}`,
|
|
3698
|
+
firstKeptEntryId: advisorSummary.firstKeptEntryId || `msg-${i + 1}`,
|
|
3312
3699
|
tokensBefore: message.tokensBefore,
|
|
3313
3700
|
} satisfies CompactionEntry;
|
|
3314
3701
|
}
|
|
@@ -3402,11 +3789,15 @@ export class AgentSession {
|
|
|
3402
3789
|
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
3403
3790
|
const tokensBefore = compactResult.tokensBefore;
|
|
3404
3791
|
|
|
3405
|
-
//
|
|
3792
|
+
// The retained messages still carry provider usage from before this
|
|
3793
|
+
// compaction. Record their exact array boundary on the in-memory summary so
|
|
3794
|
+
// only assistants appended afterward can become the next usage anchor.
|
|
3795
|
+
const advisorUsageAnchorStartIndex = preparation.recentMessages.length + 1;
|
|
3406
3796
|
const summaryMessage = {
|
|
3407
3797
|
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
3408
3798
|
firstKeptEntryId,
|
|
3409
|
-
|
|
3799
|
+
advisorUsageAnchorStartIndex,
|
|
3800
|
+
} satisfies AdvisorCompactionSummaryMessage;
|
|
3410
3801
|
|
|
3411
3802
|
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
3412
3803
|
return false;
|
|
@@ -3962,7 +4353,7 @@ export class AgentSession {
|
|
|
3962
4353
|
}
|
|
3963
4354
|
const skipPersistedRewindResult =
|
|
3964
4355
|
message.role === "toolResult" &&
|
|
3965
|
-
message.toolName === "rewind" &&
|
|
4356
|
+
semanticToolResult(message.toolName, message)?.toolName === "rewind" &&
|
|
3966
4357
|
this.#rewoundToolResultIds.delete(message.toolCallId);
|
|
3967
4358
|
if (!skipPersistedRewindResult) {
|
|
3968
4359
|
this.#appendSessionMessage(message);
|
|
@@ -4333,29 +4724,28 @@ export class AgentSession {
|
|
|
4333
4724
|
}
|
|
4334
4725
|
}
|
|
4335
4726
|
if (event.message.role === "toolResult") {
|
|
4336
|
-
const { toolName, toolCallId,
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
isError?: boolean;
|
|
4341
|
-
content?: Array<TextContent | ImageContent>;
|
|
4342
|
-
};
|
|
4727
|
+
const { toolName, toolCallId, isError, content } = event.message;
|
|
4728
|
+
const details = isRecord(event.message.details) ? event.message.details : undefined;
|
|
4729
|
+
const semanticResult = semanticToolResult(toolName, event.message);
|
|
4730
|
+
const semanticDetails = isRecord(semanticResult?.details) ? semanticResult.details : undefined;
|
|
4343
4731
|
// A tool actually ran. Clear the post-reminder suppression: the agent did
|
|
4344
4732
|
// productive work in response to the prior nudge, so the next text-only stop
|
|
4345
4733
|
// is allowed to escalate to the next reminder if todos remain incomplete.
|
|
4346
4734
|
this.#todoReminderAwaitingProgress = false;
|
|
4347
4735
|
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
4348
|
-
|
|
4349
|
-
|
|
4736
|
+
const editedPath = details ? getStringProperty(details, "path") : undefined;
|
|
4737
|
+
if (toolName === "edit" && editedPath) {
|
|
4738
|
+
this.#invalidateFileCacheForPath(editedPath);
|
|
4350
4739
|
}
|
|
4351
|
-
|
|
4352
|
-
|
|
4740
|
+
const phases = details?.phases;
|
|
4741
|
+
if (toolName === "todo" && !isError && details && Array.isArray(phases) && phases.every(isTodoPhase)) {
|
|
4742
|
+
this.setTodoPhases(phases);
|
|
4353
4743
|
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
4354
4744
|
this.#scheduleReplanTitleRefresh();
|
|
4355
4745
|
}
|
|
4356
4746
|
}
|
|
4357
4747
|
if (toolName === "todo" && isError) {
|
|
4358
|
-
const errorText = content
|
|
4748
|
+
const errorText = content.find(part => part.type === "text")?.text;
|
|
4359
4749
|
const reminderText = [
|
|
4360
4750
|
"<system-reminder>",
|
|
4361
4751
|
"todo failed, so todo progress is not visible to the user.",
|
|
@@ -4373,18 +4763,19 @@ export class AgentSession {
|
|
|
4373
4763
|
{ deliverAs: "nextTurn" },
|
|
4374
4764
|
);
|
|
4375
4765
|
}
|
|
4376
|
-
if (toolName === "checkpoint" && !isError) {
|
|
4766
|
+
if (semanticResult?.toolName === "checkpoint" && !isError) {
|
|
4377
4767
|
const checkpointEntryId = this.sessionManager.getEntries().at(-1)?.id ?? null;
|
|
4378
4768
|
this.#checkpointState = {
|
|
4379
4769
|
checkpointMessageCount: this.agent.state.messages.length,
|
|
4380
4770
|
checkpointEntryId,
|
|
4381
|
-
startedAt:
|
|
4771
|
+
startedAt:
|
|
4772
|
+
(semanticDetails && stringProperty(semanticDetails, "startedAt")) ?? new Date().toISOString(),
|
|
4382
4773
|
};
|
|
4383
4774
|
this.#pendingRewindReport = undefined;
|
|
4384
4775
|
this.#lastCompletedRewind = undefined;
|
|
4385
4776
|
}
|
|
4386
|
-
if (toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4387
|
-
const detailReport =
|
|
4777
|
+
if (semanticResult?.toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4778
|
+
const detailReport = semanticDetails ? (stringProperty(semanticDetails, "report")?.trim() ?? "") : "";
|
|
4388
4779
|
const textReport = content?.find(part => part.type === "text")?.text?.trim() ?? "";
|
|
4389
4780
|
const report = detailReport || textReport;
|
|
4390
4781
|
if (report.length > 0) {
|
|
@@ -4397,8 +4788,11 @@ export class AgentSession {
|
|
|
4397
4788
|
// Check auto-retry and auto-compaction after agent completes
|
|
4398
4789
|
if (event.type === "agent_end") {
|
|
4399
4790
|
const settledMessages = this.agent.state.messages;
|
|
4400
|
-
|
|
4401
|
-
|
|
4791
|
+
// TTSR retry work runs concurrently and clears the live flag before
|
|
4792
|
+
// maintenance can emit agent_end, so preserve the state at settle entry.
|
|
4793
|
+
const ttsrAbortPendingAtAgentEnd = this.#ttsrAbortPending;
|
|
4794
|
+
const emitAgentEndNotification = async (options?: { willContinue?: boolean }) => {
|
|
4795
|
+
await this.#emitAgentEndNotification(settledMessages, options);
|
|
4402
4796
|
};
|
|
4403
4797
|
const usage = this.getSessionStats().tokens;
|
|
4404
4798
|
await this.#goalRuntime.onAgentEnd({
|
|
@@ -4500,7 +4894,7 @@ export class AgentSession {
|
|
|
4500
4894
|
// active-goal threshold pre-empt below.
|
|
4501
4895
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
4502
4896
|
maintenanceRoute("empty-stop-handled");
|
|
4503
|
-
await emitAgentEndNotification();
|
|
4897
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4504
4898
|
return;
|
|
4505
4899
|
}
|
|
4506
4900
|
|
|
@@ -4523,30 +4917,34 @@ export class AgentSession {
|
|
|
4523
4917
|
automaticContinuationBlocked: compactionResult.automaticContinuationBlocked === true,
|
|
4524
4918
|
});
|
|
4525
4919
|
this.#resolveRetry();
|
|
4526
|
-
await emitAgentEndNotification(
|
|
4920
|
+
await emitAgentEndNotification(
|
|
4921
|
+
compactionResult.continuationScheduled ? { willContinue: true } : undefined,
|
|
4922
|
+
);
|
|
4527
4923
|
return;
|
|
4528
4924
|
}
|
|
4529
4925
|
}
|
|
4530
4926
|
|
|
4531
4927
|
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
4532
4928
|
maintenanceRoute("unexpected-stop-handled");
|
|
4533
|
-
await emitAgentEndNotification();
|
|
4929
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4534
4930
|
return;
|
|
4535
4931
|
}
|
|
4536
4932
|
|
|
4537
4933
|
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
4538
4934
|
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
4539
4935
|
if (didRetry) {
|
|
4540
|
-
await emitAgentEndNotification();
|
|
4936
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4541
4937
|
return;
|
|
4542
4938
|
}
|
|
4543
4939
|
}
|
|
4544
4940
|
|
|
4545
|
-
// A deliberate abort should settle the current turn, not trigger queued
|
|
4941
|
+
// A deliberate abort should settle the current turn, not trigger queued
|
|
4942
|
+
// continuations — except TTSR self-repair, which already scheduled a
|
|
4943
|
+
// hidden retry while #ttsrAbortPending is still true.
|
|
4546
4944
|
if (msg.stopReason === "aborted") {
|
|
4547
4945
|
this.#resolveRetry();
|
|
4548
4946
|
this.#resetSessionStopContinuationState();
|
|
4549
|
-
await emitAgentEndNotification();
|
|
4947
|
+
await emitAgentEndNotification(ttsrAbortPendingAtAgentEnd ? { willContinue: true } : undefined);
|
|
4550
4948
|
return;
|
|
4551
4949
|
}
|
|
4552
4950
|
// Fireworks Fast variants degrade to their base model on a failed turn —
|
|
@@ -4555,14 +4953,14 @@ export class AgentSession {
|
|
|
4555
4953
|
if (this.#isFireworksFastFallbackEligible(msg)) {
|
|
4556
4954
|
const didRetry = await this.#handleRetryableError(msg, { fireworksFastFallback: true });
|
|
4557
4955
|
if (didRetry) {
|
|
4558
|
-
await emitAgentEndNotification();
|
|
4956
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4559
4957
|
return;
|
|
4560
4958
|
}
|
|
4561
4959
|
}
|
|
4562
4960
|
if (this.#isRetryableError(msg)) {
|
|
4563
4961
|
const didRetry = await this.#handleRetryableError(msg);
|
|
4564
4962
|
if (didRetry) {
|
|
4565
|
-
await emitAgentEndNotification();
|
|
4963
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4566
4964
|
return;
|
|
4567
4965
|
}
|
|
4568
4966
|
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
@@ -4573,7 +4971,7 @@ export class AgentSession {
|
|
|
4573
4971
|
// backoff-retry of the failing model) when no switch happens.
|
|
4574
4972
|
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4575
4973
|
if (didRetry) {
|
|
4576
|
-
await emitAgentEndNotification();
|
|
4974
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4577
4975
|
return;
|
|
4578
4976
|
}
|
|
4579
4977
|
}
|
|
@@ -4612,22 +5010,22 @@ export class AgentSession {
|
|
|
4612
5010
|
compactionResult.continuationScheduled ||
|
|
4613
5011
|
compactionResult.automaticContinuationBlocked
|
|
4614
5012
|
) {
|
|
4615
|
-
await emitAgentEndNotification();
|
|
5013
|
+
await emitAgentEndNotification(compactionResult.continuationScheduled ? { willContinue: true } : undefined);
|
|
4616
5014
|
return;
|
|
4617
5015
|
}
|
|
4618
5016
|
if (msg.stopReason !== "error") {
|
|
4619
5017
|
if (this.#enforceRewindBeforeYield()) {
|
|
4620
|
-
await emitAgentEndNotification();
|
|
5018
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4621
5019
|
return;
|
|
4622
5020
|
}
|
|
4623
5021
|
const planModeContinuationScheduled = await this.#enforcePlanModeDecisionAtSettle();
|
|
4624
5022
|
if (planModeContinuationScheduled) {
|
|
4625
|
-
await emitAgentEndNotification();
|
|
5023
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4626
5024
|
return;
|
|
4627
5025
|
}
|
|
4628
5026
|
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
4629
5027
|
if (todoContinuationScheduled) {
|
|
4630
|
-
await emitAgentEndNotification();
|
|
5028
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4631
5029
|
return;
|
|
4632
5030
|
}
|
|
4633
5031
|
}
|
|
@@ -4637,11 +5035,11 @@ export class AgentSession {
|
|
|
4637
5035
|
// the session is fully idle (the todo reminder above defers the same
|
|
4638
5036
|
// way inside #checkTodoCompletion).
|
|
4639
5037
|
if (this.#hasPendingAsyncWake()) {
|
|
4640
|
-
await emitAgentEndNotification();
|
|
5038
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4641
5039
|
return;
|
|
4642
5040
|
}
|
|
4643
|
-
await this.#emitSessionStopEvent(settledMessages, msg);
|
|
4644
|
-
await emitAgentEndNotification();
|
|
5041
|
+
const sessionStopWillContinue = await this.#emitSessionStopEvent(settledMessages, msg);
|
|
5042
|
+
await emitAgentEndNotification(sessionStopWillContinue ? { willContinue: true } : undefined);
|
|
4645
5043
|
}
|
|
4646
5044
|
};
|
|
4647
5045
|
|
|
@@ -4771,7 +5169,28 @@ export class AgentSession {
|
|
|
4771
5169
|
);
|
|
4772
5170
|
}
|
|
4773
5171
|
|
|
4774
|
-
#
|
|
5172
|
+
#scheduleCompactionContinuation(options: {
|
|
5173
|
+
generation: number;
|
|
5174
|
+
autoContinue: boolean;
|
|
5175
|
+
terminalTextAnswer: boolean;
|
|
5176
|
+
suppressContinuation: boolean;
|
|
5177
|
+
}): boolean {
|
|
5178
|
+
if (options.suppressContinuation) return false;
|
|
5179
|
+
if (this.agent.hasQueuedMessages()) {
|
|
5180
|
+
this.#scheduleAgentContinue({
|
|
5181
|
+
delayMs: 100,
|
|
5182
|
+
generation: options.generation,
|
|
5183
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
5184
|
+
});
|
|
5185
|
+
return true;
|
|
5186
|
+
}
|
|
5187
|
+
if (!options.autoContinue) return false;
|
|
5188
|
+
const activeGoal = this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active";
|
|
5189
|
+
if (options.terminalTextAnswer && !activeGoal) return false;
|
|
5190
|
+
return this.#scheduleAutoContinuePrompt(options.generation);
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
#scheduleAutoContinuePrompt(generation: number): boolean {
|
|
4775
5194
|
const continuePrompt = async () => {
|
|
4776
5195
|
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
4777
5196
|
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
@@ -4796,10 +5215,18 @@ export class AgentSession {
|
|
|
4796
5215
|
async signal => {
|
|
4797
5216
|
await Promise.resolve();
|
|
4798
5217
|
if (signal.aborted) return;
|
|
5218
|
+
if (this.agent.hasQueuedMessages()) {
|
|
5219
|
+
this.#scheduleAgentContinue({
|
|
5220
|
+
generation,
|
|
5221
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
5222
|
+
});
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
4799
5225
|
await continuePrompt();
|
|
4800
5226
|
},
|
|
4801
5227
|
{ generation },
|
|
4802
5228
|
);
|
|
5229
|
+
return true;
|
|
4803
5230
|
}
|
|
4804
5231
|
|
|
4805
5232
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -5585,10 +6012,9 @@ export class AgentSession {
|
|
|
5585
6012
|
|
|
5586
6013
|
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
5587
6014
|
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
5588
|
-
// local-protocol handler. Other internal-scheme URLs
|
|
5589
|
-
//
|
|
5590
|
-
//
|
|
5591
|
-
// them through its normal dispatch path.
|
|
6015
|
+
// local-protocol handler. Other internal-scheme URLs have no local
|
|
6016
|
+
// filesystem representation; skip pre-cache entirely for those — the
|
|
6017
|
+
// edit tool itself will reject them through its normal dispatch path.
|
|
5592
6018
|
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
5593
6019
|
if (resolvedPath === undefined) return undefined;
|
|
5594
6020
|
|
|
@@ -5688,9 +6114,8 @@ export class AgentSession {
|
|
|
5688
6114
|
* - `local://` URLs route through the local-protocol handler so they map
|
|
5689
6115
|
* onto the session's on-disk artifacts directory; pre-caching, ENOENT
|
|
5690
6116
|
* handling, and post-edit invalidation all work normally.
|
|
5691
|
-
* - Other internal-scheme URLs
|
|
5692
|
-
*
|
|
5693
|
-
* so callers skip filesystem-only operations.
|
|
6117
|
+
* - Other internal-scheme URLs have no local filesystem path; this returns
|
|
6118
|
+
* `undefined` so callers skip filesystem-only operations.
|
|
5694
6119
|
* - Cwd-relative and absolute paths resolve via `resolveToCwd`.
|
|
5695
6120
|
*/
|
|
5696
6121
|
#resolveSessionFsPath(filePath: string): string | undefined {
|
|
@@ -5698,15 +6123,7 @@ export class AgentSession {
|
|
|
5698
6123
|
if (normalized.startsWith("local:")) {
|
|
5699
6124
|
return resolveLocalUrlToPath(normalized, this.#localProtocolOptions());
|
|
5700
6125
|
}
|
|
5701
|
-
if (
|
|
5702
|
-
normalized.startsWith("agent://") ||
|
|
5703
|
-
normalized.startsWith("skill://") ||
|
|
5704
|
-
normalized.startsWith("rule://") ||
|
|
5705
|
-
normalized.startsWith("mcp://") ||
|
|
5706
|
-
normalized.startsWith("artifact://")
|
|
5707
|
-
) {
|
|
5708
|
-
return undefined;
|
|
5709
|
-
}
|
|
6126
|
+
if (isInternalUrlPath(normalized)) return undefined;
|
|
5710
6127
|
return resolveToCwd(normalized, this.sessionManager.getCwd());
|
|
5711
6128
|
}
|
|
5712
6129
|
|
|
@@ -5870,15 +6287,22 @@ export class AgentSession {
|
|
|
5870
6287
|
return undefined;
|
|
5871
6288
|
}
|
|
5872
6289
|
|
|
5873
|
-
async #emitAgentEndNotification(messages: AgentMessage[]): Promise<void> {
|
|
5874
|
-
await this.#extensionRunner?.emit({
|
|
6290
|
+
async #emitAgentEndNotification(messages: AgentMessage[], options?: { willContinue?: boolean }): Promise<void> {
|
|
6291
|
+
await this.#extensionRunner?.emit({
|
|
6292
|
+
type: "agent_end",
|
|
6293
|
+
messages,
|
|
6294
|
+
willContinue: options?.willContinue,
|
|
6295
|
+
});
|
|
5875
6296
|
}
|
|
5876
6297
|
|
|
6298
|
+
/** @returns true when a hidden session_stop continuation turn was scheduled. */
|
|
5877
6299
|
async #emitSessionStopEvent(
|
|
5878
6300
|
messages: AgentMessage[],
|
|
5879
6301
|
lastAssistantMessage = this.getLastAssistantMessage(),
|
|
5880
|
-
): Promise<
|
|
5881
|
-
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop"))
|
|
6302
|
+
): Promise<boolean> {
|
|
6303
|
+
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop")) {
|
|
6304
|
+
return false;
|
|
6305
|
+
}
|
|
5882
6306
|
const generation = this.#promptGeneration;
|
|
5883
6307
|
const result = await this.#extensionRunner.emitSessionStop({
|
|
5884
6308
|
messages,
|
|
@@ -5890,12 +6314,12 @@ export class AgentSession {
|
|
|
5890
6314
|
});
|
|
5891
6315
|
if (this.#promptGeneration !== generation || this.#abortInProgress || this.#isDisposed) {
|
|
5892
6316
|
this.#resetSessionStopContinuationState();
|
|
5893
|
-
return;
|
|
6317
|
+
return false;
|
|
5894
6318
|
}
|
|
5895
6319
|
const additionalContext = this.#sessionStopContinuationContext(result);
|
|
5896
6320
|
if (!additionalContext) {
|
|
5897
6321
|
this.#resetSessionStopContinuationState();
|
|
5898
|
-
return;
|
|
6322
|
+
return false;
|
|
5899
6323
|
}
|
|
5900
6324
|
if (this.#sessionStopContinuationCount >= SESSION_STOP_CONTINUATION_CAP) {
|
|
5901
6325
|
logger.warn("session_stop continuation cap reached", {
|
|
@@ -5903,7 +6327,7 @@ export class AgentSession {
|
|
|
5903
6327
|
cap: SESSION_STOP_CONTINUATION_CAP,
|
|
5904
6328
|
});
|
|
5905
6329
|
this.#resetSessionStopContinuationState();
|
|
5906
|
-
return;
|
|
6330
|
+
return false;
|
|
5907
6331
|
}
|
|
5908
6332
|
this.#sessionStopContinuationCount++;
|
|
5909
6333
|
this.#sessionStopHookActive = true;
|
|
@@ -5918,6 +6342,7 @@ export class AgentSession {
|
|
|
5918
6342
|
},
|
|
5919
6343
|
true,
|
|
5920
6344
|
);
|
|
6345
|
+
return true;
|
|
5921
6346
|
}
|
|
5922
6347
|
|
|
5923
6348
|
/** Emit extension events based on session events */
|
|
@@ -6190,6 +6615,43 @@ export class AgentSession {
|
|
|
6190
6615
|
await this.refreshBaseSystemPrompt();
|
|
6191
6616
|
}
|
|
6192
6617
|
}
|
|
6618
|
+
/** Run one abortable auto-learn capture outside the primary agent loop. */
|
|
6619
|
+
async runAutolearnCapture(capture: (signal: AbortSignal) => Promise<void>): Promise<void> {
|
|
6620
|
+
if (this.#autolearnCaptureTask || this.#isDisposed) return;
|
|
6621
|
+
const controller = new AbortController();
|
|
6622
|
+
this.#autolearnCaptureAbortController = controller;
|
|
6623
|
+
const task = (async () => {
|
|
6624
|
+
try {
|
|
6625
|
+
await capture(controller.signal);
|
|
6626
|
+
} catch (error) {
|
|
6627
|
+
if (!controller.signal.aborted) throw error;
|
|
6628
|
+
} finally {
|
|
6629
|
+
if (this.#autolearnCaptureAbortController === controller) {
|
|
6630
|
+
this.#autolearnCaptureAbortController = undefined;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
})();
|
|
6634
|
+
this.#autolearnCaptureTask = task;
|
|
6635
|
+
try {
|
|
6636
|
+
await task;
|
|
6637
|
+
} finally {
|
|
6638
|
+
if (this.#autolearnCaptureTask === task) this.#autolearnCaptureTask = undefined;
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
|
|
6642
|
+
#abortAutolearnCapture(): void {
|
|
6643
|
+
this.#autolearnCaptureAbortController?.abort();
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
async #drainAutolearnCapture(): Promise<void> {
|
|
6647
|
+
const task = this.#autolearnCaptureTask;
|
|
6648
|
+
if (!task) return;
|
|
6649
|
+
try {
|
|
6650
|
+
await withTimeout(task, 3_000, "Timed out draining auto-learn capture during dispose");
|
|
6651
|
+
} catch (error) {
|
|
6652
|
+
logger.warn("Auto-learn capture did not settle during dispose", { error: String(error) });
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6193
6655
|
|
|
6194
6656
|
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
6195
6657
|
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
@@ -6212,6 +6674,8 @@ export class AgentSession {
|
|
|
6212
6674
|
*/
|
|
6213
6675
|
beginDispose(): void {
|
|
6214
6676
|
this.#isDisposed = true;
|
|
6677
|
+
this.#titleGenerationAbortController.abort();
|
|
6678
|
+
this.#abortAutolearnCapture();
|
|
6215
6679
|
this.#flushPendingIrcAsides();
|
|
6216
6680
|
this.yieldQueue.clear();
|
|
6217
6681
|
this.agent.setAsideMessageProvider(undefined);
|
|
@@ -6248,6 +6712,12 @@ export class AgentSession {
|
|
|
6248
6712
|
} catch (error) {
|
|
6249
6713
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6250
6714
|
}
|
|
6715
|
+
// Clear any timers extensions scheduled via `ctx.setInterval`/`ctx.setTimeout`
|
|
6716
|
+
// so their background work does not outlive the session (issue #5664).
|
|
6717
|
+
// Optional-called: hosts and tests may inject partial runner facades that
|
|
6718
|
+
// implement only the dispatch surface.
|
|
6719
|
+
this.#extensionRunner?.clearManagedTimers?.();
|
|
6720
|
+
this.#fallbackExtensionTimers?.clearAll();
|
|
6251
6721
|
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
6252
6722
|
// deferred-handoff task that has already advanced into
|
|
6253
6723
|
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
@@ -6265,6 +6735,7 @@ export class AgentSession {
|
|
|
6265
6735
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
6266
6736
|
this.agent.abort();
|
|
6267
6737
|
await postPromptDrain;
|
|
6738
|
+
await this.#drainAutolearnCapture();
|
|
6268
6739
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
6269
6740
|
// leak its background bash/task work into the parent's manager. Only
|
|
6270
6741
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -6739,50 +7210,99 @@ export class AgentSession {
|
|
|
6739
7210
|
|
|
6740
7211
|
async #applyActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6741
7212
|
toolNames = normalizeToolNames(toolNames);
|
|
7213
|
+
const selectedTools = toolNames.flatMap(name => {
|
|
7214
|
+
const tool = this.#toolRegistry.get(name);
|
|
7215
|
+
return tool ? [{ name, tool }] : [];
|
|
7216
|
+
});
|
|
7217
|
+
const xdevReadAvailable = this.#builtInToolNames.has("read") && selectedTools.some(({ name }) => name === "read");
|
|
7218
|
+
const isPresentationPinned = (name: string): boolean =>
|
|
7219
|
+
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
7220
|
+
const mountCandidates = selectedTools.filter(
|
|
7221
|
+
({ name, tool }) =>
|
|
7222
|
+
this.#xdevRegistry !== undefined &&
|
|
7223
|
+
xdevReadAvailable &&
|
|
7224
|
+
!isPresentationPinned(name) &&
|
|
7225
|
+
isMountableUnderXdev(tool),
|
|
7226
|
+
);
|
|
7227
|
+
|
|
7228
|
+
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
7229
|
+
if (mountCandidates.length > 0 && !builtInWriteAvailable) {
|
|
7230
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
7231
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
7232
|
+
}
|
|
7233
|
+
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
6742
7234
|
const tools: AgentTool[] = [];
|
|
6743
7235
|
const validToolNames: string[] = [];
|
|
6744
7236
|
const mountedTools: AgentTool[] = [];
|
|
6745
|
-
for (const name of
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
// Discoverable tools are presented as `xd://` devices (kept out of the
|
|
6749
|
-
// top-level schema) when the transport is active; everything else stays
|
|
6750
|
-
// top-level. `loadMode` decides presentation only — selection is upstream.
|
|
6751
|
-
if (this.#xdevRegistry && isMountableUnderXdev(tool)) {
|
|
6752
|
-
mountedTools.push(tool);
|
|
7237
|
+
for (const { name, tool } of selectedTools) {
|
|
7238
|
+
if (mountNames.has(name)) {
|
|
7239
|
+
mountedTools.push(this.#wrapToolForAcpPermission(tool));
|
|
6753
7240
|
} else {
|
|
6754
7241
|
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
6755
7242
|
validToolNames.push(name);
|
|
6756
7243
|
}
|
|
6757
7244
|
}
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
7245
|
+
|
|
7246
|
+
const pinnedWrite = isPresentationPinned("write");
|
|
7247
|
+
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
7248
|
+
const transportNeeded = mountedTools.length > 0 || activeDeferrableTool || this.#planModeState?.enabled === true;
|
|
7249
|
+
if (transportNeeded && !builtInWriteAvailable) {
|
|
7250
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
7251
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
7252
|
+
}
|
|
7253
|
+
if (transportNeeded && builtInWriteAvailable) {
|
|
7254
|
+
const write = this.#toolRegistry.get("write");
|
|
7255
|
+
if (write && !validToolNames.includes("write")) {
|
|
7256
|
+
tools.push(this.#wrapToolForAcpPermission(write));
|
|
7257
|
+
validToolNames.push("write");
|
|
7258
|
+
}
|
|
7259
|
+
} else if (
|
|
7260
|
+
!pinnedWrite &&
|
|
7261
|
+
(this.#presentationPinnedToolNames !== undefined || this.#runtimeSelectedToolNames !== undefined)
|
|
7262
|
+
) {
|
|
7263
|
+
const writeNameIndex = validToolNames.indexOf("write");
|
|
7264
|
+
if (writeNameIndex >= 0 && this.#builtInToolNames.has("write")) validToolNames.splice(writeNameIndex, 1);
|
|
7265
|
+
const writeToolIndex = tools.findIndex(tool => tool.name === "write" && this.#builtInToolNames.has("write"));
|
|
7266
|
+
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
7267
|
+
}
|
|
7268
|
+
|
|
6761
7269
|
const previousMounted = this.#mountedXdevToolNames;
|
|
7270
|
+
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
7271
|
+
const tool = this.#xdevRegistry?.get(name);
|
|
7272
|
+
return tool ? [tool] : [];
|
|
7273
|
+
});
|
|
7274
|
+
const previousActiveToolNames = this.getActiveToolNames();
|
|
6762
7275
|
this.#mountedXdevToolNames = new Set(mountedTools.map(tool => tool.name));
|
|
6763
7276
|
this.#xdevRegistry?.reconcile(mountedTools);
|
|
6764
|
-
this.#notifyXdevMountDelta(previousMounted);
|
|
6765
7277
|
this.#setActiveToolNames?.(validToolNames);
|
|
6766
|
-
this.agent.setTools(tools);
|
|
6767
7278
|
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
this.#clearInheritedProviderPromptCacheKey();
|
|
7279
|
+
let rebuiltSystemPrompt: string[] | undefined;
|
|
7280
|
+
let rebuiltSignature: string | undefined;
|
|
7281
|
+
try {
|
|
7282
|
+
if (this.#rebuildSystemPrompt) {
|
|
7283
|
+
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
7284
|
+
if (signature !== this.#lastAppliedToolSignature) {
|
|
7285
|
+
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
7286
|
+
rebuiltSystemPrompt = built.systemPrompt;
|
|
7287
|
+
rebuiltSignature = signature;
|
|
6778
7288
|
}
|
|
6779
|
-
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
6780
|
-
this.#baseSystemPrompt = built.systemPrompt;
|
|
6781
|
-
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
6782
|
-
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
6783
|
-
this.#lastAppliedToolSignature = signature;
|
|
6784
|
-
this.#promptModelKey = this.#currentPromptModelKey();
|
|
6785
7289
|
}
|
|
7290
|
+
} catch (error) {
|
|
7291
|
+
this.#mountedXdevToolNames = previousMounted;
|
|
7292
|
+
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
7293
|
+
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
7294
|
+
throw error;
|
|
7295
|
+
}
|
|
7296
|
+
|
|
7297
|
+
this.#notifyXdevMountDelta(previousMounted);
|
|
7298
|
+
this.agent.setTools(tools);
|
|
7299
|
+
if (rebuiltSystemPrompt && rebuiltSignature) {
|
|
7300
|
+
if (this.#lastAppliedToolSignature !== undefined) this.#clearInheritedProviderPromptCacheKey();
|
|
7301
|
+
this.#baseSystemPrompt = rebuiltSystemPrompt;
|
|
7302
|
+
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
7303
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
7304
|
+
this.#lastAppliedToolSignature = rebuiltSignature;
|
|
7305
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
6786
7306
|
}
|
|
6787
7307
|
}
|
|
6788
7308
|
|
|
@@ -6811,12 +7331,41 @@ export class AgentSession {
|
|
|
6811
7331
|
display: false,
|
|
6812
7332
|
timestamp: Date.now(),
|
|
6813
7333
|
});
|
|
7334
|
+
if (this.settings.get("startup.quiet")) return;
|
|
6814
7335
|
const parts: string[] = [];
|
|
6815
7336
|
if (added.length > 0) parts.push(`mounted ${added.map(entry => entry.name).join(", ")}`);
|
|
6816
7337
|
if (removed.length > 0) parts.push(`unmounted ${removed.map(entry => entry.name).join(", ")}`);
|
|
6817
7338
|
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
6818
7339
|
}
|
|
6819
7340
|
|
|
7341
|
+
/**
|
|
7342
|
+
* Rediscover disk-backed skills and rebuild prompt-facing state without
|
|
7343
|
+
* recreating the session. Explicit skill snapshots (`--no-skills`,
|
|
7344
|
+
* SDK-provided `skills`) remain fixed for the lifetime of the session.
|
|
7345
|
+
*/
|
|
7346
|
+
async refreshSkills(): Promise<void> {
|
|
7347
|
+
if (!this.#skillsReloadable) {
|
|
7348
|
+
return;
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
resetCapabilities();
|
|
7352
|
+
const skillsSettings = this.settings.getGroup("skills");
|
|
7353
|
+
const discovered = await loadSkills({
|
|
7354
|
+
...skillsSettings,
|
|
7355
|
+
cwd: this.sessionManager.getCwd(),
|
|
7356
|
+
disabledExtensions: this.settings.get("disabledExtensions") ?? [],
|
|
7357
|
+
});
|
|
7358
|
+
this.#skills = discovered.skills;
|
|
7359
|
+
this.#skillWarnings = discovered.warnings;
|
|
7360
|
+
this.#skillsSettings = skillsSettings;
|
|
7361
|
+
|
|
7362
|
+
if (this.#agentKind === "main") {
|
|
7363
|
+
setActiveSkills(this.#skills);
|
|
7364
|
+
}
|
|
7365
|
+
await this.refreshBaseSystemPrompt();
|
|
7366
|
+
this.#notifyCommandMetadataChanged();
|
|
7367
|
+
}
|
|
7368
|
+
|
|
6820
7369
|
/**
|
|
6821
7370
|
* Set active tools by name.
|
|
6822
7371
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
@@ -6824,7 +7373,24 @@ export class AgentSession {
|
|
|
6824
7373
|
* Changes take effect before the next model call.
|
|
6825
7374
|
*/
|
|
6826
7375
|
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6827
|
-
|
|
7376
|
+
const mounted = this.#mountedXdevToolNames;
|
|
7377
|
+
const normalized = normalizeToolNames(toolNames);
|
|
7378
|
+
const transportWriteActive =
|
|
7379
|
+
this.#builtInToolNames.has("write") &&
|
|
7380
|
+
this.getActiveToolNames().includes("write") &&
|
|
7381
|
+
this.#presentationPinnedToolNames?.has("write") !== true &&
|
|
7382
|
+
this.#runtimeSelectedToolNames?.has("write") !== true &&
|
|
7383
|
+
(mounted.size > 0 || this.#planModeState?.enabled === true);
|
|
7384
|
+
const previousRuntimeSelectedToolNames = this.#runtimeSelectedToolNames;
|
|
7385
|
+
this.#runtimeSelectedToolNames = new Set(
|
|
7386
|
+
normalized.filter(name => !mounted.has(name) && !(name === "write" && transportWriteActive)),
|
|
7387
|
+
);
|
|
7388
|
+
try {
|
|
7389
|
+
await this.#applyActiveToolsByName(normalized);
|
|
7390
|
+
} catch (error) {
|
|
7391
|
+
this.#runtimeSelectedToolNames = previousRuntimeSelectedToolNames;
|
|
7392
|
+
throw error;
|
|
7393
|
+
}
|
|
6828
7394
|
}
|
|
6829
7395
|
|
|
6830
7396
|
/** Rebuild the base system prompt using the current active tool set. */
|
|
@@ -6967,6 +7533,12 @@ export class AgentSession {
|
|
|
6967
7533
|
*/
|
|
6968
7534
|
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
6969
7535
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
7536
|
+
const previousMcpTools = new Map(
|
|
7537
|
+
existingNames.flatMap(name => {
|
|
7538
|
+
const tool = this.#toolRegistry.get(name);
|
|
7539
|
+
return isMCPToolName(name) && tool ? [[name, tool] as const] : [];
|
|
7540
|
+
}),
|
|
7541
|
+
);
|
|
6970
7542
|
for (const name of existingNames) {
|
|
6971
7543
|
if (isMCPToolName(name)) {
|
|
6972
7544
|
this.#toolRegistry.delete(name);
|
|
@@ -6994,10 +7566,18 @@ export class AgentSession {
|
|
|
6994
7566
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
6995
7567
|
}
|
|
6996
7568
|
|
|
6997
|
-
// Every connected MCP tool is
|
|
6998
|
-
//
|
|
7569
|
+
// Every connected MCP tool is selected; centralized repartitioning owns
|
|
7570
|
+
// presentation pins and write-transport activation/removal.
|
|
6999
7571
|
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...mcpTools.map(tool => tool.name)])];
|
|
7000
|
-
|
|
7572
|
+
try {
|
|
7573
|
+
await this.#applyActiveToolsByName(nextActive);
|
|
7574
|
+
} catch (error) {
|
|
7575
|
+
for (const name of this.#toolRegistry.keys()) {
|
|
7576
|
+
if (isMCPToolName(name)) this.#toolRegistry.delete(name);
|
|
7577
|
+
}
|
|
7578
|
+
for (const [name, tool] of previousMcpTools) this.#toolRegistry.set(name, tool);
|
|
7579
|
+
throw error;
|
|
7580
|
+
}
|
|
7001
7581
|
}
|
|
7002
7582
|
|
|
7003
7583
|
/**
|
|
@@ -7018,6 +7598,12 @@ export class AgentSession {
|
|
|
7018
7598
|
|
|
7019
7599
|
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
7020
7600
|
const previousActiveToolNames = this.getEnabledToolNames();
|
|
7601
|
+
const previousRpcHostTools = new Map(
|
|
7602
|
+
[...previousRpcHostToolNames].flatMap(name => {
|
|
7603
|
+
const tool = this.#toolRegistry.get(name);
|
|
7604
|
+
return tool ? [[name, tool] as const] : [];
|
|
7605
|
+
}),
|
|
7606
|
+
);
|
|
7021
7607
|
for (const name of previousRpcHostToolNames) {
|
|
7022
7608
|
this.#toolRegistry.delete(name);
|
|
7023
7609
|
}
|
|
@@ -7039,9 +7625,16 @@ export class AgentSession {
|
|
|
7039
7625
|
const autoActivatedRpcToolNames = rpcTools
|
|
7040
7626
|
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
7041
7627
|
.map(tool => tool.name);
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7628
|
+
try {
|
|
7629
|
+
await this.#applyActiveToolsByName(
|
|
7630
|
+
Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
|
|
7631
|
+
);
|
|
7632
|
+
} catch (error) {
|
|
7633
|
+
for (const name of this.#rpcHostToolNames) this.#toolRegistry.delete(name);
|
|
7634
|
+
this.#rpcHostToolNames = previousRpcHostToolNames;
|
|
7635
|
+
for (const [name, tool] of previousRpcHostTools) this.#toolRegistry.set(name, tool);
|
|
7636
|
+
throw error;
|
|
7637
|
+
}
|
|
7045
7638
|
}
|
|
7046
7639
|
|
|
7047
7640
|
/** Whether auto-compaction is currently running */
|
|
@@ -7369,6 +7962,11 @@ export class AgentSession {
|
|
|
7369
7962
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
7370
7963
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
7371
7964
|
this.agent.setTools(activeTools);
|
|
7965
|
+
const mountedTools = [...this.#mountedXdevToolNames]
|
|
7966
|
+
.map(name => this.#toolRegistry.get(name))
|
|
7967
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
7968
|
+
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
7969
|
+
this.#xdevRegistry?.reconcile(mountedTools);
|
|
7372
7970
|
}
|
|
7373
7971
|
|
|
7374
7972
|
#clearCheckpointRuntimeState(): void {
|
|
@@ -8030,7 +8628,7 @@ export class AgentSession {
|
|
|
8030
8628
|
const generation = this.#promptGeneration;
|
|
8031
8629
|
try {
|
|
8032
8630
|
// Flush any pending bash messages before the new prompt
|
|
8033
|
-
this.#flushPendingBashMessages();
|
|
8631
|
+
await this.#flushPendingBashMessages();
|
|
8034
8632
|
this.#flushPendingPythonMessages();
|
|
8035
8633
|
this.#flushPendingIrcAsides();
|
|
8036
8634
|
|
|
@@ -8264,8 +8862,11 @@ export class AgentSession {
|
|
|
8264
8862
|
},
|
|
8265
8863
|
hasPendingMessages: () => this.queuedMessageCount > 0,
|
|
8266
8864
|
shutdown: () => {
|
|
8267
|
-
|
|
8268
|
-
|
|
8865
|
+
// Await the idempotent dispose() before exiting so the browser
|
|
8866
|
+
// reaper and other bounded teardown complete — a fire-and-forget
|
|
8867
|
+
// `void this.dispose()` raced process.exit() and could leave an
|
|
8868
|
+
// OMP-owned Chromium alive (#5643).
|
|
8869
|
+
void this.dispose().finally(() => process.exit(0));
|
|
8269
8870
|
},
|
|
8270
8871
|
getContextUsage: () => this.getContextUsage(),
|
|
8271
8872
|
waitForIdle: () => this.waitForIdle(),
|
|
@@ -8301,9 +8902,20 @@ export class AgentSession {
|
|
|
8301
8902
|
await this.reload();
|
|
8302
8903
|
},
|
|
8303
8904
|
getSystemPrompt: () => this.systemPrompt,
|
|
8905
|
+
setInterval: (callback, ms, ...args) => this.#fallbackTimers().setInterval(callback, ms, ...args),
|
|
8906
|
+
setTimeout: (callback, ms, ...args) => this.#fallbackTimers().setTimeout(callback, ms, ...args),
|
|
8907
|
+
clearTimer: timer => this.#fallbackTimers().clear(timer),
|
|
8304
8908
|
};
|
|
8305
8909
|
}
|
|
8306
8910
|
|
|
8911
|
+
/** Lazily create the runner-less command-context timer registry (#5664). */
|
|
8912
|
+
#fallbackTimers(): ManagedTimers {
|
|
8913
|
+
this.#fallbackExtensionTimers ??= new ManagedTimers((event, error) =>
|
|
8914
|
+
logger.warn("Extension timer callback threw", { event, error }),
|
|
8915
|
+
);
|
|
8916
|
+
return this.#fallbackExtensionTimers;
|
|
8917
|
+
}
|
|
8918
|
+
|
|
8307
8919
|
/**
|
|
8308
8920
|
* Try to execute a custom command. Returns the prompt string if found, null otherwise.
|
|
8309
8921
|
* If the command returns void, returns empty string to indicate it was handled.
|
|
@@ -8937,16 +9549,26 @@ export class AgentSession {
|
|
|
8937
9549
|
this.#replanTitleRefreshInFlight = refresh;
|
|
8938
9550
|
}
|
|
8939
9551
|
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9552
|
+
/**
|
|
9553
|
+
* Generate an automatic session title tied to this session's lifecycle.
|
|
9554
|
+
* Input and replan callers share the signal so disposal cancels provider and
|
|
9555
|
+
* local-worker requests instead of leaving background inference alive.
|
|
9556
|
+
*/
|
|
9557
|
+
generateTitle(firstMessage: string): Promise<string | null> {
|
|
9558
|
+
return generateSessionTitle(
|
|
9559
|
+
firstMessage,
|
|
8943
9560
|
this.#modelRegistry,
|
|
8944
9561
|
this.settings,
|
|
8945
|
-
sessionId,
|
|
9562
|
+
this.sessionId,
|
|
8946
9563
|
this.model,
|
|
8947
9564
|
provider => this.agent.metadataForProvider(provider),
|
|
8948
9565
|
this.#titleSystemPrompt,
|
|
9566
|
+
this.#titleGenerationAbortController.signal,
|
|
8949
9567
|
);
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9570
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
9571
|
+
const title = await this.generateTitle(context);
|
|
8950
9572
|
if (!title) return;
|
|
8951
9573
|
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
8952
9574
|
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
@@ -9014,6 +9636,7 @@ export class AgentSession {
|
|
|
9014
9636
|
// auto-starting a fresh turn during cleanup.
|
|
9015
9637
|
this.#abortInProgress = true;
|
|
9016
9638
|
try {
|
|
9639
|
+
this.#abortAutolearnCapture();
|
|
9017
9640
|
this.abortRetry();
|
|
9018
9641
|
this.#promptGeneration++;
|
|
9019
9642
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -9035,6 +9658,7 @@ export class AgentSession {
|
|
|
9035
9658
|
this.agent.abort(options?.reason);
|
|
9036
9659
|
await postPromptDrain;
|
|
9037
9660
|
await this.agent.waitForIdle();
|
|
9661
|
+
await this.#drainAutolearnCapture();
|
|
9038
9662
|
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
9039
9663
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
9040
9664
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
@@ -9094,27 +9718,36 @@ export class AgentSession {
|
|
|
9094
9718
|
await this.abort();
|
|
9095
9719
|
this.#cancelOwnAsyncJobs();
|
|
9096
9720
|
this.#closeAllProviderSessions("new session");
|
|
9097
|
-
this
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9721
|
+
await this.#flushPendingBashMessages();
|
|
9722
|
+
const bashTransition = this.#beginBashSessionTransition({ persistDetached: options?.drop !== true });
|
|
9723
|
+
let sessionTransitioned = false;
|
|
9724
|
+
try {
|
|
9725
|
+
this.agent.reset();
|
|
9726
|
+
if (options?.drop && previousSessionFile) {
|
|
9727
|
+
// Detach the advisor recorder feed and drain its writer BEFORE deleting the
|
|
9728
|
+
// old artifacts dir: `await this.abort()` only stops the primary, so a still-
|
|
9729
|
+
// running advisor turn could otherwise finish, emit `message_end`, and recreate
|
|
9730
|
+
// `<old>/__advisor.jsonl`. #resetAdvisorSessionState (after newSession) re-primes
|
|
9731
|
+
// the advisor and re-attaches the feed at the new session's path.
|
|
9732
|
+
for (const a of this.#advisors) {
|
|
9733
|
+
a.agentUnsubscribe?.();
|
|
9734
|
+
a.agentUnsubscribe = undefined;
|
|
9735
|
+
await a.recorder.close();
|
|
9736
|
+
}
|
|
9737
|
+
try {
|
|
9738
|
+
await this.sessionManager.dropSession(previousSessionFile);
|
|
9739
|
+
} catch (err) {
|
|
9740
|
+
logger.error("Failed to delete session during /drop", { err });
|
|
9741
|
+
}
|
|
9742
|
+
} else {
|
|
9743
|
+
await this.sessionManager.flush();
|
|
9113
9744
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9745
|
+
await this.sessionManager.newSession(options);
|
|
9746
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9747
|
+
sessionTransitioned = true;
|
|
9748
|
+
} finally {
|
|
9749
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
9116
9750
|
}
|
|
9117
|
-
await this.sessionManager.newSession(options);
|
|
9118
9751
|
|
|
9119
9752
|
this.#clearCheckpointRuntimeState();
|
|
9120
9753
|
this.setTodoPhases([]);
|
|
@@ -9180,14 +9813,25 @@ export class AgentSession {
|
|
|
9180
9813
|
}
|
|
9181
9814
|
}
|
|
9182
9815
|
|
|
9816
|
+
await this.#flushPendingBashMessages();
|
|
9183
9817
|
// Flush current session to ensure all entries are written
|
|
9184
9818
|
await this.sessionManager.flush();
|
|
9819
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
9185
9820
|
|
|
9186
9821
|
// Fork the session (creates new session file with same entries)
|
|
9187
|
-
|
|
9822
|
+
let forkResult: { oldSessionFile: string; newSessionFile: string } | undefined;
|
|
9823
|
+
try {
|
|
9824
|
+
forkResult = await this.sessionManager.fork();
|
|
9825
|
+
} catch (error) {
|
|
9826
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
9827
|
+
throw error;
|
|
9828
|
+
}
|
|
9188
9829
|
if (!forkResult) {
|
|
9830
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
9189
9831
|
return false;
|
|
9190
9832
|
}
|
|
9833
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9834
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
9191
9835
|
|
|
9192
9836
|
// Copy artifacts directory if it exists
|
|
9193
9837
|
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
|
@@ -10563,9 +11207,20 @@ export class AgentSession {
|
|
|
10563
11207
|
return undefined;
|
|
10564
11208
|
}
|
|
10565
11209
|
}
|
|
11210
|
+
await this.#flushPendingBashMessages();
|
|
10566
11211
|
await this.sessionManager.flush();
|
|
11212
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
10567
11213
|
this.#cancelOwnAsyncJobs();
|
|
10568
|
-
|
|
11214
|
+
let sessionTransitioned = false;
|
|
11215
|
+
try {
|
|
11216
|
+
await this.sessionManager.newSession(
|
|
11217
|
+
previousSessionFile ? { parentSession: previousSessionFile } : undefined,
|
|
11218
|
+
);
|
|
11219
|
+
this.#markBashSessionTransition(bashTransition);
|
|
11220
|
+
sessionTransitioned = true;
|
|
11221
|
+
} finally {
|
|
11222
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
11223
|
+
}
|
|
10569
11224
|
|
|
10570
11225
|
this.#clearCheckpointRuntimeState();
|
|
10571
11226
|
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
@@ -11025,6 +11680,7 @@ export class AgentSession {
|
|
|
11025
11680
|
autoContinue,
|
|
11026
11681
|
triggerContextTokens: postMaintenanceContextTokens,
|
|
11027
11682
|
phase: "pre_turn",
|
|
11683
|
+
terminalTextAnswer: isTerminalTextAssistantAnswer(assistantMessage),
|
|
11028
11684
|
});
|
|
11029
11685
|
}
|
|
11030
11686
|
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
@@ -11458,11 +12114,13 @@ export class AgentSession {
|
|
|
11458
12114
|
|
|
11459
12115
|
if (!branchEntry) return;
|
|
11460
12116
|
const targetParentId = prunePrompt ? parentEntry.parentId : branchEntry.parentId;
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
12117
|
+
this.#withBashBranchTransition(() => {
|
|
12118
|
+
if (targetParentId === null) {
|
|
12119
|
+
this.sessionManager.resetLeaf();
|
|
12120
|
+
} else {
|
|
12121
|
+
this.sessionManager.branch(targetParentId);
|
|
12122
|
+
}
|
|
12123
|
+
});
|
|
11466
12124
|
this.sessionManager.appendCustomEntry("accepted-terminal-empty-stop");
|
|
11467
12125
|
}
|
|
11468
12126
|
|
|
@@ -11489,11 +12147,13 @@ export class AgentSession {
|
|
|
11489
12147
|
if (!branchEntry) {
|
|
11490
12148
|
return;
|
|
11491
12149
|
}
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
12150
|
+
this.#withBashBranchTransition(() => {
|
|
12151
|
+
if (branchEntry.parentId === null) {
|
|
12152
|
+
this.sessionManager.resetLeaf();
|
|
12153
|
+
} else {
|
|
12154
|
+
this.sessionManager.branch(branchEntry.parentId);
|
|
12155
|
+
}
|
|
12156
|
+
});
|
|
11497
12157
|
}
|
|
11498
12158
|
|
|
11499
12159
|
#isSameAssistantMessage(left: AssistantMessage, right: AssistantMessage): boolean {
|
|
@@ -11530,8 +12190,10 @@ export class AgentSession {
|
|
|
11530
12190
|
if (this.#pendingRewindReport) return this.#pendingRewindReport;
|
|
11531
12191
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11532
12192
|
const message = messages[i];
|
|
11533
|
-
if (message?.role !== "toolResult" || message.
|
|
11534
|
-
const
|
|
12193
|
+
if (message?.role !== "toolResult" || message.isError) continue;
|
|
12194
|
+
const semanticResult = semanticToolResult(message.toolName, message);
|
|
12195
|
+
if (semanticResult?.toolName !== "rewind") continue;
|
|
12196
|
+
const details = semanticResult.details;
|
|
11535
12197
|
const detailReport =
|
|
11536
12198
|
details && typeof details === "object" && "report" in details && typeof details.report === "string"
|
|
11537
12199
|
? details.report.trim()
|
|
@@ -11548,16 +12210,18 @@ export class AgentSession {
|
|
|
11548
12210
|
if (!checkpointState) {
|
|
11549
12211
|
return;
|
|
11550
12212
|
}
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
12213
|
+
this.#withBashBranchTransition(() => {
|
|
12214
|
+
try {
|
|
12215
|
+
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
12216
|
+
startedAt: checkpointState.startedAt,
|
|
12217
|
+
});
|
|
12218
|
+
} catch (error) {
|
|
12219
|
+
logger.warn("Rewind branch checkpoint missing, falling back to root", {
|
|
12220
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12221
|
+
});
|
|
12222
|
+
this.sessionManager.branchWithSummary(null, report, { startedAt: checkpointState.startedAt });
|
|
12223
|
+
}
|
|
12224
|
+
});
|
|
11561
12225
|
|
|
11562
12226
|
const rewoundAt = new Date().toISOString();
|
|
11563
12227
|
const details = { report, startedAt: checkpointState.startedAt, rewoundAt };
|
|
@@ -11572,7 +12236,7 @@ export class AgentSession {
|
|
|
11572
12236
|
|
|
11573
12237
|
if (activeMessages) {
|
|
11574
12238
|
for (const message of activeMessages) {
|
|
11575
|
-
if (message.role === "toolResult" && message.toolName === "rewind") {
|
|
12239
|
+
if (message.role === "toolResult" && semanticToolResult(message.toolName, message)?.toolName === "rewind") {
|
|
11576
12240
|
this.#rewoundToolResultIds.add(message.toolCallId);
|
|
11577
12241
|
}
|
|
11578
12242
|
}
|
|
@@ -12871,12 +13535,15 @@ export class AgentSession {
|
|
|
12871
13535
|
suppressContinuation?: boolean;
|
|
12872
13536
|
suppressHandoff?: boolean;
|
|
12873
13537
|
phase?: CodexCompactionContext["phase"];
|
|
13538
|
+
terminalTextAnswer?: boolean;
|
|
12874
13539
|
} = {},
|
|
12875
13540
|
): Promise<CompactionCheckResult> {
|
|
12876
13541
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
12877
13542
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
12878
13543
|
if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
|
|
12879
13544
|
const generation = this.#promptGeneration;
|
|
13545
|
+
const terminalTextAnswer =
|
|
13546
|
+
options.terminalTextAnswer ?? isTerminalTextAssistantAnswer(this.#findLastAssistantMessage());
|
|
12880
13547
|
const suppressContinuation = options.suppressContinuation === true;
|
|
12881
13548
|
const shouldAutoContinue =
|
|
12882
13549
|
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
@@ -12891,6 +13558,7 @@ export class AgentSession {
|
|
|
12891
13558
|
willRetry,
|
|
12892
13559
|
generation,
|
|
12893
13560
|
shouldAutoContinue,
|
|
13561
|
+
terminalTextAnswer,
|
|
12894
13562
|
options.triggerContextTokens,
|
|
12895
13563
|
suppressContinuation,
|
|
12896
13564
|
);
|
|
@@ -12913,11 +13581,17 @@ export class AgentSession {
|
|
|
12913
13581
|
async signal => {
|
|
12914
13582
|
await Promise.resolve();
|
|
12915
13583
|
if (signal.aborted) return;
|
|
12916
|
-
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13584
|
+
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13585
|
+
...options,
|
|
13586
|
+
terminalTextAnswer,
|
|
13587
|
+
});
|
|
12917
13588
|
},
|
|
12918
13589
|
{ generation },
|
|
12919
13590
|
);
|
|
12920
|
-
return
|
|
13591
|
+
return {
|
|
13592
|
+
...COMPACTION_CHECK_DEFERRED_HANDOFF,
|
|
13593
|
+
continuationScheduled: shouldAutoContinue,
|
|
13594
|
+
};
|
|
12921
13595
|
}
|
|
12922
13596
|
|
|
12923
13597
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
@@ -12984,10 +13658,14 @@ export class AgentSession {
|
|
|
12984
13658
|
aborted: false,
|
|
12985
13659
|
willRetry: false,
|
|
12986
13660
|
});
|
|
12987
|
-
const continuationScheduled =
|
|
12988
|
-
|
|
12989
|
-
this.#
|
|
12990
|
-
|
|
13661
|
+
const continuationScheduled =
|
|
13662
|
+
!autoCompactionSignal.aborted &&
|
|
13663
|
+
this.#scheduleCompactionContinuation({
|
|
13664
|
+
generation,
|
|
13665
|
+
autoContinue: reason !== "idle" && shouldAutoContinue,
|
|
13666
|
+
terminalTextAnswer,
|
|
13667
|
+
suppressContinuation,
|
|
13668
|
+
});
|
|
12991
13669
|
return {
|
|
12992
13670
|
...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
|
|
12993
13671
|
historyRewritten: true,
|
|
@@ -13476,20 +14154,13 @@ export class AgentSession {
|
|
|
13476
14154
|
if (retryFits) {
|
|
13477
14155
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13478
14156
|
continuationScheduled = true;
|
|
13479
|
-
} else
|
|
13480
|
-
this.#
|
|
13481
|
-
continuationScheduled = true;
|
|
13482
|
-
}
|
|
13483
|
-
if (!continuationScheduled && !suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
13484
|
-
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
13485
|
-
// Kick the loop so queued messages are actually delivered. This remains separate
|
|
13486
|
-
// from the no-progress warning: pausing maintenance must not strand user input.
|
|
13487
|
-
this.#scheduleAgentContinue({
|
|
13488
|
-
delayMs: 100,
|
|
14157
|
+
} else {
|
|
14158
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13489
14159
|
generation,
|
|
13490
|
-
|
|
14160
|
+
autoContinue: hasHeadroom && shouldAutoContinue,
|
|
14161
|
+
terminalTextAnswer,
|
|
14162
|
+
suppressContinuation,
|
|
13491
14163
|
});
|
|
13492
|
-
continuationScheduled = true;
|
|
13493
14164
|
}
|
|
13494
14165
|
|
|
13495
14166
|
if (deadEndWarning) {
|
|
@@ -13545,6 +14216,7 @@ export class AgentSession {
|
|
|
13545
14216
|
willRetry: boolean,
|
|
13546
14217
|
generation: number,
|
|
13547
14218
|
autoContinue: boolean,
|
|
14219
|
+
terminalTextAnswer: boolean,
|
|
13548
14220
|
triggerContextTokens?: number,
|
|
13549
14221
|
suppressContinuation = false,
|
|
13550
14222
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
@@ -13627,10 +14299,6 @@ export class AgentSession {
|
|
|
13627
14299
|
});
|
|
13628
14300
|
|
|
13629
14301
|
let continuationScheduled = false;
|
|
13630
|
-
if (!willRetry && reason !== "idle" && autoContinue) {
|
|
13631
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13632
|
-
continuationScheduled = true;
|
|
13633
|
-
}
|
|
13634
14302
|
if (willRetry) {
|
|
13635
14303
|
// The shake rebuild replays every entry, so a trailing error/length
|
|
13636
14304
|
// assistant from the failed turn re-enters agent state — drop it before
|
|
@@ -13646,13 +14314,13 @@ export class AgentSession {
|
|
|
13646
14314
|
}
|
|
13647
14315
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13648
14316
|
continuationScheduled = true;
|
|
13649
|
-
} else
|
|
13650
|
-
this.#
|
|
13651
|
-
delayMs: 100,
|
|
14317
|
+
} else {
|
|
14318
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13652
14319
|
generation,
|
|
13653
|
-
|
|
14320
|
+
autoContinue: reason !== "idle" && autoContinue,
|
|
14321
|
+
terminalTextAnswer,
|
|
14322
|
+
suppressContinuation,
|
|
13654
14323
|
});
|
|
13655
|
-
continuationScheduled = true;
|
|
13656
14324
|
}
|
|
13657
14325
|
if (!reclaimed) {
|
|
13658
14326
|
return willRetry && continuationScheduled
|
|
@@ -13806,6 +14474,11 @@ export class AgentSession {
|
|
|
13806
14474
|
return stopType === "refusal" || stopType === "sensitive";
|
|
13807
14475
|
}
|
|
13808
14476
|
|
|
14477
|
+
/** True when any registered model belongs to `provider`. */
|
|
14478
|
+
#hasProviderModels(provider: string): boolean {
|
|
14479
|
+
return this.#modelRegistry.getAll().some(model => model.provider === provider);
|
|
14480
|
+
}
|
|
14481
|
+
|
|
13809
14482
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
13810
14483
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
13811
14484
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -13836,8 +14509,8 @@ export class AgentSession {
|
|
|
13836
14509
|
const keyKind = isRetryFallbackModelKey(key) ? "model" : "role";
|
|
13837
14510
|
if (keyKind === "model") {
|
|
13838
14511
|
if (isRetryFallbackWildcardKey(key)) {
|
|
13839
|
-
const provider = key
|
|
13840
|
-
if (!this.#
|
|
14512
|
+
const { provider } = parseRetryFallbackWildcard(key, p => this.#hasProviderModels(p));
|
|
14513
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13841
14514
|
const msg = `retry.fallbackChains wildcard key references unknown provider: ${key}`;
|
|
13842
14515
|
logger.warn(msg);
|
|
13843
14516
|
this.configWarnings.push(msg);
|
|
@@ -13869,8 +14542,8 @@ export class AgentSession {
|
|
|
13869
14542
|
continue;
|
|
13870
14543
|
}
|
|
13871
14544
|
if (isRetryFallbackWildcardKey(selectorStr)) {
|
|
13872
|
-
const provider = selectorStr
|
|
13873
|
-
if (!this.#
|
|
14545
|
+
const { provider } = parseRetryFallbackWildcard(selectorStr, p => this.#hasProviderModels(p));
|
|
14546
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13874
14547
|
const msg = `Fallback chain for ${keyKind} '${key}' references unknown provider: ${selectorStr}`;
|
|
13875
14548
|
logger.warn(msg);
|
|
13876
14549
|
this.configWarnings.push(msg);
|
|
@@ -13929,13 +14602,16 @@ export class AgentSession {
|
|
|
13929
14602
|
* Model-oriented keys win over roles so a chain follows the model across
|
|
13930
14603
|
* role reassignments.
|
|
13931
14604
|
*/
|
|
13932
|
-
#resolveRetryFallbackRole(
|
|
14605
|
+
#resolveRetryFallbackRole(
|
|
14606
|
+
currentSelector: string,
|
|
14607
|
+
currentModel: Model | null | undefined = this.model,
|
|
14608
|
+
): string | undefined {
|
|
13933
14609
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13934
14610
|
if (!parsedCurrent) return undefined;
|
|
13935
14611
|
const chains = this.#getRetryFallbackChains();
|
|
13936
14612
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
13937
|
-
const currentPlainSelector =
|
|
13938
|
-
? formatModelSelectorValue(formatModelString(
|
|
14613
|
+
const currentPlainSelector = currentModel
|
|
14614
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
13939
14615
|
: undefined;
|
|
13940
14616
|
const currentPlainBaseSelector =
|
|
13941
14617
|
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -13961,9 +14637,22 @@ export class AgentSession {
|
|
|
13961
14637
|
for (const key of exactModelKeys) {
|
|
13962
14638
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
13963
14639
|
}
|
|
13964
|
-
// 2. Provider
|
|
13965
|
-
|
|
13966
|
-
|
|
14640
|
+
// 2. Provider wildcards — an id-prefixed key (`openrouter/google/*`)
|
|
14641
|
+
// beats the plain `provider/*` key for ids under its prefix.
|
|
14642
|
+
let wildcardMatch: string | undefined;
|
|
14643
|
+
let wildcardPrefixLength = -1;
|
|
14644
|
+
for (const key in chains) {
|
|
14645
|
+
if (!isRetryFallbackWildcardKey(key) || !Array.isArray(chains[key])) continue;
|
|
14646
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(key, p => this.#hasProviderModels(p));
|
|
14647
|
+
if (provider !== parsedCurrent.provider) continue;
|
|
14648
|
+
if (idPrefix !== undefined && !parsedCurrent.id.startsWith(`${idPrefix}/`)) continue;
|
|
14649
|
+
const prefixLength = idPrefix === undefined ? 0 : idPrefix.length;
|
|
14650
|
+
if (prefixLength > wildcardPrefixLength) {
|
|
14651
|
+
wildcardMatch = key;
|
|
14652
|
+
wildcardPrefixLength = prefixLength;
|
|
14653
|
+
}
|
|
14654
|
+
}
|
|
14655
|
+
if (wildcardMatch) return wildcardMatch;
|
|
13967
14656
|
// 3. Role keys — matched by the role's currently-assigned model.
|
|
13968
14657
|
for (const key of roleKeys) {
|
|
13969
14658
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
@@ -13982,9 +14671,11 @@ export class AgentSession {
|
|
|
13982
14671
|
|
|
13983
14672
|
/**
|
|
13984
14673
|
* Parse one configured chain entry. A `provider/*` entry keeps the failing
|
|
13985
|
-
* model's id and swaps the provider (google-antigravity/x → google/x);
|
|
13986
|
-
*
|
|
13987
|
-
*
|
|
14674
|
+
* model's id and swaps the provider (google-antigravity/x → google/x); an
|
|
14675
|
+
* id-prefixed `provider/prefix/*` entry re-prefixes the failing model's
|
|
14676
|
+
* bare id instead (openrouter/google/* : google-antigravity/x →
|
|
14677
|
+
* openrouter/google/x). Ids the target provider lacks are skipped by the
|
|
14678
|
+
* candidate loop's registry lookup.
|
|
13988
14679
|
*/
|
|
13989
14680
|
#parseRetryFallbackChainEntry(
|
|
13990
14681
|
entry: string,
|
|
@@ -13992,8 +14683,23 @@ export class AgentSession {
|
|
|
13992
14683
|
): RetryFallbackSelector | undefined {
|
|
13993
14684
|
if (isRetryFallbackWildcardKey(entry)) {
|
|
13994
14685
|
if (!current) return undefined;
|
|
13995
|
-
const provider = entry
|
|
13996
|
-
|
|
14686
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(entry, p => this.#hasProviderModels(p));
|
|
14687
|
+
const bareId = current.id.slice(current.id.lastIndexOf("/") + 1);
|
|
14688
|
+
let id: string;
|
|
14689
|
+
if (idPrefix !== undefined) {
|
|
14690
|
+
id = `${idPrefix}/${bareId}`;
|
|
14691
|
+
} else if (
|
|
14692
|
+
bareId !== current.id &&
|
|
14693
|
+
!this.#modelRegistry.find(provider, current.id) &&
|
|
14694
|
+
this.#modelRegistry.find(provider, bareId)
|
|
14695
|
+
) {
|
|
14696
|
+
// Aggregator → direct: the failing id carries a vendor prefix the
|
|
14697
|
+
// target provider does not use (openrouter/google/x → google-vertex/x).
|
|
14698
|
+
id = bareId;
|
|
14699
|
+
} else {
|
|
14700
|
+
id = current.id;
|
|
14701
|
+
}
|
|
14702
|
+
return { raw: `${provider}/${id}`, provider, id, thinkingLevel: undefined };
|
|
13997
14703
|
}
|
|
13998
14704
|
return parseRetryFallbackSelector(entry, this.#modelRegistry);
|
|
13999
14705
|
}
|
|
@@ -14026,7 +14732,11 @@ export class AgentSession {
|
|
|
14026
14732
|
return chain;
|
|
14027
14733
|
}
|
|
14028
14734
|
|
|
14029
|
-
#findRetryFallbackCandidates(
|
|
14735
|
+
#findRetryFallbackCandidates(
|
|
14736
|
+
role: string,
|
|
14737
|
+
currentSelector: string,
|
|
14738
|
+
currentModel: Model | null | undefined = this.model,
|
|
14739
|
+
): RetryFallbackSelector[] {
|
|
14030
14740
|
let chain = this.#getRetryFallbackEffectiveChain(role, currentSelector);
|
|
14031
14741
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
14032
14742
|
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
@@ -14050,8 +14760,8 @@ export class AgentSession {
|
|
|
14050
14760
|
if (chain.length <= 1) return [];
|
|
14051
14761
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
14052
14762
|
const currentPlainSelector =
|
|
14053
|
-
|
|
14054
|
-
? formatModelSelectorValue(formatModelString(
|
|
14763
|
+
currentModel && parsedCurrent
|
|
14764
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
14055
14765
|
: undefined;
|
|
14056
14766
|
const currentPlainBaseSelector =
|
|
14057
14767
|
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -14661,17 +15371,187 @@ export class AgentSession {
|
|
|
14661
15371
|
// Bash Execution
|
|
14662
15372
|
// =========================================================================
|
|
14663
15373
|
|
|
14664
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
|
|
15374
|
+
async #saveBashOriginalArtifact(target: BashSessionTarget, originalText: string): Promise<string | undefined> {
|
|
14665
15375
|
try {
|
|
14666
|
-
|
|
15376
|
+
const destination = target.destination ?? (await target.pending);
|
|
15377
|
+
return await destination?.manager.saveArtifact(originalText, "bash-original");
|
|
14667
15378
|
} catch {
|
|
14668
15379
|
return undefined;
|
|
14669
15380
|
}
|
|
14670
15381
|
}
|
|
14671
15382
|
|
|
15383
|
+
#createBashMessage(
|
|
15384
|
+
command: string,
|
|
15385
|
+
result: BashResult,
|
|
15386
|
+
options?: { excludeFromContext?: boolean },
|
|
15387
|
+
): BashExecutionMessage {
|
|
15388
|
+
const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
|
|
15389
|
+
return {
|
|
15390
|
+
role: "bashExecution",
|
|
15391
|
+
command,
|
|
15392
|
+
output: result.output,
|
|
15393
|
+
exitCode: result.exitCode,
|
|
15394
|
+
cancelled: result.cancelled,
|
|
15395
|
+
truncated: result.truncated,
|
|
15396
|
+
meta,
|
|
15397
|
+
timestamp: Date.now(),
|
|
15398
|
+
excludeFromContext: options?.excludeFromContext,
|
|
15399
|
+
};
|
|
15400
|
+
}
|
|
15401
|
+
|
|
15402
|
+
#captureBashSessionTarget(): BashSessionTarget {
|
|
15403
|
+
this.#bashSessionTarget.refs++;
|
|
15404
|
+
return this.#bashSessionTarget;
|
|
15405
|
+
}
|
|
15406
|
+
|
|
15407
|
+
async #releaseBashSessionTarget(target: BashSessionTarget): Promise<void> {
|
|
15408
|
+
if (target.refs <= 0) throw new Error("Bash session target released more than once");
|
|
15409
|
+
target.refs--;
|
|
15410
|
+
if (target.refs === 0 && target.destination?.kind === "detached") {
|
|
15411
|
+
await target.destination.manager.close();
|
|
15412
|
+
}
|
|
15413
|
+
}
|
|
15414
|
+
|
|
15415
|
+
#appendBashMessage(destination: BashAppendDestination, message: BashExecutionMessage): void {
|
|
15416
|
+
switch (destination.kind) {
|
|
15417
|
+
case "current":
|
|
15418
|
+
this.agent.appendMessage(message);
|
|
15419
|
+
destination.manager.appendMessage(message);
|
|
15420
|
+
break;
|
|
15421
|
+
case "detached":
|
|
15422
|
+
destination.manager.appendMessage(message);
|
|
15423
|
+
break;
|
|
15424
|
+
case "branch":
|
|
15425
|
+
destination.parentId = destination.manager.appendMessageToBranch(message, destination.parentId);
|
|
15426
|
+
break;
|
|
15427
|
+
}
|
|
15428
|
+
}
|
|
15429
|
+
|
|
15430
|
+
async #appendOwnedBashMessage(target: BashSessionTarget, message: BashExecutionMessage): Promise<void> {
|
|
15431
|
+
try {
|
|
15432
|
+
const destination = target.destination ?? (await target.pending);
|
|
15433
|
+
if (!destination) throw new Error("Bash session target has no append destination");
|
|
15434
|
+
this.#appendBashMessage(destination, message);
|
|
15435
|
+
} finally {
|
|
15436
|
+
await this.#releaseBashSessionTarget(target);
|
|
15437
|
+
}
|
|
15438
|
+
}
|
|
15439
|
+
|
|
15440
|
+
async #recordBashResultForTarget(
|
|
15441
|
+
target: BashSessionTarget,
|
|
15442
|
+
command: string,
|
|
15443
|
+
result: BashResult,
|
|
15444
|
+
options?: { excludeFromContext?: boolean },
|
|
15445
|
+
): Promise<void> {
|
|
15446
|
+
const message = this.#createBashMessage(command, result, options);
|
|
15447
|
+
if (this.isStreaming && target === this.#bashSessionTarget) {
|
|
15448
|
+
this.#pendingBashMessages.push({ target, message });
|
|
15449
|
+
return;
|
|
15450
|
+
}
|
|
15451
|
+
await this.#appendOwnedBashMessage(target, message);
|
|
15452
|
+
}
|
|
15453
|
+
|
|
15454
|
+
/** Run a leaf rewrite while retaining any in-flight bash on its originating branch. */
|
|
15455
|
+
#withBashBranchTransition<T>(mutate: () => T): T {
|
|
15456
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15457
|
+
let branchTransitioned = false;
|
|
15458
|
+
try {
|
|
15459
|
+
const result = mutate();
|
|
15460
|
+
this.#markBashSessionTransition(bashTransition);
|
|
15461
|
+
branchTransitioned = true;
|
|
15462
|
+
return result;
|
|
15463
|
+
} finally {
|
|
15464
|
+
this.#finishBashSessionTransition(bashTransition, branchTransitioned);
|
|
15465
|
+
}
|
|
15466
|
+
}
|
|
15467
|
+
|
|
14672
15468
|
/**
|
|
14673
|
-
*
|
|
14674
|
-
*
|
|
15469
|
+
* Snapshot the session/branch that owns any in-flight bash before a transition.
|
|
15470
|
+
* When an owner is still active, its target is detached to a clone so a failed
|
|
15471
|
+
* or intentionally dropped transition never redirects the late result.
|
|
15472
|
+
*/
|
|
15473
|
+
#beginBashSessionTransition(options?: { persistDetached?: boolean }): BashSessionTransition {
|
|
15474
|
+
const oldTarget = this.#bashSessionTarget;
|
|
15475
|
+
let detachedManager: SessionManager | undefined;
|
|
15476
|
+
let resolveOld: ((destination: BashAppendDestination) => void) | undefined;
|
|
15477
|
+
if (oldTarget.refs > 0) {
|
|
15478
|
+
detachedManager = this.sessionManager.cloneCurrentSession({ persist: options?.persistDetached });
|
|
15479
|
+
const pendingOld = Promise.withResolvers<BashAppendDestination>();
|
|
15480
|
+
oldTarget.destination = undefined;
|
|
15481
|
+
oldTarget.pending = pendingOld.promise;
|
|
15482
|
+
resolveOld = pendingOld.resolve;
|
|
15483
|
+
}
|
|
15484
|
+
|
|
15485
|
+
const pendingNew = Promise.withResolvers<BashAppendDestination>();
|
|
15486
|
+
return {
|
|
15487
|
+
oldTarget,
|
|
15488
|
+
newTarget: {
|
|
15489
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
15490
|
+
refs: 0,
|
|
15491
|
+
pending: pendingNew.promise,
|
|
15492
|
+
},
|
|
15493
|
+
oldSessionId: this.sessionManager.getSessionId(),
|
|
15494
|
+
oldSessionFile: this.sessionManager.getSessionFile(),
|
|
15495
|
+
oldLeafId: this.sessionManager.getLeafId(),
|
|
15496
|
+
detachedManager,
|
|
15497
|
+
resolveOld,
|
|
15498
|
+
resolveNew: pendingNew.resolve,
|
|
15499
|
+
};
|
|
15500
|
+
}
|
|
15501
|
+
|
|
15502
|
+
/** Adopt the transition's new target as the live bash owner. */
|
|
15503
|
+
#markBashSessionTransition(transition: BashSessionTransition): void {
|
|
15504
|
+
transition.newTarget.sessionId = this.sessionManager.getSessionId();
|
|
15505
|
+
this.#bashSessionTarget = transition.newTarget;
|
|
15506
|
+
}
|
|
15507
|
+
|
|
15508
|
+
/**
|
|
15509
|
+
* Resolve the pending append destinations opened by {@link #beginBashSessionTransition}.
|
|
15510
|
+
* On success the old owner keeps its original session/branch (same file → current or
|
|
15511
|
+
* branch destination; different file → detached clone); on failure both fall back to
|
|
15512
|
+
* the still-current manager and the clone is discarded.
|
|
15513
|
+
*/
|
|
15514
|
+
#finishBashSessionTransition(transition: BashSessionTransition, success: boolean): void {
|
|
15515
|
+
const currentDestination: BashAppendDestination = { kind: "current", manager: this.sessionManager };
|
|
15516
|
+
let oldDestination: BashAppendDestination = currentDestination;
|
|
15517
|
+
if (success && transition.resolveOld) {
|
|
15518
|
+
const currentFile = this.sessionManager.getSessionFile();
|
|
15519
|
+
const sameFile =
|
|
15520
|
+
transition.oldSessionFile === currentFile ||
|
|
15521
|
+
(transition.oldSessionFile !== undefined &&
|
|
15522
|
+
currentFile !== undefined &&
|
|
15523
|
+
path.resolve(transition.oldSessionFile) === path.resolve(currentFile));
|
|
15524
|
+
const sameSession = transition.oldSessionId === this.sessionManager.getSessionId() && sameFile;
|
|
15525
|
+
if (sameSession) {
|
|
15526
|
+
oldDestination =
|
|
15527
|
+
transition.oldLeafId === this.sessionManager.getLeafId()
|
|
15528
|
+
? currentDestination
|
|
15529
|
+
: { kind: "branch", manager: this.sessionManager, parentId: transition.oldLeafId };
|
|
15530
|
+
} else if (transition.detachedManager) {
|
|
15531
|
+
oldDestination = { kind: "detached", manager: transition.detachedManager };
|
|
15532
|
+
}
|
|
15533
|
+
}
|
|
15534
|
+
|
|
15535
|
+
if (transition.resolveOld) {
|
|
15536
|
+
transition.oldTarget.pending = undefined;
|
|
15537
|
+
transition.oldTarget.destination = oldDestination;
|
|
15538
|
+
transition.resolveOld(oldDestination);
|
|
15539
|
+
}
|
|
15540
|
+
|
|
15541
|
+
transition.newTarget.pending = undefined;
|
|
15542
|
+
transition.newTarget.destination = currentDestination;
|
|
15543
|
+
if (!success) transition.newTarget.sessionId = this.sessionManager.getSessionId();
|
|
15544
|
+
transition.resolveNew(currentDestination);
|
|
15545
|
+
|
|
15546
|
+
if (transition.detachedManager && (oldDestination.kind !== "detached" || transition.oldTarget.refs === 0)) {
|
|
15547
|
+
void transition.detachedManager.close().catch(error => {
|
|
15548
|
+
logger.warn("Failed to close detached bash session writer", { error: String(error) });
|
|
15549
|
+
});
|
|
15550
|
+
}
|
|
15551
|
+
}
|
|
15552
|
+
|
|
15553
|
+
/**
|
|
15554
|
+
* Execute a bash command and retain the session/branch that owned its start.
|
|
14675
15555
|
* @param command The bash command to execute
|
|
14676
15556
|
* @param onChunk Optional streaming callback for output
|
|
14677
15557
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
@@ -14682,72 +15562,72 @@ export class AgentSession {
|
|
|
14682
15562
|
onChunk?: (chunk: string) => void,
|
|
14683
15563
|
options?: { excludeFromContext?: boolean; useUserShell?: boolean },
|
|
14684
15564
|
): Promise<BashResult> {
|
|
15565
|
+
const target = this.#captureBashSessionTarget();
|
|
15566
|
+
let targetTransferred = false;
|
|
14685
15567
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
14686
15568
|
const cwd = this.sessionManager.getCwd();
|
|
14687
15569
|
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
15570
|
+
try {
|
|
15571
|
+
if (this.#extensionRunner?.hasHandlers("user_bash")) {
|
|
15572
|
+
const hookResult = await this.#extensionRunner.emitUserBash({
|
|
15573
|
+
type: "user_bash",
|
|
15574
|
+
command,
|
|
15575
|
+
excludeFromContext,
|
|
15576
|
+
cwd,
|
|
15577
|
+
});
|
|
15578
|
+
if (hookResult?.result) {
|
|
15579
|
+
targetTransferred = true;
|
|
15580
|
+
await this.#recordBashResultForTarget(target, command, hookResult.result, options);
|
|
15581
|
+
return hookResult.result;
|
|
15582
|
+
}
|
|
14698
15583
|
}
|
|
14699
|
-
}
|
|
14700
|
-
|
|
14701
|
-
const abortController = new AbortController();
|
|
14702
|
-
this.#bashAbortControllers.add(abortController);
|
|
14703
15584
|
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
15585
|
+
const abortController = new AbortController();
|
|
15586
|
+
this.#bashAbortControllers.add(abortController);
|
|
15587
|
+
let result: BashResult;
|
|
15588
|
+
try {
|
|
15589
|
+
result = await executeBashCommand(command, {
|
|
15590
|
+
onChunk,
|
|
15591
|
+
signal: abortController.signal,
|
|
15592
|
+
sessionKey: target.sessionId,
|
|
15593
|
+
cwd,
|
|
15594
|
+
timeout: clampTimeout("bash") * 1000,
|
|
15595
|
+
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(target, originalText),
|
|
15596
|
+
useUserShell: options?.useUserShell,
|
|
15597
|
+
});
|
|
15598
|
+
} finally {
|
|
15599
|
+
this.#bashAbortControllers.delete(abortController);
|
|
15600
|
+
}
|
|
14714
15601
|
|
|
14715
|
-
|
|
15602
|
+
targetTransferred = true;
|
|
15603
|
+
await this.#recordBashResultForTarget(target, command, result, options);
|
|
14716
15604
|
return result;
|
|
14717
15605
|
} finally {
|
|
14718
|
-
this.#
|
|
15606
|
+
if (!targetTransferred) await this.#releaseBashSessionTarget(target);
|
|
14719
15607
|
}
|
|
14720
15608
|
}
|
|
14721
15609
|
|
|
14722
|
-
/**
|
|
14723
|
-
* Record a bash execution result in session history.
|
|
14724
|
-
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
14725
|
-
*/
|
|
15610
|
+
/** Record a bash result supplied outside executeBash in the current ownership scope. */
|
|
14726
15611
|
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {
|
|
14727
|
-
const
|
|
14728
|
-
const
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
cancelled: result.cancelled,
|
|
14734
|
-
truncated: result.truncated,
|
|
14735
|
-
meta,
|
|
14736
|
-
timestamp: Date.now(),
|
|
14737
|
-
excludeFromContext: options?.excludeFromContext,
|
|
14738
|
-
};
|
|
14739
|
-
|
|
14740
|
-
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
14741
|
-
if (this.isStreaming) {
|
|
14742
|
-
// Queue for later - will be flushed on agent_end
|
|
14743
|
-
this.#pendingBashMessages.push(bashMessage);
|
|
14744
|
-
} else {
|
|
14745
|
-
// Add to agent state immediately
|
|
14746
|
-
this.agent.appendMessage(bashMessage);
|
|
15612
|
+
const target = this.#captureBashSessionTarget();
|
|
15613
|
+
const message = this.#createBashMessage(command, result, options);
|
|
15614
|
+
if (this.isStreaming && target === this.#bashSessionTarget) {
|
|
15615
|
+
this.#pendingBashMessages.push({ target, message });
|
|
15616
|
+
return;
|
|
15617
|
+
}
|
|
14747
15618
|
|
|
14748
|
-
|
|
14749
|
-
|
|
15619
|
+
if (target.destination) {
|
|
15620
|
+
try {
|
|
15621
|
+
this.#appendBashMessage(target.destination, message);
|
|
15622
|
+
} finally {
|
|
15623
|
+
void this.#releaseBashSessionTarget(target);
|
|
15624
|
+
}
|
|
15625
|
+
return;
|
|
14750
15626
|
}
|
|
15627
|
+
|
|
15628
|
+
void this.#appendOwnedBashMessage(target, message).catch(error => {
|
|
15629
|
+
logger.error("Failed to record bash result in its owning session", { error: String(error) });
|
|
15630
|
+
});
|
|
14751
15631
|
}
|
|
14752
15632
|
|
|
14753
15633
|
/**
|
|
@@ -14769,22 +15649,14 @@ export class AgentSession {
|
|
|
14769
15649
|
return this.#pendingBashMessages.length > 0;
|
|
14770
15650
|
}
|
|
14771
15651
|
|
|
14772
|
-
/**
|
|
14773
|
-
|
|
14774
|
-
* Called after agent turn completes to maintain proper message ordering.
|
|
14775
|
-
*/
|
|
14776
|
-
#flushPendingBashMessages(): void {
|
|
15652
|
+
/** Flush pending bash messages after the active turn without changing their ownership. */
|
|
15653
|
+
async #flushPendingBashMessages(): Promise<void> {
|
|
14777
15654
|
if (this.#pendingBashMessages.length === 0) return;
|
|
14778
|
-
|
|
14779
|
-
for (const bashMessage of this.#pendingBashMessages) {
|
|
14780
|
-
// Add to agent state
|
|
14781
|
-
this.agent.appendMessage(bashMessage);
|
|
14782
|
-
|
|
14783
|
-
// Save to session
|
|
14784
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
14785
|
-
}
|
|
14786
|
-
|
|
15655
|
+
const pending = this.#pendingBashMessages;
|
|
14787
15656
|
this.#pendingBashMessages = [];
|
|
15657
|
+
for (const { target, message } of pending) {
|
|
15658
|
+
await this.#appendOwnedBashMessage(target, message);
|
|
15659
|
+
}
|
|
14788
15660
|
}
|
|
14789
15661
|
|
|
14790
15662
|
// =========================================================================
|
|
@@ -15377,9 +16249,11 @@ export class AgentSession {
|
|
|
15377
16249
|
this.#disconnectFromAgent();
|
|
15378
16250
|
await this.abort({ goalReason: "internal" });
|
|
15379
16251
|
|
|
16252
|
+
await this.#flushPendingBashMessages();
|
|
15380
16253
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
15381
16254
|
await this.sessionManager.flush();
|
|
15382
16255
|
const previousSessionState = this.sessionManager.captureState();
|
|
16256
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15383
16257
|
// Only same-session reloads compare against the prior context to detect
|
|
15384
16258
|
// rollback edits (`#didSessionMessagesChange` below). Building it for a
|
|
15385
16259
|
// different-session switch is a pure waste — and on huge pre-fix sessions
|
|
@@ -15424,6 +16298,7 @@ export class AgentSession {
|
|
|
15424
16298
|
|
|
15425
16299
|
try {
|
|
15426
16300
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
16301
|
+
this.#markBashSessionTransition(bashTransition);
|
|
15427
16302
|
if (switchingToDifferentSession) {
|
|
15428
16303
|
this.#freshProviderSessionId = undefined;
|
|
15429
16304
|
this.#clearInheritedProviderPromptCacheKey();
|
|
@@ -15557,6 +16432,7 @@ export class AgentSession {
|
|
|
15557
16432
|
error: String(error),
|
|
15558
16433
|
});
|
|
15559
16434
|
}
|
|
16435
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
15560
16436
|
return true;
|
|
15561
16437
|
} catch (error) {
|
|
15562
16438
|
this.sessionManager.restoreState(previousSessionState);
|
|
@@ -15588,6 +16464,7 @@ export class AgentSession {
|
|
|
15588
16464
|
this.#syncTodoPhasesFromBranch();
|
|
15589
16465
|
this.#resetAllAdvisorRuntimes();
|
|
15590
16466
|
this.#reconnectToAgent();
|
|
16467
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
15591
16468
|
throw error;
|
|
15592
16469
|
}
|
|
15593
16470
|
}
|
|
@@ -15633,14 +16510,25 @@ export class AgentSession {
|
|
|
15633
16510
|
this.#pendingNextTurnMessages = [];
|
|
15634
16511
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
15635
16512
|
|
|
16513
|
+
await this.#flushPendingBashMessages();
|
|
15636
16514
|
// Flush pending writes before branching
|
|
15637
16515
|
await this.sessionManager.flush();
|
|
16516
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15638
16517
|
this.#cancelOwnAsyncJobs();
|
|
16518
|
+
this.#abortAutolearnCapture();
|
|
16519
|
+
await this.#drainAutolearnCapture();
|
|
15639
16520
|
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
16521
|
+
let sessionTransitioned = false;
|
|
16522
|
+
try {
|
|
16523
|
+
if (!selectedEntry.parentId) {
|
|
16524
|
+
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
16525
|
+
} else {
|
|
16526
|
+
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
16527
|
+
}
|
|
16528
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16529
|
+
sessionTransitioned = true;
|
|
16530
|
+
} finally {
|
|
16531
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
15644
16532
|
}
|
|
15645
16533
|
this.#rehydrateCheckpointRewindState();
|
|
15646
16534
|
this.#syncTodoPhasesFromBranch();
|
|
@@ -15724,10 +16612,21 @@ export class AgentSession {
|
|
|
15724
16612
|
await this.abort({ goalReason: "internal", reason: "branching /btw" });
|
|
15725
16613
|
this.agent.replaceQueues([], []);
|
|
15726
16614
|
}
|
|
16615
|
+
await this.#flushPendingBashMessages();
|
|
15727
16616
|
await this.sessionManager.flush();
|
|
16617
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15728
16618
|
this.#cancelOwnAsyncJobs();
|
|
16619
|
+
this.#abortAutolearnCapture();
|
|
16620
|
+
await this.#drainAutolearnCapture();
|
|
15729
16621
|
|
|
15730
|
-
|
|
16622
|
+
let sessionTransitioned = false;
|
|
16623
|
+
try {
|
|
16624
|
+
this.sessionManager.createBranchedSession(leafId);
|
|
16625
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16626
|
+
sessionTransitioned = true;
|
|
16627
|
+
} finally {
|
|
16628
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
16629
|
+
}
|
|
15731
16630
|
|
|
15732
16631
|
this.#rehydrateCheckpointRewindState();
|
|
15733
16632
|
this.sessionManager.appendMessage({
|
|
@@ -15783,6 +16682,7 @@ export class AgentSession {
|
|
|
15783
16682
|
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
15784
16683
|
sessionContext?: SessionContext;
|
|
15785
16684
|
}> {
|
|
16685
|
+
await this.#flushPendingBashMessages();
|
|
15786
16686
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
15787
16687
|
|
|
15788
16688
|
// No-op if already at target
|
|
@@ -15910,18 +16810,28 @@ export class AgentSession {
|
|
|
15910
16810
|
|
|
15911
16811
|
// Switch leaf (with or without summary)
|
|
15912
16812
|
// Summary is attached at the navigation target position (newLeafId), not the old branch
|
|
16813
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15913
16814
|
let summaryEntry: BranchSummaryEntry | undefined;
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
16815
|
+
let branchTransitioned = false;
|
|
16816
|
+
try {
|
|
16817
|
+
if (summaryText) {
|
|
16818
|
+
// Create summary at target position (can be null for root)
|
|
16819
|
+
const summaryId = this.sessionManager.branchWithSummary(
|
|
16820
|
+
newLeafId,
|
|
16821
|
+
summaryText,
|
|
16822
|
+
summaryDetails,
|
|
16823
|
+
fromExtension,
|
|
16824
|
+
);
|
|
16825
|
+
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
|
|
16826
|
+
} else if (newLeafId === null) {
|
|
16827
|
+
this.sessionManager.resetLeaf();
|
|
16828
|
+
} else {
|
|
16829
|
+
this.sessionManager.branch(newLeafId);
|
|
16830
|
+
}
|
|
16831
|
+
this.#markBashSessionTransition(bashTransition);
|
|
16832
|
+
branchTransitioned = true;
|
|
16833
|
+
} finally {
|
|
16834
|
+
this.#finishBashSessionTransition(bashTransition, branchTransitioned);
|
|
15925
16835
|
}
|
|
15926
16836
|
|
|
15927
16837
|
// Update agent state — build display context to populate agent messages.
|
|
@@ -16664,29 +17574,73 @@ export class AgentSession {
|
|
|
16664
17574
|
return this.#advisors[0]?.agent;
|
|
16665
17575
|
}
|
|
16666
17576
|
|
|
17577
|
+
/**
|
|
17578
|
+
* Lightweight advisor status for the status line: returns just the configured
|
|
17579
|
+
* flag and per-advisor name/status without computing token/cost breakdowns.
|
|
17580
|
+
* Avoids re-tokenizing the advisor transcript on every render frame.
|
|
17581
|
+
*/
|
|
17582
|
+
getAdvisorStatusOverview(): { configured: boolean; advisors: { name: string; status: AdvisorRuntimeStatus }[] } {
|
|
17583
|
+
// Override stale map entries with live runtime status: failureNotified/quotaExhausted
|
|
17584
|
+
// clear on reset() but #advisorStatuses lags until the next build.
|
|
17585
|
+
const liveStatusBySlug = new Map<string, AdvisorRuntimeStatus>();
|
|
17586
|
+
for (const a of this.#advisors) {
|
|
17587
|
+
liveStatusBySlug.set(
|
|
17588
|
+
a.slug,
|
|
17589
|
+
a.runtime.quotaExhausted ? "quota_exhausted" : a.runtime.failureNotified ? "error" : "running",
|
|
17590
|
+
);
|
|
17591
|
+
}
|
|
17592
|
+
const advisors = [...this.#advisorStatuses.entries()].map(([slug, { name, status }]) => ({
|
|
17593
|
+
name,
|
|
17594
|
+
status: liveStatusBySlug.get(slug) ?? status,
|
|
17595
|
+
}));
|
|
17596
|
+
return { configured: this.#advisorEnabled, advisors };
|
|
17597
|
+
}
|
|
16667
17598
|
/**
|
|
16668
17599
|
* Return structured advisor stats for the status command and TUI panel.
|
|
16669
17600
|
*/
|
|
16670
17601
|
getAdvisorStats(): AdvisorStats {
|
|
16671
17602
|
const configured = this.#advisorEnabled;
|
|
16672
|
-
const
|
|
16673
|
-
|
|
17603
|
+
const liveAdvisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
17604
|
+
// Build the complete roster from #advisorStatuses, which already has the
|
|
17605
|
+
// correct de-duped slugs as keys. Live advisors (from #advisors) carry full
|
|
17606
|
+
// token/cost data; disabled/no-model/quota-exhausted advisors appear as
|
|
17607
|
+
// skeleton entries with just name + status so the status line renders a dot.
|
|
17608
|
+
const liveStatBySlug = new Map(this.#advisors.map((a, i) => [a.slug, liveAdvisors[i]]));
|
|
17609
|
+
const roster: PerAdvisorStat[] = [];
|
|
17610
|
+
for (const [slug, entry] of this.#advisorStatuses) {
|
|
17611
|
+
const live = liveStatBySlug.get(slug);
|
|
17612
|
+
if (live) {
|
|
17613
|
+
roster.push(live);
|
|
17614
|
+
} else {
|
|
17615
|
+
roster.push({
|
|
17616
|
+
name: entry.name,
|
|
17617
|
+
status: entry.status,
|
|
17618
|
+
contextWindow: 0,
|
|
17619
|
+
contextTokens: 0,
|
|
17620
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
17621
|
+
cost: 0,
|
|
17622
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
17623
|
+
});
|
|
17624
|
+
}
|
|
17625
|
+
}
|
|
17626
|
+
const active = liveAdvisors.length > 0;
|
|
17627
|
+
if (liveAdvisors.length === 0) {
|
|
16674
17628
|
return {
|
|
16675
17629
|
configured,
|
|
16676
|
-
active
|
|
17630
|
+
active,
|
|
16677
17631
|
contextWindow: 0,
|
|
16678
17632
|
contextTokens: 0,
|
|
16679
17633
|
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
16680
17634
|
cost: 0,
|
|
16681
17635
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
16682
|
-
advisors:
|
|
17636
|
+
advisors: roster,
|
|
16683
17637
|
};
|
|
16684
17638
|
}
|
|
16685
17639
|
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
16686
17640
|
const messages = { user: 0, assistant: 0, total: 0 };
|
|
16687
17641
|
let cost = 0;
|
|
16688
17642
|
let contextTokens = 0;
|
|
16689
|
-
for (const a of
|
|
17643
|
+
for (const a of liveAdvisors) {
|
|
16690
17644
|
tokens.input += a.tokens.input;
|
|
16691
17645
|
tokens.output += a.tokens.output;
|
|
16692
17646
|
tokens.reasoning += a.tokens.reasoning;
|
|
@@ -16703,14 +17657,14 @@ export class AgentSession {
|
|
|
16703
17657
|
// first advisor's so the legacy status line stays byte-identical.
|
|
16704
17658
|
return {
|
|
16705
17659
|
configured,
|
|
16706
|
-
active
|
|
16707
|
-
model:
|
|
16708
|
-
contextWindow:
|
|
17660
|
+
active,
|
|
17661
|
+
model: liveAdvisors[0].model,
|
|
17662
|
+
contextWindow: liveAdvisors[0].contextWindow,
|
|
16709
17663
|
contextTokens,
|
|
16710
17664
|
tokens,
|
|
16711
17665
|
cost,
|
|
16712
17666
|
messages,
|
|
16713
|
-
advisors,
|
|
17667
|
+
advisors: roster,
|
|
16714
17668
|
};
|
|
16715
17669
|
}
|
|
16716
17670
|
|
|
@@ -16744,12 +17698,18 @@ export class AgentSession {
|
|
|
16744
17698
|
}
|
|
16745
17699
|
return {
|
|
16746
17700
|
name: advisor.name,
|
|
17701
|
+
status: advisor.runtime.quotaExhausted
|
|
17702
|
+
? "quota_exhausted"
|
|
17703
|
+
: advisor.runtime.failureNotified
|
|
17704
|
+
? "error"
|
|
17705
|
+
: "running",
|
|
16747
17706
|
model,
|
|
16748
17707
|
contextWindow: model.contextWindow ?? 0,
|
|
16749
17708
|
contextTokens,
|
|
16750
17709
|
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
|
|
16751
17710
|
cost,
|
|
16752
17711
|
messages: { user, assistant, total: messages.length },
|
|
17712
|
+
sessionId: advisor.agent.sessionId,
|
|
16753
17713
|
};
|
|
16754
17714
|
}
|
|
16755
17715
|
|
|
@@ -16758,13 +17718,18 @@ export class AgentSession {
|
|
|
16758
17718
|
*/
|
|
16759
17719
|
formatAdvisorStatus(): string {
|
|
16760
17720
|
const stats = this.getAdvisorStats();
|
|
16761
|
-
if (!stats.active) {
|
|
17721
|
+
if (!stats.active && stats.advisors.length === 0) {
|
|
16762
17722
|
return stats.configured
|
|
16763
17723
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
16764
17724
|
: "Advisor is disabled.";
|
|
16765
17725
|
}
|
|
16766
17726
|
if (stats.advisors.length <= 1) {
|
|
16767
17727
|
const s = stats.advisors[0];
|
|
17728
|
+
if (s && s.status === "no_model") {
|
|
17729
|
+
return stats.configured
|
|
17730
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
17731
|
+
: "Advisor is disabled.";
|
|
17732
|
+
}
|
|
16768
17733
|
const contextLine =
|
|
16769
17734
|
s.contextWindow > 0
|
|
16770
17735
|
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
@@ -16773,6 +17738,7 @@ export class AgentSession {
|
|
|
16773
17738
|
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
16774
17739
|
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
16775
17740
|
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
17741
|
+
if (!s.model || s.status !== "running") return `Advisor "${s.name}" is ${s.status.replace("_", " ")}.`;
|
|
16776
17742
|
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
16777
17743
|
}
|
|
16778
17744
|
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
@@ -16781,7 +17747,9 @@ export class AgentSession {
|
|
|
16781
17747
|
s.contextWindow > 0
|
|
16782
17748
|
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
16783
17749
|
: `${s.contextTokens.toLocaleString()}`;
|
|
16784
|
-
lines.push(
|
|
17750
|
+
lines.push(
|
|
17751
|
+
` • ${s.name}${s.model && s.status === "running" ? ` (${s.model.provider}/${s.model.id})` : ` [${s.status}]`} — context ${ctx} tokens, $${s.cost.toFixed(4)}`,
|
|
17752
|
+
);
|
|
16785
17753
|
}
|
|
16786
17754
|
lines.push(
|
|
16787
17755
|
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
@@ -16790,37 +17758,51 @@ export class AgentSession {
|
|
|
16790
17758
|
}
|
|
16791
17759
|
|
|
16792
17760
|
/**
|
|
16793
|
-
* Estimate the advisor's current context tokens.
|
|
16794
|
-
*
|
|
16795
|
-
*
|
|
16796
|
-
*
|
|
17761
|
+
* Estimate the advisor's current context tokens. A successful provider usage
|
|
17762
|
+
* after the latest advisor compaction is ground truth for the prompt plus its
|
|
17763
|
+
* generated output; only messages after that anchor are estimated. Usage from
|
|
17764
|
+
* retained pre-compaction messages is stale and must not immediately retrigger
|
|
17765
|
+
* maintenance on the newly compacted context.
|
|
16797
17766
|
*/
|
|
16798
17767
|
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
16799
|
-
let
|
|
16800
|
-
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
17768
|
+
let usageAnchorStartIndex = 0;
|
|
16801
17769
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16802
|
-
const
|
|
16803
|
-
if (
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
17770
|
+
const message = messages[i];
|
|
17771
|
+
if (message.role !== "compactionSummary") continue;
|
|
17772
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
17773
|
+
// Advisor summaries created before this runtime-only boundary existed have
|
|
17774
|
+
// no trustworthy way to distinguish retained from newly appended messages.
|
|
17775
|
+
// Conservatively ignore every current assistant until the next compaction.
|
|
17776
|
+
usageAnchorStartIndex = advisorSummary.advisorUsageAnchorStartIndex ?? messages.length;
|
|
17777
|
+
break;
|
|
17778
|
+
}
|
|
17779
|
+
|
|
17780
|
+
let lastUsageIndex: number | undefined;
|
|
17781
|
+
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
17782
|
+
for (let i = messages.length - 1; i >= usageAnchorStartIndex; i--) {
|
|
17783
|
+
const message = messages[i];
|
|
17784
|
+
if (message.role !== "assistant") continue;
|
|
17785
|
+
const assistant = message as AssistantMessage;
|
|
17786
|
+
if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error" && assistant.usage) {
|
|
17787
|
+
lastUsage = assistant.usage;
|
|
17788
|
+
lastUsageIndex = i;
|
|
17789
|
+
break;
|
|
16810
17790
|
}
|
|
16811
17791
|
}
|
|
16812
|
-
|
|
17792
|
+
|
|
17793
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
17794
|
+
if (!lastUsage || lastUsageIndex === undefined) {
|
|
16813
17795
|
let estimated = 0;
|
|
16814
17796
|
for (const message of messages) {
|
|
16815
|
-
estimated += estimateTokens(message);
|
|
17797
|
+
estimated += estimateTokens(message, estimateOptions);
|
|
16816
17798
|
}
|
|
16817
17799
|
return estimated;
|
|
16818
17800
|
}
|
|
16819
17801
|
let trailingTokens = 0;
|
|
16820
17802
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
16821
|
-
trailingTokens += estimateTokens(messages[i]);
|
|
17803
|
+
trailingTokens += estimateTokens(messages[i], estimateOptions);
|
|
16822
17804
|
}
|
|
16823
|
-
return
|
|
17805
|
+
return calculateContextTokens(lastUsage) + trailingTokens;
|
|
16824
17806
|
}
|
|
16825
17807
|
|
|
16826
17808
|
/**
|