@oh-my-pi/pi-coding-agent 17.0.1 → 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 +92 -20
- package/dist/cli.js +3485 -3448
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +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/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/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- 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/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +1 -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/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +42 -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/helpers.ts +28 -5
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +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/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +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/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +139 -54
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +221 -42
- package/src/session/agent-session.ts +1285 -348
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +48 -0
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- 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/essential-tools.ts +45 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +7 -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/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
|
@@ -155,6 +155,7 @@ import {
|
|
|
155
155
|
type AdvisorNote,
|
|
156
156
|
AdvisorOutputQuarantinedError,
|
|
157
157
|
AdvisorRuntime,
|
|
158
|
+
type AdvisorRuntimeStatus,
|
|
158
159
|
type AdvisorSeverity,
|
|
159
160
|
AdvisorTranscriptRecorder,
|
|
160
161
|
advisorTranscriptFilename,
|
|
@@ -197,6 +198,7 @@ import {
|
|
|
197
198
|
onModelRolesChanged,
|
|
198
199
|
validateProviderMaxInFlightRequests,
|
|
199
200
|
} from "../config/settings";
|
|
201
|
+
import { CursorExecHandlers } from "../cursor";
|
|
200
202
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
201
203
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
202
204
|
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
@@ -233,6 +235,7 @@ import type {
|
|
|
233
235
|
TurnEndEvent,
|
|
234
236
|
TurnStartEvent,
|
|
235
237
|
} from "../extensibility/extensions";
|
|
238
|
+
import { ManagedTimers } from "../extensibility/extensions/managed-timers";
|
|
236
239
|
import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
|
|
237
240
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
238
241
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
@@ -252,7 +255,11 @@ import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
|
252
255
|
import { theme } from "../modes/theme/theme";
|
|
253
256
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
254
257
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
255
|
-
import {
|
|
258
|
+
import {
|
|
259
|
+
computeNonMessageBreakdown,
|
|
260
|
+
computeNonMessageTokens,
|
|
261
|
+
estimateToolSchemaTokens,
|
|
262
|
+
} from "../modes/utils/context-usage";
|
|
256
263
|
import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
|
|
257
264
|
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
258
265
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
@@ -393,6 +400,34 @@ import { YieldQueue } from "./yield-queue";
|
|
|
393
400
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
394
401
|
const PLAN_MODE_REMINDER_MAX = 3;
|
|
395
402
|
|
|
403
|
+
type BashAppendDestination =
|
|
404
|
+
| { kind: "current"; manager: SessionManager }
|
|
405
|
+
| { kind: "detached"; manager: SessionManager }
|
|
406
|
+
| { kind: "branch"; manager: SessionManager; parentId: string | null };
|
|
407
|
+
|
|
408
|
+
interface BashSessionTarget {
|
|
409
|
+
sessionId: string;
|
|
410
|
+
refs: number;
|
|
411
|
+
destination?: BashAppendDestination;
|
|
412
|
+
pending?: Promise<BashAppendDestination>;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
interface PendingBashMessage {
|
|
416
|
+
target: BashSessionTarget;
|
|
417
|
+
message: BashExecutionMessage;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface BashSessionTransition {
|
|
421
|
+
oldTarget: BashSessionTarget;
|
|
422
|
+
newTarget: BashSessionTarget;
|
|
423
|
+
oldSessionId: string;
|
|
424
|
+
oldSessionFile: string | undefined;
|
|
425
|
+
oldLeafId: string | null;
|
|
426
|
+
detachedManager: SessionManager | undefined;
|
|
427
|
+
resolveOld: ((destination: BashAppendDestination) => void) | undefined;
|
|
428
|
+
resolveNew: (destination: BashAppendDestination) => void;
|
|
429
|
+
}
|
|
430
|
+
|
|
396
431
|
/**
|
|
397
432
|
* Mutating tool results (`bash`/`eval`/`edit`/`write`/`ast_edit`) without the
|
|
398
433
|
* agent touching the `todo` tool that trip the mid-run reconciliation nudge.
|
|
@@ -531,6 +566,43 @@ function reportFromRewindReportContent(content: string): string {
|
|
|
531
566
|
return report.trim();
|
|
532
567
|
}
|
|
533
568
|
|
|
569
|
+
type SemanticCheckpointToolName = "checkpoint" | "rewind";
|
|
570
|
+
|
|
571
|
+
type SemanticToolResult = {
|
|
572
|
+
toolName: SemanticCheckpointToolName;
|
|
573
|
+
details?: unknown;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Normalize checkpoint/rewind results across native calls and `write xd://`
|
|
578
|
+
* dispatches. Xdev keeps the wrapped tool's result details under `xdev.inner`,
|
|
579
|
+
* while direct calls put them on the result itself.
|
|
580
|
+
*/
|
|
581
|
+
function semanticToolResult(toolName: string | undefined, result: unknown): SemanticToolResult | undefined {
|
|
582
|
+
if (toolName === "checkpoint" || toolName === "rewind") {
|
|
583
|
+
const details = result && typeof result === "object" && "details" in result ? result.details : undefined;
|
|
584
|
+
return { toolName, details };
|
|
585
|
+
}
|
|
586
|
+
const dispatch = writeDeviceDispatch(toolName ?? "", result);
|
|
587
|
+
if (dispatch?.mode !== "execute" || (dispatch.tool !== "checkpoint" && dispatch.tool !== "rewind")) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
return { toolName: dispatch.tool, details: dispatch.inner };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isTodoPhase(value: unknown): value is TodoPhase {
|
|
594
|
+
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
595
|
+
return value.tasks.every(
|
|
596
|
+
task =>
|
|
597
|
+
isRecord(task) &&
|
|
598
|
+
typeof task.content === "string" &&
|
|
599
|
+
(task.status === "pending" ||
|
|
600
|
+
task.status === "in_progress" ||
|
|
601
|
+
task.status === "completed" ||
|
|
602
|
+
task.status === "abandoned"),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
534
606
|
function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | undefined {
|
|
535
607
|
if (entry.type !== "custom_message" || entry.customType !== "rewind-report") return undefined;
|
|
536
608
|
const details = entry.details;
|
|
@@ -543,21 +615,18 @@ function completedRewindFromEntry(entry: SessionEntry): CompletedRewindState | u
|
|
|
543
615
|
reportFromRewindReportContent(customMessageContentText(entry.content));
|
|
544
616
|
return report.length > 0 ? { report, startedAt, rewoundAt } : undefined;
|
|
545
617
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
entry.message.toolName === "checkpoint" &&
|
|
554
|
-
entry.message.isError !== true
|
|
555
|
-
);
|
|
618
|
+
function isSuccessfulCheckpointEntry(
|
|
619
|
+
entry: SessionEntry,
|
|
620
|
+
): entry is SessionEntry & { type: "message"; message: Extract<AgentMessage, { role: "toolResult" }> } {
|
|
621
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.isError === true) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
return semanticToolResult(entry.message.toolName, entry.message)?.toolName === "checkpoint";
|
|
556
625
|
}
|
|
557
626
|
|
|
558
627
|
function checkpointStartedAtFromEntry(entry: SessionEntry): string | undefined {
|
|
559
628
|
if (!isSuccessfulCheckpointEntry(entry)) return undefined;
|
|
560
|
-
const details = entry.message.details;
|
|
629
|
+
const details = semanticToolResult(entry.message.toolName, entry.message)?.details;
|
|
561
630
|
if (details && typeof details === "object") {
|
|
562
631
|
const startedAt = stringProperty(details, "startedAt");
|
|
563
632
|
if (startedAt) return startedAt;
|
|
@@ -671,7 +740,7 @@ const COMPACTION_CHECK_NONE: CompactionCheckResult = {
|
|
|
671
740
|
};
|
|
672
741
|
const COMPACTION_CHECK_DEFERRED_HANDOFF: CompactionCheckResult = {
|
|
673
742
|
deferredHandoff: true,
|
|
674
|
-
continuationScheduled:
|
|
743
|
+
continuationScheduled: false,
|
|
675
744
|
};
|
|
676
745
|
const COMPACTION_CHECK_CONTINUATION: CompactionCheckResult = {
|
|
677
746
|
deferredHandoff: false,
|
|
@@ -843,6 +912,8 @@ export interface AgentSessionConfig {
|
|
|
843
912
|
builtInToolNames?: Iterable<string>;
|
|
844
913
|
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
845
914
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
915
|
+
/** Register the write transport lazily when runtime xdev mounts first need it. */
|
|
916
|
+
ensureWriteRegistered?: () => Promise<boolean>;
|
|
846
917
|
/** Current session pre-LLM message transform pipeline */
|
|
847
918
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
848
919
|
/**
|
|
@@ -889,7 +960,8 @@ export interface AgentSessionConfig {
|
|
|
889
960
|
xdevRegistry?: XdevRegistry;
|
|
890
961
|
/** Discoverable tools mounted under `xd://` in the initial enabled set (startup partition in `sdk.ts`). */
|
|
891
962
|
initialMountedXdevToolNames?: string[];
|
|
892
|
-
|
|
963
|
+
/** Explicit/effective names pinned top-level during runtime repartitioning. */
|
|
964
|
+
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
893
965
|
/**
|
|
894
966
|
* Optional accessor for live MCP server instructions. Read by the session's
|
|
895
967
|
* `rebuildSystemPrompt`-skip optimization to detect server-side instruction
|
|
@@ -1117,15 +1189,20 @@ export interface AdvisorStats {
|
|
|
1117
1189
|
advisors: PerAdvisorStat[];
|
|
1118
1190
|
}
|
|
1119
1191
|
|
|
1120
|
-
/** One advisor's slice of {@link AdvisorStats}
|
|
1192
|
+
/** One advisor's slice of {@link AdvisorStats}. Active advisors carry full
|
|
1193
|
+
* token/cost data; disabled/no-model/quota-exhausted advisors appear with
|
|
1194
|
+
* just `name` + `status` so the status line can render a dot for every
|
|
1195
|
+
* configured advisor. */
|
|
1121
1196
|
export interface PerAdvisorStat {
|
|
1122
1197
|
name: string;
|
|
1123
|
-
|
|
1198
|
+
status: AdvisorRuntimeStatus;
|
|
1199
|
+
model?: Model;
|
|
1124
1200
|
contextWindow: number;
|
|
1125
1201
|
contextTokens: number;
|
|
1126
1202
|
tokens: AdvisorStats["tokens"];
|
|
1127
1203
|
cost: number;
|
|
1128
1204
|
messages: AdvisorStats["messages"];
|
|
1205
|
+
sessionId?: string;
|
|
1129
1206
|
}
|
|
1130
1207
|
|
|
1131
1208
|
/**
|
|
@@ -1134,6 +1211,13 @@ export interface PerAdvisorStat {
|
|
|
1134
1211
|
* primary-scoped state (turn counters, interrupt latches, the shared yield
|
|
1135
1212
|
* channel) stays on the session.
|
|
1136
1213
|
*/
|
|
1214
|
+
interface AdvisorRetryFallbackState {
|
|
1215
|
+
role: string;
|
|
1216
|
+
originalSelector: string;
|
|
1217
|
+
originalThinkingLevel: ThinkingLevel;
|
|
1218
|
+
lastAppliedThinkingLevel: ThinkingLevel;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1137
1221
|
interface ActiveAdvisor {
|
|
1138
1222
|
/** Display name from config ("default" for the legacy no-YAML advisor). */
|
|
1139
1223
|
name: string;
|
|
@@ -1150,10 +1234,23 @@ interface ActiveAdvisor {
|
|
|
1150
1234
|
agentUnsubscribe?: () => void;
|
|
1151
1235
|
model: Model;
|
|
1152
1236
|
thinkingLevel: ThinkingLevel;
|
|
1237
|
+
/** Provider credential/session identity retained across advisor model switches. */
|
|
1238
|
+
providerSessionId: string | undefined;
|
|
1239
|
+
/** Active chain state retained until the configured primary can be restored. */
|
|
1240
|
+
retryFallback?: AdvisorRetryFallbackState;
|
|
1241
|
+
/** A switched advisor model has not yet completed its first successful turn. */
|
|
1242
|
+
retryFallbackPendingSuccess: boolean;
|
|
1153
1243
|
/** Stable key for the resolved runtime inputs that require a rebuild to change. */
|
|
1154
1244
|
signature: string;
|
|
1155
1245
|
}
|
|
1156
1246
|
|
|
1247
|
+
/** Runtime-only advisor compaction metadata. It never enters the model-facing summary text. */
|
|
1248
|
+
interface AdvisorCompactionSummaryMessage extends CompactionSummaryMessage {
|
|
1249
|
+
firstKeptEntryId?: string;
|
|
1250
|
+
/** First message index eligible to anchor provider usage after this compaction. */
|
|
1251
|
+
advisorUsageAnchorStartIndex?: number;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1157
1254
|
/** Resolved advisor config ready to instantiate as an {@link ActiveAdvisor}. */
|
|
1158
1255
|
interface AdvisorRuntimeDescriptor {
|
|
1159
1256
|
config: AdvisorConfig;
|
|
@@ -1229,13 +1326,31 @@ function isRetryFallbackModelKey(key: string): boolean {
|
|
|
1229
1326
|
}
|
|
1230
1327
|
|
|
1231
1328
|
/**
|
|
1232
|
-
* A
|
|
1233
|
-
*
|
|
1329
|
+
* A wildcard fallback-chain key/entry: `provider/*` matches any model of that
|
|
1330
|
+
* provider; an id-prefixed `provider/prefix/*` (e.g. `openrouter/google/*`)
|
|
1331
|
+
* scopes it to ids under that prefix — aggregators namespace model ids by
|
|
1332
|
+
* upstream vendor.
|
|
1234
1333
|
*/
|
|
1235
1334
|
function isRetryFallbackWildcardKey(key: string): boolean {
|
|
1236
1335
|
return key.endsWith("/*");
|
|
1237
1336
|
}
|
|
1238
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* Split a `…/*` wildcard key/entry into its provider and optional id prefix
|
|
1340
|
+
* (`google-vertex/*` → provider only; `openrouter/google/*` → provider
|
|
1341
|
+
* `openrouter`, prefix `google`). A template that names a known provider in
|
|
1342
|
+
* full wins over the split, so provider ids containing `/` keep working.
|
|
1343
|
+
*/
|
|
1344
|
+
function parseRetryFallbackWildcard(
|
|
1345
|
+
key: string,
|
|
1346
|
+
isKnownProvider: (provider: string) => boolean,
|
|
1347
|
+
): { provider: string; idPrefix: string | undefined } {
|
|
1348
|
+
const template = key.slice(0, -2);
|
|
1349
|
+
const slash = template.indexOf("/");
|
|
1350
|
+
if (slash < 0 || isKnownProvider(template)) return { provider: template, idPrefix: undefined };
|
|
1351
|
+
return { provider: template.slice(0, slash), idPrefix: template.slice(slash + 1) };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1239
1354
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1240
1355
|
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
1241
1356
|
}
|
|
@@ -1797,6 +1912,11 @@ export class AgentSession {
|
|
|
1797
1912
|
#advisors: ActiveAdvisor[] = [];
|
|
1798
1913
|
/** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
|
|
1799
1914
|
#advisorConfigs?: AdvisorConfig[];
|
|
1915
|
+
/** Per-advisor runtime status (slug → {name, status}). Tracks disabled/quota/states
|
|
1916
|
+
* for the configured roster even when the advisor has no live runtime. The name
|
|
1917
|
+
* is stored alongside the status so {@link getAdvisorStats} doesn't need to
|
|
1918
|
+
* recompute slugs or resolve config names. */
|
|
1919
|
+
#advisorStatuses: Map<string, { name: string; status: AdvisorRuntimeStatus }> = new Map();
|
|
1800
1920
|
/** Provider-facing UUIDv7 identities keyed by primary provider session and advisor slug. */
|
|
1801
1921
|
#advisorProviderSessionIds = new Map<string, string>();
|
|
1802
1922
|
/** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
|
|
@@ -1860,11 +1980,13 @@ export class AgentSession {
|
|
|
1860
1980
|
* generation path. Refresh via {@link AgentSession.setTitleSystemPrompt} when
|
|
1861
1981
|
* the session cwd changes. */
|
|
1862
1982
|
#titleSystemPrompt: string | undefined;
|
|
1983
|
+
#titleGenerationAbortController = new AbortController();
|
|
1863
1984
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
1864
1985
|
|
|
1865
1986
|
// Bash execution state
|
|
1866
1987
|
#bashAbortControllers = new Set<AbortController>();
|
|
1867
|
-
#pendingBashMessages:
|
|
1988
|
+
#pendingBashMessages: PendingBashMessage[] = [];
|
|
1989
|
+
#bashSessionTarget!: BashSessionTarget;
|
|
1868
1990
|
|
|
1869
1991
|
// Python execution state
|
|
1870
1992
|
#evalAbortControllers = new Set<AbortController>();
|
|
@@ -1898,9 +2020,17 @@ export class AgentSession {
|
|
|
1898
2020
|
#providerSessionId: string | undefined;
|
|
1899
2021
|
#freshProviderSessionId: string | undefined;
|
|
1900
2022
|
#inheritedProviderPromptCacheKey: string | undefined;
|
|
2023
|
+
#autolearnCaptureAbortController: AbortController | undefined;
|
|
2024
|
+
#autolearnCaptureTask: Promise<void> | undefined;
|
|
1901
2025
|
#isDisposed = false;
|
|
1902
2026
|
// Extension system
|
|
1903
2027
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
2028
|
+
/**
|
|
2029
|
+
* Backs `ctx.setInterval`/`setTimeout`/`clearTimer` for the runner-less
|
|
2030
|
+
* command-context fallback (SDK embeddings with no extension runner). Lazily
|
|
2031
|
+
* created; cleared on dispose alongside the runner's own timers (#5664).
|
|
2032
|
+
*/
|
|
2033
|
+
#fallbackExtensionTimers: ManagedTimers | undefined = undefined;
|
|
1904
2034
|
#turnIndex = 0;
|
|
1905
2035
|
#messageEndPersistenceTail: Promise<void> = Promise.resolve();
|
|
1906
2036
|
#pendingMessageEndPersistence = new Map<string, Promise<void>>();
|
|
@@ -1939,8 +2069,10 @@ export class AgentSession {
|
|
|
1939
2069
|
#getLocalCalendarDate: () => string;
|
|
1940
2070
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1941
2071
|
#setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
|
|
2072
|
+
#ensureWriteRegistered: (() => Promise<boolean>) | undefined;
|
|
1942
2073
|
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
|
1943
|
-
#
|
|
2074
|
+
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
2075
|
+
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
1944
2076
|
#baseSystemPrompt: string[];
|
|
1945
2077
|
#baseSystemPromptBeforeMemoryPromotion: string[] | undefined;
|
|
1946
2078
|
/**
|
|
@@ -2431,12 +2563,17 @@ export class AgentSession {
|
|
|
2431
2563
|
readPlan: url => this.#readPlanYoloFile(url),
|
|
2432
2564
|
listPlanFiles: () => this.#listPlanYoloFiles(),
|
|
2433
2565
|
});
|
|
2566
|
+
this.setPlanModeState(undefined);
|
|
2434
2567
|
const previousTools = this.#planYoloPreviousTools;
|
|
2435
|
-
|
|
2436
|
-
|
|
2568
|
+
try {
|
|
2569
|
+
if (previousTools) {
|
|
2570
|
+
await this.setActiveToolsByName(previousTools);
|
|
2571
|
+
}
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
this.setPlanModeState(state);
|
|
2574
|
+
throw error;
|
|
2437
2575
|
}
|
|
2438
2576
|
this.setPlanProposalHandler(null);
|
|
2439
|
-
this.setPlanModeState(undefined);
|
|
2440
2577
|
this.#planYolo = undefined;
|
|
2441
2578
|
this.#planYoloPreviousTools = undefined;
|
|
2442
2579
|
await this.setModelTemporary(planYolo.target, planYolo.thinkingLevel, { ephemeral: true });
|
|
@@ -2494,6 +2631,11 @@ export class AgentSession {
|
|
|
2494
2631
|
constructor(config: AgentSessionConfig) {
|
|
2495
2632
|
this.agent = config.agent;
|
|
2496
2633
|
this.sessionManager = config.sessionManager;
|
|
2634
|
+
this.#bashSessionTarget = {
|
|
2635
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
2636
|
+
refs: 0,
|
|
2637
|
+
destination: { kind: "current", manager: this.sessionManager },
|
|
2638
|
+
};
|
|
2497
2639
|
this.settings = config.settings;
|
|
2498
2640
|
this.#autoApprove = config.autoApprove === true;
|
|
2499
2641
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
@@ -2544,7 +2686,8 @@ export class AgentSession {
|
|
|
2544
2686
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2545
2687
|
this.#createVibeTools = config.createVibeTools;
|
|
2546
2688
|
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
2547
|
-
this.#
|
|
2689
|
+
this.#presentationPinnedToolNames = config.presentationPinnedToolNames;
|
|
2690
|
+
this.#ensureWriteRegistered = config.ensureWriteRegistered;
|
|
2548
2691
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2549
2692
|
this.#transformProviderContext = config.transformProviderContext;
|
|
2550
2693
|
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
@@ -2596,7 +2739,18 @@ export class AgentSession {
|
|
|
2596
2739
|
this.#advisorPrimaryTurnsCompleted++;
|
|
2597
2740
|
if (this.#advisors.length > 0) {
|
|
2598
2741
|
for (const a of this.#advisors) {
|
|
2599
|
-
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
|
+
}
|
|
2600
2754
|
}
|
|
2601
2755
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
2602
2756
|
if (syncBacklog !== "off") {
|
|
@@ -2805,6 +2959,12 @@ export class AgentSession {
|
|
|
2805
2959
|
slug = candidate;
|
|
2806
2960
|
usedSlugs.add(slug);
|
|
2807
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
|
+
}
|
|
2808
2968
|
|
|
2809
2969
|
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
2810
2970
|
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
@@ -2815,6 +2975,7 @@ export class AgentSession {
|
|
|
2815
2975
|
model = resolved.model;
|
|
2816
2976
|
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
2817
2977
|
if (!model) {
|
|
2978
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2818
2979
|
if (emitWarnings) {
|
|
2819
2980
|
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2820
2981
|
}
|
|
@@ -2823,6 +2984,7 @@ export class AgentSession {
|
|
|
2823
2984
|
} else {
|
|
2824
2985
|
const sel = resolveAdvisorRoleSelection(this.settings, this.#modelRegistry.getAvailable());
|
|
2825
2986
|
if (!sel) {
|
|
2987
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
2826
2988
|
if (emitWarnings) {
|
|
2827
2989
|
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2828
2990
|
advisor: config.name,
|
|
@@ -2846,6 +3008,11 @@ export class AgentSession {
|
|
|
2846
3008
|
const requestedLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2847
3009
|
const resolvedLevel = resolveThinkingLevelForModel(model, requestedLevel);
|
|
2848
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" });
|
|
2849
3016
|
descriptors.push({
|
|
2850
3017
|
config,
|
|
2851
3018
|
name: config.name,
|
|
@@ -2881,6 +3048,11 @@ export class AgentSession {
|
|
|
2881
3048
|
if (!this.#advisorEnabled) return false;
|
|
2882
3049
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
2883
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();
|
|
2884
3056
|
const descriptors = this.#resolveAdvisorRuntimeDescriptors(true);
|
|
2885
3057
|
|
|
2886
3058
|
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
@@ -2921,11 +3093,16 @@ export class AgentSession {
|
|
|
2921
3093
|
|
|
2922
3094
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2923
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>();
|
|
2924
3098
|
const availableAdvisorToolNames = new Set<string>();
|
|
2925
|
-
|
|
2926
|
-
for (const tool of tools) {
|
|
3099
|
+
for (const tool of advisorLoopTools) {
|
|
2927
3100
|
availableAdvisorToolNames.add(tool.name);
|
|
2928
|
-
|
|
3101
|
+
advisorToolMap.set(tool.name, tool);
|
|
3102
|
+
if (tool.customWireName !== undefined) {
|
|
3103
|
+
availableAdvisorToolNames.add(tool.customWireName);
|
|
3104
|
+
advisorToolMap.set(tool.customWireName, tool);
|
|
3105
|
+
}
|
|
2929
3106
|
}
|
|
2930
3107
|
let quarantinedAdvisorOutput: string | undefined;
|
|
2931
3108
|
let currentAdvisorInput = "";
|
|
@@ -2965,17 +3142,36 @@ export class AgentSession {
|
|
|
2965
3142
|
// Codex request identity remains UUID-shaped while local labels keep the
|
|
2966
3143
|
// `-advisor` suffix.
|
|
2967
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
|
+
});
|
|
2968
3162
|
const advisorAgent = new Agent({
|
|
2969
3163
|
initialState: {
|
|
2970
3164
|
systemPrompt,
|
|
2971
3165
|
model: advisorModel,
|
|
2972
3166
|
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
2973
|
-
tools:
|
|
3167
|
+
tools: advisorLoopTools,
|
|
2974
3168
|
},
|
|
2975
3169
|
appendOnlyContext,
|
|
2976
3170
|
sessionId: advisorProviderSessionId,
|
|
2977
3171
|
promptCacheKey: advisorPromptCacheKey,
|
|
2978
3172
|
providerSessionState: this.#providerSessionState,
|
|
3173
|
+
cursorExecHandlers: advisorCursorExecHandlers,
|
|
3174
|
+
cwdResolver: () => this.sessionManager.getCwd(),
|
|
2979
3175
|
preferWebsockets: this.#preferWebsockets,
|
|
2980
3176
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
2981
3177
|
streamFn: this.#advisorStreamFn,
|
|
@@ -3046,33 +3242,30 @@ export class AgentSession {
|
|
|
3046
3242
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
3047
3243
|
obfuscator: this.#obfuscator,
|
|
3048
3244
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
3049
|
-
onTurnError:
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
advisorModel.provider,
|
|
3060
|
-
advisorProviderSessionId,
|
|
3061
|
-
{
|
|
3062
|
-
retryAfterMs: extractRetryHint(undefined, message),
|
|
3063
|
-
baseUrl: advisorModel.baseUrl,
|
|
3064
|
-
modelId: advisorModel.id,
|
|
3065
|
-
},
|
|
3066
|
-
);
|
|
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
|
+
});
|
|
3067
3255
|
},
|
|
3068
3256
|
notifyFailure: error => {
|
|
3257
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "error" });
|
|
3069
3258
|
const message = error instanceof Error ? error.message : String(error);
|
|
3070
3259
|
this.emitNotice(
|
|
3071
3260
|
"warning",
|
|
3072
|
-
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(
|
|
3261
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorAgent.state.model)}: ${message}`,
|
|
3073
3262
|
"advisor",
|
|
3074
3263
|
);
|
|
3075
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
|
+
},
|
|
3076
3269
|
});
|
|
3077
3270
|
|
|
3078
3271
|
const advisorRef: ActiveAdvisor = {
|
|
@@ -3086,10 +3279,13 @@ export class AgentSession {
|
|
|
3086
3279
|
recorderClosed: Promise.resolve(),
|
|
3087
3280
|
model: advisorModel,
|
|
3088
3281
|
thinkingLevel: advisorThinkingLevel,
|
|
3282
|
+
providerSessionId: advisorProviderSessionId,
|
|
3283
|
+
retryFallbackPendingSuccess: false,
|
|
3089
3284
|
signature,
|
|
3090
3285
|
};
|
|
3091
3286
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
3092
3287
|
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
3288
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
3093
3289
|
this.#advisors.push(advisorRef);
|
|
3094
3290
|
}
|
|
3095
3291
|
|
|
@@ -3251,6 +3447,163 @@ export class AgentSession {
|
|
|
3251
3447
|
});
|
|
3252
3448
|
}
|
|
3253
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
|
+
|
|
3254
3607
|
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
3255
3608
|
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
3256
3609
|
if (!promotionSettings.enabled) return false;
|
|
@@ -3263,10 +3616,7 @@ export class AgentSession {
|
|
|
3263
3616
|
// keeps its suffix across a promotion); only the model changes.
|
|
3264
3617
|
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
3265
3618
|
try {
|
|
3266
|
-
advisor
|
|
3267
|
-
advisor.agent.setThinkingLevel(toReasoningEffort(advisorThinkingLevel));
|
|
3268
|
-
advisor.agent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
3269
|
-
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3619
|
+
this.#setAdvisorModel(advisor, targetModel, advisorThinkingLevel);
|
|
3270
3620
|
logger.debug("Advisor context promotion switched model on overflow", {
|
|
3271
3621
|
advisor: advisor.name,
|
|
3272
3622
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
@@ -3285,6 +3635,7 @@ export class AgentSession {
|
|
|
3285
3635
|
}
|
|
3286
3636
|
|
|
3287
3637
|
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
3638
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor);
|
|
3288
3639
|
const agent = advisor.agent;
|
|
3289
3640
|
|
|
3290
3641
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -3296,10 +3647,23 @@ export class AgentSession {
|
|
|
3296
3647
|
if (contextWindow <= 0) return false;
|
|
3297
3648
|
|
|
3298
3649
|
const messages = agent.state.messages;
|
|
3299
|
-
|
|
3650
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
3651
|
+
let storedConversationTokens = 0;
|
|
3300
3652
|
for (const message of messages) {
|
|
3301
|
-
|
|
3302
|
-
}
|
|
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);
|
|
3303
3667
|
|
|
3304
3668
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
3305
3669
|
return false;
|
|
@@ -3323,6 +3687,7 @@ export class AgentSession {
|
|
|
3323
3687
|
const timestamp = String(message.timestamp || Date.now());
|
|
3324
3688
|
|
|
3325
3689
|
if (message.role === "compactionSummary") {
|
|
3690
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
3326
3691
|
return {
|
|
3327
3692
|
type: "compaction",
|
|
3328
3693
|
id,
|
|
@@ -3330,9 +3695,7 @@ export class AgentSession {
|
|
|
3330
3695
|
timestamp,
|
|
3331
3696
|
summary: message.summary,
|
|
3332
3697
|
shortSummary: message.shortSummary,
|
|
3333
|
-
firstKeptEntryId:
|
|
3334
|
-
(message as CompactionSummaryMessage & { firstKeptEntryId?: string }).firstKeptEntryId ||
|
|
3335
|
-
`msg-${i + 1}`,
|
|
3698
|
+
firstKeptEntryId: advisorSummary.firstKeptEntryId || `msg-${i + 1}`,
|
|
3336
3699
|
tokensBefore: message.tokensBefore,
|
|
3337
3700
|
} satisfies CompactionEntry;
|
|
3338
3701
|
}
|
|
@@ -3426,11 +3789,15 @@ export class AgentSession {
|
|
|
3426
3789
|
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
3427
3790
|
const tokensBefore = compactResult.tokensBefore;
|
|
3428
3791
|
|
|
3429
|
-
//
|
|
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;
|
|
3430
3796
|
const summaryMessage = {
|
|
3431
3797
|
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
3432
3798
|
firstKeptEntryId,
|
|
3433
|
-
|
|
3799
|
+
advisorUsageAnchorStartIndex,
|
|
3800
|
+
} satisfies AdvisorCompactionSummaryMessage;
|
|
3434
3801
|
|
|
3435
3802
|
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
3436
3803
|
return false;
|
|
@@ -3986,7 +4353,7 @@ export class AgentSession {
|
|
|
3986
4353
|
}
|
|
3987
4354
|
const skipPersistedRewindResult =
|
|
3988
4355
|
message.role === "toolResult" &&
|
|
3989
|
-
message.toolName === "rewind" &&
|
|
4356
|
+
semanticToolResult(message.toolName, message)?.toolName === "rewind" &&
|
|
3990
4357
|
this.#rewoundToolResultIds.delete(message.toolCallId);
|
|
3991
4358
|
if (!skipPersistedRewindResult) {
|
|
3992
4359
|
this.#appendSessionMessage(message);
|
|
@@ -4357,29 +4724,28 @@ export class AgentSession {
|
|
|
4357
4724
|
}
|
|
4358
4725
|
}
|
|
4359
4726
|
if (event.message.role === "toolResult") {
|
|
4360
|
-
const { toolName, toolCallId,
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
isError?: boolean;
|
|
4365
|
-
content?: Array<TextContent | ImageContent>;
|
|
4366
|
-
};
|
|
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;
|
|
4367
4731
|
// A tool actually ran. Clear the post-reminder suppression: the agent did
|
|
4368
4732
|
// productive work in response to the prior nudge, so the next text-only stop
|
|
4369
4733
|
// is allowed to escalate to the next reminder if todos remain incomplete.
|
|
4370
4734
|
this.#todoReminderAwaitingProgress = false;
|
|
4371
4735
|
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
4372
|
-
|
|
4373
|
-
|
|
4736
|
+
const editedPath = details ? getStringProperty(details, "path") : undefined;
|
|
4737
|
+
if (toolName === "edit" && editedPath) {
|
|
4738
|
+
this.#invalidateFileCacheForPath(editedPath);
|
|
4374
4739
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4740
|
+
const phases = details?.phases;
|
|
4741
|
+
if (toolName === "todo" && !isError && details && Array.isArray(phases) && phases.every(isTodoPhase)) {
|
|
4742
|
+
this.setTodoPhases(phases);
|
|
4377
4743
|
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
4378
4744
|
this.#scheduleReplanTitleRefresh();
|
|
4379
4745
|
}
|
|
4380
4746
|
}
|
|
4381
4747
|
if (toolName === "todo" && isError) {
|
|
4382
|
-
const errorText = content
|
|
4748
|
+
const errorText = content.find(part => part.type === "text")?.text;
|
|
4383
4749
|
const reminderText = [
|
|
4384
4750
|
"<system-reminder>",
|
|
4385
4751
|
"todo failed, so todo progress is not visible to the user.",
|
|
@@ -4397,18 +4763,19 @@ export class AgentSession {
|
|
|
4397
4763
|
{ deliverAs: "nextTurn" },
|
|
4398
4764
|
);
|
|
4399
4765
|
}
|
|
4400
|
-
if (toolName === "checkpoint" && !isError) {
|
|
4766
|
+
if (semanticResult?.toolName === "checkpoint" && !isError) {
|
|
4401
4767
|
const checkpointEntryId = this.sessionManager.getEntries().at(-1)?.id ?? null;
|
|
4402
4768
|
this.#checkpointState = {
|
|
4403
4769
|
checkpointMessageCount: this.agent.state.messages.length,
|
|
4404
4770
|
checkpointEntryId,
|
|
4405
|
-
startedAt:
|
|
4771
|
+
startedAt:
|
|
4772
|
+
(semanticDetails && stringProperty(semanticDetails, "startedAt")) ?? new Date().toISOString(),
|
|
4406
4773
|
};
|
|
4407
4774
|
this.#pendingRewindReport = undefined;
|
|
4408
4775
|
this.#lastCompletedRewind = undefined;
|
|
4409
4776
|
}
|
|
4410
|
-
if (toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4411
|
-
const detailReport =
|
|
4777
|
+
if (semanticResult?.toolName === "rewind" && !isError && this.#checkpointState) {
|
|
4778
|
+
const detailReport = semanticDetails ? (stringProperty(semanticDetails, "report")?.trim() ?? "") : "";
|
|
4412
4779
|
const textReport = content?.find(part => part.type === "text")?.text?.trim() ?? "";
|
|
4413
4780
|
const report = detailReport || textReport;
|
|
4414
4781
|
if (report.length > 0) {
|
|
@@ -4421,8 +4788,11 @@ export class AgentSession {
|
|
|
4421
4788
|
// Check auto-retry and auto-compaction after agent completes
|
|
4422
4789
|
if (event.type === "agent_end") {
|
|
4423
4790
|
const settledMessages = this.agent.state.messages;
|
|
4424
|
-
|
|
4425
|
-
|
|
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);
|
|
4426
4796
|
};
|
|
4427
4797
|
const usage = this.getSessionStats().tokens;
|
|
4428
4798
|
await this.#goalRuntime.onAgentEnd({
|
|
@@ -4524,7 +4894,7 @@ export class AgentSession {
|
|
|
4524
4894
|
// active-goal threshold pre-empt below.
|
|
4525
4895
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
4526
4896
|
maintenanceRoute("empty-stop-handled");
|
|
4527
|
-
await emitAgentEndNotification();
|
|
4897
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4528
4898
|
return;
|
|
4529
4899
|
}
|
|
4530
4900
|
|
|
@@ -4547,30 +4917,34 @@ export class AgentSession {
|
|
|
4547
4917
|
automaticContinuationBlocked: compactionResult.automaticContinuationBlocked === true,
|
|
4548
4918
|
});
|
|
4549
4919
|
this.#resolveRetry();
|
|
4550
|
-
await emitAgentEndNotification(
|
|
4920
|
+
await emitAgentEndNotification(
|
|
4921
|
+
compactionResult.continuationScheduled ? { willContinue: true } : undefined,
|
|
4922
|
+
);
|
|
4551
4923
|
return;
|
|
4552
4924
|
}
|
|
4553
4925
|
}
|
|
4554
4926
|
|
|
4555
4927
|
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
4556
4928
|
maintenanceRoute("unexpected-stop-handled");
|
|
4557
|
-
await emitAgentEndNotification();
|
|
4929
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4558
4930
|
return;
|
|
4559
4931
|
}
|
|
4560
4932
|
|
|
4561
4933
|
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
4562
4934
|
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
4563
4935
|
if (didRetry) {
|
|
4564
|
-
await emitAgentEndNotification();
|
|
4936
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4565
4937
|
return;
|
|
4566
4938
|
}
|
|
4567
4939
|
}
|
|
4568
4940
|
|
|
4569
|
-
// 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.
|
|
4570
4944
|
if (msg.stopReason === "aborted") {
|
|
4571
4945
|
this.#resolveRetry();
|
|
4572
4946
|
this.#resetSessionStopContinuationState();
|
|
4573
|
-
await emitAgentEndNotification();
|
|
4947
|
+
await emitAgentEndNotification(ttsrAbortPendingAtAgentEnd ? { willContinue: true } : undefined);
|
|
4574
4948
|
return;
|
|
4575
4949
|
}
|
|
4576
4950
|
// Fireworks Fast variants degrade to their base model on a failed turn —
|
|
@@ -4579,14 +4953,14 @@ export class AgentSession {
|
|
|
4579
4953
|
if (this.#isFireworksFastFallbackEligible(msg)) {
|
|
4580
4954
|
const didRetry = await this.#handleRetryableError(msg, { fireworksFastFallback: true });
|
|
4581
4955
|
if (didRetry) {
|
|
4582
|
-
await emitAgentEndNotification();
|
|
4956
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4583
4957
|
return;
|
|
4584
4958
|
}
|
|
4585
4959
|
}
|
|
4586
4960
|
if (this.#isRetryableError(msg)) {
|
|
4587
4961
|
const didRetry = await this.#handleRetryableError(msg);
|
|
4588
4962
|
if (didRetry) {
|
|
4589
|
-
await emitAgentEndNotification();
|
|
4963
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4590
4964
|
return;
|
|
4591
4965
|
}
|
|
4592
4966
|
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
@@ -4597,7 +4971,7 @@ export class AgentSession {
|
|
|
4597
4971
|
// backoff-retry of the failing model) when no switch happens.
|
|
4598
4972
|
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4599
4973
|
if (didRetry) {
|
|
4600
|
-
await emitAgentEndNotification();
|
|
4974
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4601
4975
|
return;
|
|
4602
4976
|
}
|
|
4603
4977
|
}
|
|
@@ -4636,22 +5010,22 @@ export class AgentSession {
|
|
|
4636
5010
|
compactionResult.continuationScheduled ||
|
|
4637
5011
|
compactionResult.automaticContinuationBlocked
|
|
4638
5012
|
) {
|
|
4639
|
-
await emitAgentEndNotification();
|
|
5013
|
+
await emitAgentEndNotification(compactionResult.continuationScheduled ? { willContinue: true } : undefined);
|
|
4640
5014
|
return;
|
|
4641
5015
|
}
|
|
4642
5016
|
if (msg.stopReason !== "error") {
|
|
4643
5017
|
if (this.#enforceRewindBeforeYield()) {
|
|
4644
|
-
await emitAgentEndNotification();
|
|
5018
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4645
5019
|
return;
|
|
4646
5020
|
}
|
|
4647
5021
|
const planModeContinuationScheduled = await this.#enforcePlanModeDecisionAtSettle();
|
|
4648
5022
|
if (planModeContinuationScheduled) {
|
|
4649
|
-
await emitAgentEndNotification();
|
|
5023
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4650
5024
|
return;
|
|
4651
5025
|
}
|
|
4652
5026
|
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
4653
5027
|
if (todoContinuationScheduled) {
|
|
4654
|
-
await emitAgentEndNotification();
|
|
5028
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4655
5029
|
return;
|
|
4656
5030
|
}
|
|
4657
5031
|
}
|
|
@@ -4661,11 +5035,11 @@ export class AgentSession {
|
|
|
4661
5035
|
// the session is fully idle (the todo reminder above defers the same
|
|
4662
5036
|
// way inside #checkTodoCompletion).
|
|
4663
5037
|
if (this.#hasPendingAsyncWake()) {
|
|
4664
|
-
await emitAgentEndNotification();
|
|
5038
|
+
await emitAgentEndNotification({ willContinue: true });
|
|
4665
5039
|
return;
|
|
4666
5040
|
}
|
|
4667
|
-
await this.#emitSessionStopEvent(settledMessages, msg);
|
|
4668
|
-
await emitAgentEndNotification();
|
|
5041
|
+
const sessionStopWillContinue = await this.#emitSessionStopEvent(settledMessages, msg);
|
|
5042
|
+
await emitAgentEndNotification(sessionStopWillContinue ? { willContinue: true } : undefined);
|
|
4669
5043
|
}
|
|
4670
5044
|
};
|
|
4671
5045
|
|
|
@@ -4795,7 +5169,28 @@ export class AgentSession {
|
|
|
4795
5169
|
);
|
|
4796
5170
|
}
|
|
4797
5171
|
|
|
4798
|
-
#
|
|
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 {
|
|
4799
5194
|
const continuePrompt = async () => {
|
|
4800
5195
|
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
4801
5196
|
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
@@ -4820,10 +5215,18 @@ export class AgentSession {
|
|
|
4820
5215
|
async signal => {
|
|
4821
5216
|
await Promise.resolve();
|
|
4822
5217
|
if (signal.aborted) return;
|
|
5218
|
+
if (this.agent.hasQueuedMessages()) {
|
|
5219
|
+
this.#scheduleAgentContinue({
|
|
5220
|
+
generation,
|
|
5221
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
5222
|
+
});
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
4823
5225
|
await continuePrompt();
|
|
4824
5226
|
},
|
|
4825
5227
|
{ generation },
|
|
4826
5228
|
);
|
|
5229
|
+
return true;
|
|
4827
5230
|
}
|
|
4828
5231
|
|
|
4829
5232
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -5884,15 +6287,22 @@ export class AgentSession {
|
|
|
5884
6287
|
return undefined;
|
|
5885
6288
|
}
|
|
5886
6289
|
|
|
5887
|
-
async #emitAgentEndNotification(messages: AgentMessage[]): Promise<void> {
|
|
5888
|
-
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
|
+
});
|
|
5889
6296
|
}
|
|
5890
6297
|
|
|
6298
|
+
/** @returns true when a hidden session_stop continuation turn was scheduled. */
|
|
5891
6299
|
async #emitSessionStopEvent(
|
|
5892
6300
|
messages: AgentMessage[],
|
|
5893
6301
|
lastAssistantMessage = this.getLastAssistantMessage(),
|
|
5894
|
-
): Promise<
|
|
5895
|
-
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
|
+
}
|
|
5896
6306
|
const generation = this.#promptGeneration;
|
|
5897
6307
|
const result = await this.#extensionRunner.emitSessionStop({
|
|
5898
6308
|
messages,
|
|
@@ -5904,12 +6314,12 @@ export class AgentSession {
|
|
|
5904
6314
|
});
|
|
5905
6315
|
if (this.#promptGeneration !== generation || this.#abortInProgress || this.#isDisposed) {
|
|
5906
6316
|
this.#resetSessionStopContinuationState();
|
|
5907
|
-
return;
|
|
6317
|
+
return false;
|
|
5908
6318
|
}
|
|
5909
6319
|
const additionalContext = this.#sessionStopContinuationContext(result);
|
|
5910
6320
|
if (!additionalContext) {
|
|
5911
6321
|
this.#resetSessionStopContinuationState();
|
|
5912
|
-
return;
|
|
6322
|
+
return false;
|
|
5913
6323
|
}
|
|
5914
6324
|
if (this.#sessionStopContinuationCount >= SESSION_STOP_CONTINUATION_CAP) {
|
|
5915
6325
|
logger.warn("session_stop continuation cap reached", {
|
|
@@ -5917,7 +6327,7 @@ export class AgentSession {
|
|
|
5917
6327
|
cap: SESSION_STOP_CONTINUATION_CAP,
|
|
5918
6328
|
});
|
|
5919
6329
|
this.#resetSessionStopContinuationState();
|
|
5920
|
-
return;
|
|
6330
|
+
return false;
|
|
5921
6331
|
}
|
|
5922
6332
|
this.#sessionStopContinuationCount++;
|
|
5923
6333
|
this.#sessionStopHookActive = true;
|
|
@@ -5932,6 +6342,7 @@ export class AgentSession {
|
|
|
5932
6342
|
},
|
|
5933
6343
|
true,
|
|
5934
6344
|
);
|
|
6345
|
+
return true;
|
|
5935
6346
|
}
|
|
5936
6347
|
|
|
5937
6348
|
/** Emit extension events based on session events */
|
|
@@ -6204,6 +6615,43 @@ export class AgentSession {
|
|
|
6204
6615
|
await this.refreshBaseSystemPrompt();
|
|
6205
6616
|
}
|
|
6206
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
|
+
}
|
|
6207
6655
|
|
|
6208
6656
|
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
6209
6657
|
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
@@ -6226,6 +6674,8 @@ export class AgentSession {
|
|
|
6226
6674
|
*/
|
|
6227
6675
|
beginDispose(): void {
|
|
6228
6676
|
this.#isDisposed = true;
|
|
6677
|
+
this.#titleGenerationAbortController.abort();
|
|
6678
|
+
this.#abortAutolearnCapture();
|
|
6229
6679
|
this.#flushPendingIrcAsides();
|
|
6230
6680
|
this.yieldQueue.clear();
|
|
6231
6681
|
this.agent.setAsideMessageProvider(undefined);
|
|
@@ -6262,6 +6712,12 @@ export class AgentSession {
|
|
|
6262
6712
|
} catch (error) {
|
|
6263
6713
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6264
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();
|
|
6265
6721
|
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
6266
6722
|
// deferred-handoff task that has already advanced into
|
|
6267
6723
|
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
@@ -6279,6 +6735,7 @@ export class AgentSession {
|
|
|
6279
6735
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
6280
6736
|
this.agent.abort();
|
|
6281
6737
|
await postPromptDrain;
|
|
6738
|
+
await this.#drainAutolearnCapture();
|
|
6282
6739
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
6283
6740
|
// leak its background bash/task work into the parent's manager. Only
|
|
6284
6741
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -6753,50 +7210,99 @@ export class AgentSession {
|
|
|
6753
7210
|
|
|
6754
7211
|
async #applyActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6755
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>();
|
|
6756
7234
|
const tools: AgentTool[] = [];
|
|
6757
7235
|
const validToolNames: string[] = [];
|
|
6758
7236
|
const mountedTools: AgentTool[] = [];
|
|
6759
|
-
for (const name of
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
// Discoverable tools are presented as `xd://` devices (kept out of the
|
|
6763
|
-
// top-level schema) when the transport is active; everything else stays
|
|
6764
|
-
// top-level. `loadMode` decides presentation only — selection is upstream.
|
|
6765
|
-
if (this.#xdevRegistry && isMountableUnderXdev(tool)) {
|
|
6766
|
-
mountedTools.push(tool);
|
|
7237
|
+
for (const { name, tool } of selectedTools) {
|
|
7238
|
+
if (mountNames.has(name)) {
|
|
7239
|
+
mountedTools.push(this.#wrapToolForAcpPermission(tool));
|
|
6767
7240
|
} else {
|
|
6768
7241
|
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
6769
7242
|
validToolNames.push(name);
|
|
6770
7243
|
}
|
|
6771
7244
|
}
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
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
|
+
|
|
6775
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();
|
|
6776
7275
|
this.#mountedXdevToolNames = new Set(mountedTools.map(tool => tool.name));
|
|
6777
7276
|
this.#xdevRegistry?.reconcile(mountedTools);
|
|
6778
|
-
this.#notifyXdevMountDelta(previousMounted);
|
|
6779
7277
|
this.#setActiveToolNames?.(validToolNames);
|
|
6780
|
-
this.agent.setTools(tools);
|
|
6781
7278
|
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
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;
|
|
6792
7288
|
}
|
|
6793
|
-
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
6794
|
-
this.#baseSystemPrompt = built.systemPrompt;
|
|
6795
|
-
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
6796
|
-
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
6797
|
-
this.#lastAppliedToolSignature = signature;
|
|
6798
|
-
this.#promptModelKey = this.#currentPromptModelKey();
|
|
6799
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();
|
|
6800
7306
|
}
|
|
6801
7307
|
}
|
|
6802
7308
|
|
|
@@ -6825,6 +7331,7 @@ export class AgentSession {
|
|
|
6825
7331
|
display: false,
|
|
6826
7332
|
timestamp: Date.now(),
|
|
6827
7333
|
});
|
|
7334
|
+
if (this.settings.get("startup.quiet")) return;
|
|
6828
7335
|
const parts: string[] = [];
|
|
6829
7336
|
if (added.length > 0) parts.push(`mounted ${added.map(entry => entry.name).join(", ")}`);
|
|
6830
7337
|
if (removed.length > 0) parts.push(`unmounted ${removed.map(entry => entry.name).join(", ")}`);
|
|
@@ -6866,7 +7373,24 @@ export class AgentSession {
|
|
|
6866
7373
|
* Changes take effect before the next model call.
|
|
6867
7374
|
*/
|
|
6868
7375
|
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
6869
|
-
|
|
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
|
+
}
|
|
6870
7394
|
}
|
|
6871
7395
|
|
|
6872
7396
|
/** Rebuild the base system prompt using the current active tool set. */
|
|
@@ -7009,6 +7533,12 @@ export class AgentSession {
|
|
|
7009
7533
|
*/
|
|
7010
7534
|
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
7011
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
|
+
);
|
|
7012
7542
|
for (const name of existingNames) {
|
|
7013
7543
|
if (isMCPToolName(name)) {
|
|
7014
7544
|
this.#toolRegistry.delete(name);
|
|
@@ -7036,10 +7566,18 @@ export class AgentSession {
|
|
|
7036
7566
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
7037
7567
|
}
|
|
7038
7568
|
|
|
7039
|
-
// Every connected MCP tool is
|
|
7040
|
-
//
|
|
7569
|
+
// Every connected MCP tool is selected; centralized repartitioning owns
|
|
7570
|
+
// presentation pins and write-transport activation/removal.
|
|
7041
7571
|
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...mcpTools.map(tool => tool.name)])];
|
|
7042
|
-
|
|
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
|
+
}
|
|
7043
7581
|
}
|
|
7044
7582
|
|
|
7045
7583
|
/**
|
|
@@ -7060,6 +7598,12 @@ export class AgentSession {
|
|
|
7060
7598
|
|
|
7061
7599
|
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
7062
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
|
+
);
|
|
7063
7607
|
for (const name of previousRpcHostToolNames) {
|
|
7064
7608
|
this.#toolRegistry.delete(name);
|
|
7065
7609
|
}
|
|
@@ -7081,9 +7625,16 @@ export class AgentSession {
|
|
|
7081
7625
|
const autoActivatedRpcToolNames = rpcTools
|
|
7082
7626
|
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
7083
7627
|
.map(tool => tool.name);
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
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
|
+
}
|
|
7087
7638
|
}
|
|
7088
7639
|
|
|
7089
7640
|
/** Whether auto-compaction is currently running */
|
|
@@ -7411,6 +7962,11 @@ export class AgentSession {
|
|
|
7411
7962
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
7412
7963
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
7413
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);
|
|
7414
7970
|
}
|
|
7415
7971
|
|
|
7416
7972
|
#clearCheckpointRuntimeState(): void {
|
|
@@ -8072,7 +8628,7 @@ export class AgentSession {
|
|
|
8072
8628
|
const generation = this.#promptGeneration;
|
|
8073
8629
|
try {
|
|
8074
8630
|
// Flush any pending bash messages before the new prompt
|
|
8075
|
-
this.#flushPendingBashMessages();
|
|
8631
|
+
await this.#flushPendingBashMessages();
|
|
8076
8632
|
this.#flushPendingPythonMessages();
|
|
8077
8633
|
this.#flushPendingIrcAsides();
|
|
8078
8634
|
|
|
@@ -8346,9 +8902,20 @@ export class AgentSession {
|
|
|
8346
8902
|
await this.reload();
|
|
8347
8903
|
},
|
|
8348
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),
|
|
8349
8908
|
};
|
|
8350
8909
|
}
|
|
8351
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
|
+
|
|
8352
8919
|
/**
|
|
8353
8920
|
* Try to execute a custom command. Returns the prompt string if found, null otherwise.
|
|
8354
8921
|
* If the command returns void, returns empty string to indicate it was handled.
|
|
@@ -8982,16 +9549,26 @@ export class AgentSession {
|
|
|
8982
9549
|
this.#replanTitleRefreshInFlight = refresh;
|
|
8983
9550
|
}
|
|
8984
9551
|
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
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,
|
|
8988
9560
|
this.#modelRegistry,
|
|
8989
9561
|
this.settings,
|
|
8990
|
-
sessionId,
|
|
9562
|
+
this.sessionId,
|
|
8991
9563
|
this.model,
|
|
8992
9564
|
provider => this.agent.metadataForProvider(provider),
|
|
8993
9565
|
this.#titleSystemPrompt,
|
|
9566
|
+
this.#titleGenerationAbortController.signal,
|
|
8994
9567
|
);
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9570
|
+
async #refreshTitleAfterReplan(context: string, sessionId: string): Promise<void> {
|
|
9571
|
+
const title = await this.generateTitle(context);
|
|
8995
9572
|
if (!title) return;
|
|
8996
9573
|
if (this.sessionManager.getSessionId() !== sessionId) return;
|
|
8997
9574
|
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
@@ -9059,6 +9636,7 @@ export class AgentSession {
|
|
|
9059
9636
|
// auto-starting a fresh turn during cleanup.
|
|
9060
9637
|
this.#abortInProgress = true;
|
|
9061
9638
|
try {
|
|
9639
|
+
this.#abortAutolearnCapture();
|
|
9062
9640
|
this.abortRetry();
|
|
9063
9641
|
this.#promptGeneration++;
|
|
9064
9642
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -9080,6 +9658,7 @@ export class AgentSession {
|
|
|
9080
9658
|
this.agent.abort(options?.reason);
|
|
9081
9659
|
await postPromptDrain;
|
|
9082
9660
|
await this.agent.waitForIdle();
|
|
9661
|
+
await this.#drainAutolearnCapture();
|
|
9083
9662
|
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
9084
9663
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
9085
9664
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
@@ -9139,27 +9718,36 @@ export class AgentSession {
|
|
|
9139
9718
|
await this.abort();
|
|
9140
9719
|
this.#cancelOwnAsyncJobs();
|
|
9141
9720
|
this.#closeAllProviderSessions("new session");
|
|
9142
|
-
this
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
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();
|
|
9158
9744
|
}
|
|
9159
|
-
|
|
9160
|
-
|
|
9745
|
+
await this.sessionManager.newSession(options);
|
|
9746
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9747
|
+
sessionTransitioned = true;
|
|
9748
|
+
} finally {
|
|
9749
|
+
this.#finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
9161
9750
|
}
|
|
9162
|
-
await this.sessionManager.newSession(options);
|
|
9163
9751
|
|
|
9164
9752
|
this.#clearCheckpointRuntimeState();
|
|
9165
9753
|
this.setTodoPhases([]);
|
|
@@ -9225,14 +9813,25 @@ export class AgentSession {
|
|
|
9225
9813
|
}
|
|
9226
9814
|
}
|
|
9227
9815
|
|
|
9816
|
+
await this.#flushPendingBashMessages();
|
|
9228
9817
|
// Flush current session to ensure all entries are written
|
|
9229
9818
|
await this.sessionManager.flush();
|
|
9819
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
9230
9820
|
|
|
9231
9821
|
// Fork the session (creates new session file with same entries)
|
|
9232
|
-
|
|
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
|
+
}
|
|
9233
9829
|
if (!forkResult) {
|
|
9830
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
9234
9831
|
return false;
|
|
9235
9832
|
}
|
|
9833
|
+
this.#markBashSessionTransition(bashTransition);
|
|
9834
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
9236
9835
|
|
|
9237
9836
|
// Copy artifacts directory if it exists
|
|
9238
9837
|
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
|
@@ -10608,9 +11207,20 @@ export class AgentSession {
|
|
|
10608
11207
|
return undefined;
|
|
10609
11208
|
}
|
|
10610
11209
|
}
|
|
11210
|
+
await this.#flushPendingBashMessages();
|
|
10611
11211
|
await this.sessionManager.flush();
|
|
11212
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
10612
11213
|
this.#cancelOwnAsyncJobs();
|
|
10613
|
-
|
|
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
|
+
}
|
|
10614
11224
|
|
|
10615
11225
|
this.#clearCheckpointRuntimeState();
|
|
10616
11226
|
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
@@ -11070,6 +11680,7 @@ export class AgentSession {
|
|
|
11070
11680
|
autoContinue,
|
|
11071
11681
|
triggerContextTokens: postMaintenanceContextTokens,
|
|
11072
11682
|
phase: "pre_turn",
|
|
11683
|
+
terminalTextAnswer: isTerminalTextAssistantAnswer(assistantMessage),
|
|
11073
11684
|
});
|
|
11074
11685
|
}
|
|
11075
11686
|
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
@@ -11503,11 +12114,13 @@ export class AgentSession {
|
|
|
11503
12114
|
|
|
11504
12115
|
if (!branchEntry) return;
|
|
11505
12116
|
const targetParentId = prunePrompt ? parentEntry.parentId : branchEntry.parentId;
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
12117
|
+
this.#withBashBranchTransition(() => {
|
|
12118
|
+
if (targetParentId === null) {
|
|
12119
|
+
this.sessionManager.resetLeaf();
|
|
12120
|
+
} else {
|
|
12121
|
+
this.sessionManager.branch(targetParentId);
|
|
12122
|
+
}
|
|
12123
|
+
});
|
|
11511
12124
|
this.sessionManager.appendCustomEntry("accepted-terminal-empty-stop");
|
|
11512
12125
|
}
|
|
11513
12126
|
|
|
@@ -11534,11 +12147,13 @@ export class AgentSession {
|
|
|
11534
12147
|
if (!branchEntry) {
|
|
11535
12148
|
return;
|
|
11536
12149
|
}
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
12150
|
+
this.#withBashBranchTransition(() => {
|
|
12151
|
+
if (branchEntry.parentId === null) {
|
|
12152
|
+
this.sessionManager.resetLeaf();
|
|
12153
|
+
} else {
|
|
12154
|
+
this.sessionManager.branch(branchEntry.parentId);
|
|
12155
|
+
}
|
|
12156
|
+
});
|
|
11542
12157
|
}
|
|
11543
12158
|
|
|
11544
12159
|
#isSameAssistantMessage(left: AssistantMessage, right: AssistantMessage): boolean {
|
|
@@ -11575,8 +12190,10 @@ export class AgentSession {
|
|
|
11575
12190
|
if (this.#pendingRewindReport) return this.#pendingRewindReport;
|
|
11576
12191
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11577
12192
|
const message = messages[i];
|
|
11578
|
-
if (message?.role !== "toolResult" || message.
|
|
11579
|
-
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;
|
|
11580
12197
|
const detailReport =
|
|
11581
12198
|
details && typeof details === "object" && "report" in details && typeof details.report === "string"
|
|
11582
12199
|
? details.report.trim()
|
|
@@ -11593,16 +12210,18 @@ export class AgentSession {
|
|
|
11593
12210
|
if (!checkpointState) {
|
|
11594
12211
|
return;
|
|
11595
12212
|
}
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
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
|
+
});
|
|
11606
12225
|
|
|
11607
12226
|
const rewoundAt = new Date().toISOString();
|
|
11608
12227
|
const details = { report, startedAt: checkpointState.startedAt, rewoundAt };
|
|
@@ -11617,7 +12236,7 @@ export class AgentSession {
|
|
|
11617
12236
|
|
|
11618
12237
|
if (activeMessages) {
|
|
11619
12238
|
for (const message of activeMessages) {
|
|
11620
|
-
if (message.role === "toolResult" && message.toolName === "rewind") {
|
|
12239
|
+
if (message.role === "toolResult" && semanticToolResult(message.toolName, message)?.toolName === "rewind") {
|
|
11621
12240
|
this.#rewoundToolResultIds.add(message.toolCallId);
|
|
11622
12241
|
}
|
|
11623
12242
|
}
|
|
@@ -12916,12 +13535,15 @@ export class AgentSession {
|
|
|
12916
13535
|
suppressContinuation?: boolean;
|
|
12917
13536
|
suppressHandoff?: boolean;
|
|
12918
13537
|
phase?: CodexCompactionContext["phase"];
|
|
13538
|
+
terminalTextAnswer?: boolean;
|
|
12919
13539
|
} = {},
|
|
12920
13540
|
): Promise<CompactionCheckResult> {
|
|
12921
13541
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
12922
13542
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
12923
13543
|
if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
|
|
12924
13544
|
const generation = this.#promptGeneration;
|
|
13545
|
+
const terminalTextAnswer =
|
|
13546
|
+
options.terminalTextAnswer ?? isTerminalTextAssistantAnswer(this.#findLastAssistantMessage());
|
|
12925
13547
|
const suppressContinuation = options.suppressContinuation === true;
|
|
12926
13548
|
const shouldAutoContinue =
|
|
12927
13549
|
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
@@ -12936,6 +13558,7 @@ export class AgentSession {
|
|
|
12936
13558
|
willRetry,
|
|
12937
13559
|
generation,
|
|
12938
13560
|
shouldAutoContinue,
|
|
13561
|
+
terminalTextAnswer,
|
|
12939
13562
|
options.triggerContextTokens,
|
|
12940
13563
|
suppressContinuation,
|
|
12941
13564
|
);
|
|
@@ -12958,11 +13581,17 @@ export class AgentSession {
|
|
|
12958
13581
|
async signal => {
|
|
12959
13582
|
await Promise.resolve();
|
|
12960
13583
|
if (signal.aborted) return;
|
|
12961
|
-
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13584
|
+
await this.#runAutoCompaction(reason, willRetry, true, true, {
|
|
13585
|
+
...options,
|
|
13586
|
+
terminalTextAnswer,
|
|
13587
|
+
});
|
|
12962
13588
|
},
|
|
12963
13589
|
{ generation },
|
|
12964
13590
|
);
|
|
12965
|
-
return
|
|
13591
|
+
return {
|
|
13592
|
+
...COMPACTION_CHECK_DEFERRED_HANDOFF,
|
|
13593
|
+
continuationScheduled: shouldAutoContinue,
|
|
13594
|
+
};
|
|
12966
13595
|
}
|
|
12967
13596
|
|
|
12968
13597
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
@@ -13029,10 +13658,14 @@ export class AgentSession {
|
|
|
13029
13658
|
aborted: false,
|
|
13030
13659
|
willRetry: false,
|
|
13031
13660
|
});
|
|
13032
|
-
const continuationScheduled =
|
|
13033
|
-
|
|
13034
|
-
this.#
|
|
13035
|
-
|
|
13661
|
+
const continuationScheduled =
|
|
13662
|
+
!autoCompactionSignal.aborted &&
|
|
13663
|
+
this.#scheduleCompactionContinuation({
|
|
13664
|
+
generation,
|
|
13665
|
+
autoContinue: reason !== "idle" && shouldAutoContinue,
|
|
13666
|
+
terminalTextAnswer,
|
|
13667
|
+
suppressContinuation,
|
|
13668
|
+
});
|
|
13036
13669
|
return {
|
|
13037
13670
|
...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
|
|
13038
13671
|
historyRewritten: true,
|
|
@@ -13521,20 +14154,13 @@ export class AgentSession {
|
|
|
13521
14154
|
if (retryFits) {
|
|
13522
14155
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13523
14156
|
continuationScheduled = true;
|
|
13524
|
-
} else
|
|
13525
|
-
this.#
|
|
13526
|
-
continuationScheduled = true;
|
|
13527
|
-
}
|
|
13528
|
-
if (!continuationScheduled && !suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
13529
|
-
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
13530
|
-
// Kick the loop so queued messages are actually delivered. This remains separate
|
|
13531
|
-
// from the no-progress warning: pausing maintenance must not strand user input.
|
|
13532
|
-
this.#scheduleAgentContinue({
|
|
13533
|
-
delayMs: 100,
|
|
14157
|
+
} else {
|
|
14158
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13534
14159
|
generation,
|
|
13535
|
-
|
|
14160
|
+
autoContinue: hasHeadroom && shouldAutoContinue,
|
|
14161
|
+
terminalTextAnswer,
|
|
14162
|
+
suppressContinuation,
|
|
13536
14163
|
});
|
|
13537
|
-
continuationScheduled = true;
|
|
13538
14164
|
}
|
|
13539
14165
|
|
|
13540
14166
|
if (deadEndWarning) {
|
|
@@ -13590,6 +14216,7 @@ export class AgentSession {
|
|
|
13590
14216
|
willRetry: boolean,
|
|
13591
14217
|
generation: number,
|
|
13592
14218
|
autoContinue: boolean,
|
|
14219
|
+
terminalTextAnswer: boolean,
|
|
13593
14220
|
triggerContextTokens?: number,
|
|
13594
14221
|
suppressContinuation = false,
|
|
13595
14222
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
@@ -13672,10 +14299,6 @@ export class AgentSession {
|
|
|
13672
14299
|
});
|
|
13673
14300
|
|
|
13674
14301
|
let continuationScheduled = false;
|
|
13675
|
-
if (!willRetry && reason !== "idle" && autoContinue) {
|
|
13676
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
13677
|
-
continuationScheduled = true;
|
|
13678
|
-
}
|
|
13679
14302
|
if (willRetry) {
|
|
13680
14303
|
// The shake rebuild replays every entry, so a trailing error/length
|
|
13681
14304
|
// assistant from the failed turn re-enters agent state — drop it before
|
|
@@ -13691,13 +14314,13 @@ export class AgentSession {
|
|
|
13691
14314
|
}
|
|
13692
14315
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
13693
14316
|
continuationScheduled = true;
|
|
13694
|
-
} else
|
|
13695
|
-
this.#
|
|
13696
|
-
delayMs: 100,
|
|
14317
|
+
} else {
|
|
14318
|
+
continuationScheduled = this.#scheduleCompactionContinuation({
|
|
13697
14319
|
generation,
|
|
13698
|
-
|
|
14320
|
+
autoContinue: reason !== "idle" && autoContinue,
|
|
14321
|
+
terminalTextAnswer,
|
|
14322
|
+
suppressContinuation,
|
|
13699
14323
|
});
|
|
13700
|
-
continuationScheduled = true;
|
|
13701
14324
|
}
|
|
13702
14325
|
if (!reclaimed) {
|
|
13703
14326
|
return willRetry && continuationScheduled
|
|
@@ -13851,6 +14474,11 @@ export class AgentSession {
|
|
|
13851
14474
|
return stopType === "refusal" || stopType === "sensitive";
|
|
13852
14475
|
}
|
|
13853
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
|
+
|
|
13854
14482
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
13855
14483
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
13856
14484
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -13881,8 +14509,8 @@ export class AgentSession {
|
|
|
13881
14509
|
const keyKind = isRetryFallbackModelKey(key) ? "model" : "role";
|
|
13882
14510
|
if (keyKind === "model") {
|
|
13883
14511
|
if (isRetryFallbackWildcardKey(key)) {
|
|
13884
|
-
const provider = key
|
|
13885
|
-
if (!this.#
|
|
14512
|
+
const { provider } = parseRetryFallbackWildcard(key, p => this.#hasProviderModels(p));
|
|
14513
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13886
14514
|
const msg = `retry.fallbackChains wildcard key references unknown provider: ${key}`;
|
|
13887
14515
|
logger.warn(msg);
|
|
13888
14516
|
this.configWarnings.push(msg);
|
|
@@ -13914,8 +14542,8 @@ export class AgentSession {
|
|
|
13914
14542
|
continue;
|
|
13915
14543
|
}
|
|
13916
14544
|
if (isRetryFallbackWildcardKey(selectorStr)) {
|
|
13917
|
-
const provider = selectorStr
|
|
13918
|
-
if (!this.#
|
|
14545
|
+
const { provider } = parseRetryFallbackWildcard(selectorStr, p => this.#hasProviderModels(p));
|
|
14546
|
+
if (!this.#hasProviderModels(provider)) {
|
|
13919
14547
|
const msg = `Fallback chain for ${keyKind} '${key}' references unknown provider: ${selectorStr}`;
|
|
13920
14548
|
logger.warn(msg);
|
|
13921
14549
|
this.configWarnings.push(msg);
|
|
@@ -13974,13 +14602,16 @@ export class AgentSession {
|
|
|
13974
14602
|
* Model-oriented keys win over roles so a chain follows the model across
|
|
13975
14603
|
* role reassignments.
|
|
13976
14604
|
*/
|
|
13977
|
-
#resolveRetryFallbackRole(
|
|
14605
|
+
#resolveRetryFallbackRole(
|
|
14606
|
+
currentSelector: string,
|
|
14607
|
+
currentModel: Model | null | undefined = this.model,
|
|
14608
|
+
): string | undefined {
|
|
13978
14609
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13979
14610
|
if (!parsedCurrent) return undefined;
|
|
13980
14611
|
const chains = this.#getRetryFallbackChains();
|
|
13981
14612
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
13982
|
-
const currentPlainSelector =
|
|
13983
|
-
? formatModelSelectorValue(formatModelString(
|
|
14613
|
+
const currentPlainSelector = currentModel
|
|
14614
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
13984
14615
|
: undefined;
|
|
13985
14616
|
const currentPlainBaseSelector =
|
|
13986
14617
|
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -14006,9 +14637,22 @@ export class AgentSession {
|
|
|
14006
14637
|
for (const key of exactModelKeys) {
|
|
14007
14638
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
14008
14639
|
}
|
|
14009
|
-
// 2. Provider
|
|
14010
|
-
|
|
14011
|
-
|
|
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;
|
|
14012
14656
|
// 3. Role keys — matched by the role's currently-assigned model.
|
|
14013
14657
|
for (const key of roleKeys) {
|
|
14014
14658
|
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
@@ -14027,9 +14671,11 @@ export class AgentSession {
|
|
|
14027
14671
|
|
|
14028
14672
|
/**
|
|
14029
14673
|
* Parse one configured chain entry. A `provider/*` entry keeps the failing
|
|
14030
|
-
* model's id and swaps the provider (google-antigravity/x → google/x);
|
|
14031
|
-
*
|
|
14032
|
-
*
|
|
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.
|
|
14033
14679
|
*/
|
|
14034
14680
|
#parseRetryFallbackChainEntry(
|
|
14035
14681
|
entry: string,
|
|
@@ -14037,8 +14683,23 @@ export class AgentSession {
|
|
|
14037
14683
|
): RetryFallbackSelector | undefined {
|
|
14038
14684
|
if (isRetryFallbackWildcardKey(entry)) {
|
|
14039
14685
|
if (!current) return undefined;
|
|
14040
|
-
const provider = entry
|
|
14041
|
-
|
|
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 };
|
|
14042
14703
|
}
|
|
14043
14704
|
return parseRetryFallbackSelector(entry, this.#modelRegistry);
|
|
14044
14705
|
}
|
|
@@ -14071,7 +14732,11 @@ export class AgentSession {
|
|
|
14071
14732
|
return chain;
|
|
14072
14733
|
}
|
|
14073
14734
|
|
|
14074
|
-
#findRetryFallbackCandidates(
|
|
14735
|
+
#findRetryFallbackCandidates(
|
|
14736
|
+
role: string,
|
|
14737
|
+
currentSelector: string,
|
|
14738
|
+
currentModel: Model | null | undefined = this.model,
|
|
14739
|
+
): RetryFallbackSelector[] {
|
|
14075
14740
|
let chain = this.#getRetryFallbackEffectiveChain(role, currentSelector);
|
|
14076
14741
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
14077
14742
|
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
@@ -14095,8 +14760,8 @@ export class AgentSession {
|
|
|
14095
14760
|
if (chain.length <= 1) return [];
|
|
14096
14761
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
14097
14762
|
const currentPlainSelector =
|
|
14098
|
-
|
|
14099
|
-
? formatModelSelectorValue(formatModelString(
|
|
14763
|
+
currentModel && parsedCurrent
|
|
14764
|
+
? formatModelSelectorValue(formatModelString(currentModel), parsedCurrent.thinkingLevel)
|
|
14100
14765
|
: undefined;
|
|
14101
14766
|
const currentPlainBaseSelector =
|
|
14102
14767
|
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
@@ -14706,17 +15371,187 @@ export class AgentSession {
|
|
|
14706
15371
|
// Bash Execution
|
|
14707
15372
|
// =========================================================================
|
|
14708
15373
|
|
|
14709
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
|
|
15374
|
+
async #saveBashOriginalArtifact(target: BashSessionTarget, originalText: string): Promise<string | undefined> {
|
|
14710
15375
|
try {
|
|
14711
|
-
|
|
15376
|
+
const destination = target.destination ?? (await target.pending);
|
|
15377
|
+
return await destination?.manager.saveArtifact(originalText, "bash-original");
|
|
14712
15378
|
} catch {
|
|
14713
15379
|
return undefined;
|
|
14714
15380
|
}
|
|
14715
15381
|
}
|
|
14716
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
|
+
|
|
15468
|
+
/**
|
|
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
|
+
|
|
14717
15553
|
/**
|
|
14718
|
-
* Execute a bash command.
|
|
14719
|
-
* Adds result to agent context and session.
|
|
15554
|
+
* Execute a bash command and retain the session/branch that owned its start.
|
|
14720
15555
|
* @param command The bash command to execute
|
|
14721
15556
|
* @param onChunk Optional streaming callback for output
|
|
14722
15557
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
@@ -14727,72 +15562,72 @@ export class AgentSession {
|
|
|
14727
15562
|
onChunk?: (chunk: string) => void,
|
|
14728
15563
|
options?: { excludeFromContext?: boolean; useUserShell?: boolean },
|
|
14729
15564
|
): Promise<BashResult> {
|
|
15565
|
+
const target = this.#captureBashSessionTarget();
|
|
15566
|
+
let targetTransferred = false;
|
|
14730
15567
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
14731
15568
|
const cwd = this.sessionManager.getCwd();
|
|
14732
15569
|
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
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
|
+
}
|
|
14743
15583
|
}
|
|
14744
|
-
}
|
|
14745
|
-
|
|
14746
|
-
const abortController = new AbortController();
|
|
14747
|
-
this.#bashAbortControllers.add(abortController);
|
|
14748
15584
|
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
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
|
+
}
|
|
14759
15601
|
|
|
14760
|
-
|
|
15602
|
+
targetTransferred = true;
|
|
15603
|
+
await this.#recordBashResultForTarget(target, command, result, options);
|
|
14761
15604
|
return result;
|
|
14762
15605
|
} finally {
|
|
14763
|
-
this.#
|
|
15606
|
+
if (!targetTransferred) await this.#releaseBashSessionTarget(target);
|
|
14764
15607
|
}
|
|
14765
15608
|
}
|
|
14766
15609
|
|
|
14767
|
-
/**
|
|
14768
|
-
* Record a bash execution result in session history.
|
|
14769
|
-
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
14770
|
-
*/
|
|
15610
|
+
/** Record a bash result supplied outside executeBash in the current ownership scope. */
|
|
14771
15611
|
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {
|
|
14772
|
-
const
|
|
14773
|
-
const
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
cancelled: result.cancelled,
|
|
14779
|
-
truncated: result.truncated,
|
|
14780
|
-
meta,
|
|
14781
|
-
timestamp: Date.now(),
|
|
14782
|
-
excludeFromContext: options?.excludeFromContext,
|
|
14783
|
-
};
|
|
14784
|
-
|
|
14785
|
-
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
14786
|
-
if (this.isStreaming) {
|
|
14787
|
-
// Queue for later - will be flushed on agent_end
|
|
14788
|
-
this.#pendingBashMessages.push(bashMessage);
|
|
14789
|
-
} else {
|
|
14790
|
-
// Add to agent state immediately
|
|
14791
|
-
this.agent.appendMessage(bashMessage);
|
|
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
|
+
}
|
|
14792
15618
|
|
|
14793
|
-
|
|
14794
|
-
|
|
15619
|
+
if (target.destination) {
|
|
15620
|
+
try {
|
|
15621
|
+
this.#appendBashMessage(target.destination, message);
|
|
15622
|
+
} finally {
|
|
15623
|
+
void this.#releaseBashSessionTarget(target);
|
|
15624
|
+
}
|
|
15625
|
+
return;
|
|
14795
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
|
+
});
|
|
14796
15631
|
}
|
|
14797
15632
|
|
|
14798
15633
|
/**
|
|
@@ -14814,22 +15649,14 @@ export class AgentSession {
|
|
|
14814
15649
|
return this.#pendingBashMessages.length > 0;
|
|
14815
15650
|
}
|
|
14816
15651
|
|
|
14817
|
-
/**
|
|
14818
|
-
|
|
14819
|
-
* Called after agent turn completes to maintain proper message ordering.
|
|
14820
|
-
*/
|
|
14821
|
-
#flushPendingBashMessages(): void {
|
|
15652
|
+
/** Flush pending bash messages after the active turn without changing their ownership. */
|
|
15653
|
+
async #flushPendingBashMessages(): Promise<void> {
|
|
14822
15654
|
if (this.#pendingBashMessages.length === 0) return;
|
|
14823
|
-
|
|
14824
|
-
for (const bashMessage of this.#pendingBashMessages) {
|
|
14825
|
-
// Add to agent state
|
|
14826
|
-
this.agent.appendMessage(bashMessage);
|
|
14827
|
-
|
|
14828
|
-
// Save to session
|
|
14829
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
14830
|
-
}
|
|
14831
|
-
|
|
15655
|
+
const pending = this.#pendingBashMessages;
|
|
14832
15656
|
this.#pendingBashMessages = [];
|
|
15657
|
+
for (const { target, message } of pending) {
|
|
15658
|
+
await this.#appendOwnedBashMessage(target, message);
|
|
15659
|
+
}
|
|
14833
15660
|
}
|
|
14834
15661
|
|
|
14835
15662
|
// =========================================================================
|
|
@@ -15422,9 +16249,11 @@ export class AgentSession {
|
|
|
15422
16249
|
this.#disconnectFromAgent();
|
|
15423
16250
|
await this.abort({ goalReason: "internal" });
|
|
15424
16251
|
|
|
16252
|
+
await this.#flushPendingBashMessages();
|
|
15425
16253
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
15426
16254
|
await this.sessionManager.flush();
|
|
15427
16255
|
const previousSessionState = this.sessionManager.captureState();
|
|
16256
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15428
16257
|
// Only same-session reloads compare against the prior context to detect
|
|
15429
16258
|
// rollback edits (`#didSessionMessagesChange` below). Building it for a
|
|
15430
16259
|
// different-session switch is a pure waste — and on huge pre-fix sessions
|
|
@@ -15469,6 +16298,7 @@ export class AgentSession {
|
|
|
15469
16298
|
|
|
15470
16299
|
try {
|
|
15471
16300
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
16301
|
+
this.#markBashSessionTransition(bashTransition);
|
|
15472
16302
|
if (switchingToDifferentSession) {
|
|
15473
16303
|
this.#freshProviderSessionId = undefined;
|
|
15474
16304
|
this.#clearInheritedProviderPromptCacheKey();
|
|
@@ -15602,6 +16432,7 @@ export class AgentSession {
|
|
|
15602
16432
|
error: String(error),
|
|
15603
16433
|
});
|
|
15604
16434
|
}
|
|
16435
|
+
this.#finishBashSessionTransition(bashTransition, true);
|
|
15605
16436
|
return true;
|
|
15606
16437
|
} catch (error) {
|
|
15607
16438
|
this.sessionManager.restoreState(previousSessionState);
|
|
@@ -15633,6 +16464,7 @@ export class AgentSession {
|
|
|
15633
16464
|
this.#syncTodoPhasesFromBranch();
|
|
15634
16465
|
this.#resetAllAdvisorRuntimes();
|
|
15635
16466
|
this.#reconnectToAgent();
|
|
16467
|
+
this.#finishBashSessionTransition(bashTransition, false);
|
|
15636
16468
|
throw error;
|
|
15637
16469
|
}
|
|
15638
16470
|
}
|
|
@@ -15678,14 +16510,25 @@ export class AgentSession {
|
|
|
15678
16510
|
this.#pendingNextTurnMessages = [];
|
|
15679
16511
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
15680
16512
|
|
|
16513
|
+
await this.#flushPendingBashMessages();
|
|
15681
16514
|
// Flush pending writes before branching
|
|
15682
16515
|
await this.sessionManager.flush();
|
|
16516
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15683
16517
|
this.#cancelOwnAsyncJobs();
|
|
16518
|
+
this.#abortAutolearnCapture();
|
|
16519
|
+
await this.#drainAutolearnCapture();
|
|
15684
16520
|
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15688
|
-
|
|
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);
|
|
15689
16532
|
}
|
|
15690
16533
|
this.#rehydrateCheckpointRewindState();
|
|
15691
16534
|
this.#syncTodoPhasesFromBranch();
|
|
@@ -15769,10 +16612,21 @@ export class AgentSession {
|
|
|
15769
16612
|
await this.abort({ goalReason: "internal", reason: "branching /btw" });
|
|
15770
16613
|
this.agent.replaceQueues([], []);
|
|
15771
16614
|
}
|
|
16615
|
+
await this.#flushPendingBashMessages();
|
|
15772
16616
|
await this.sessionManager.flush();
|
|
16617
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15773
16618
|
this.#cancelOwnAsyncJobs();
|
|
16619
|
+
this.#abortAutolearnCapture();
|
|
16620
|
+
await this.#drainAutolearnCapture();
|
|
15774
16621
|
|
|
15775
|
-
|
|
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
|
+
}
|
|
15776
16630
|
|
|
15777
16631
|
this.#rehydrateCheckpointRewindState();
|
|
15778
16632
|
this.sessionManager.appendMessage({
|
|
@@ -15828,6 +16682,7 @@ export class AgentSession {
|
|
|
15828
16682
|
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
15829
16683
|
sessionContext?: SessionContext;
|
|
15830
16684
|
}> {
|
|
16685
|
+
await this.#flushPendingBashMessages();
|
|
15831
16686
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
15832
16687
|
|
|
15833
16688
|
// No-op if already at target
|
|
@@ -15955,18 +16810,28 @@ export class AgentSession {
|
|
|
15955
16810
|
|
|
15956
16811
|
// Switch leaf (with or without summary)
|
|
15957
16812
|
// Summary is attached at the navigation target position (newLeafId), not the old branch
|
|
16813
|
+
const bashTransition = this.#beginBashSessionTransition();
|
|
15958
16814
|
let summaryEntry: BranchSummaryEntry | undefined;
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
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);
|
|
15970
16835
|
}
|
|
15971
16836
|
|
|
15972
16837
|
// Update agent state — build display context to populate agent messages.
|
|
@@ -16709,29 +17574,73 @@ export class AgentSession {
|
|
|
16709
17574
|
return this.#advisors[0]?.agent;
|
|
16710
17575
|
}
|
|
16711
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
|
+
}
|
|
16712
17598
|
/**
|
|
16713
17599
|
* Return structured advisor stats for the status command and TUI panel.
|
|
16714
17600
|
*/
|
|
16715
17601
|
getAdvisorStats(): AdvisorStats {
|
|
16716
17602
|
const configured = this.#advisorEnabled;
|
|
16717
|
-
const
|
|
16718
|
-
|
|
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) {
|
|
16719
17628
|
return {
|
|
16720
17629
|
configured,
|
|
16721
|
-
active
|
|
17630
|
+
active,
|
|
16722
17631
|
contextWindow: 0,
|
|
16723
17632
|
contextTokens: 0,
|
|
16724
17633
|
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
16725
17634
|
cost: 0,
|
|
16726
17635
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
16727
|
-
advisors:
|
|
17636
|
+
advisors: roster,
|
|
16728
17637
|
};
|
|
16729
17638
|
}
|
|
16730
17639
|
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
16731
17640
|
const messages = { user: 0, assistant: 0, total: 0 };
|
|
16732
17641
|
let cost = 0;
|
|
16733
17642
|
let contextTokens = 0;
|
|
16734
|
-
for (const a of
|
|
17643
|
+
for (const a of liveAdvisors) {
|
|
16735
17644
|
tokens.input += a.tokens.input;
|
|
16736
17645
|
tokens.output += a.tokens.output;
|
|
16737
17646
|
tokens.reasoning += a.tokens.reasoning;
|
|
@@ -16748,14 +17657,14 @@ export class AgentSession {
|
|
|
16748
17657
|
// first advisor's so the legacy status line stays byte-identical.
|
|
16749
17658
|
return {
|
|
16750
17659
|
configured,
|
|
16751
|
-
active
|
|
16752
|
-
model:
|
|
16753
|
-
contextWindow:
|
|
17660
|
+
active,
|
|
17661
|
+
model: liveAdvisors[0].model,
|
|
17662
|
+
contextWindow: liveAdvisors[0].contextWindow,
|
|
16754
17663
|
contextTokens,
|
|
16755
17664
|
tokens,
|
|
16756
17665
|
cost,
|
|
16757
17666
|
messages,
|
|
16758
|
-
advisors,
|
|
17667
|
+
advisors: roster,
|
|
16759
17668
|
};
|
|
16760
17669
|
}
|
|
16761
17670
|
|
|
@@ -16789,12 +17698,18 @@ export class AgentSession {
|
|
|
16789
17698
|
}
|
|
16790
17699
|
return {
|
|
16791
17700
|
name: advisor.name,
|
|
17701
|
+
status: advisor.runtime.quotaExhausted
|
|
17702
|
+
? "quota_exhausted"
|
|
17703
|
+
: advisor.runtime.failureNotified
|
|
17704
|
+
? "error"
|
|
17705
|
+
: "running",
|
|
16792
17706
|
model,
|
|
16793
17707
|
contextWindow: model.contextWindow ?? 0,
|
|
16794
17708
|
contextTokens,
|
|
16795
17709
|
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
|
|
16796
17710
|
cost,
|
|
16797
17711
|
messages: { user, assistant, total: messages.length },
|
|
17712
|
+
sessionId: advisor.agent.sessionId,
|
|
16798
17713
|
};
|
|
16799
17714
|
}
|
|
16800
17715
|
|
|
@@ -16803,13 +17718,18 @@ export class AgentSession {
|
|
|
16803
17718
|
*/
|
|
16804
17719
|
formatAdvisorStatus(): string {
|
|
16805
17720
|
const stats = this.getAdvisorStats();
|
|
16806
|
-
if (!stats.active) {
|
|
17721
|
+
if (!stats.active && stats.advisors.length === 0) {
|
|
16807
17722
|
return stats.configured
|
|
16808
17723
|
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
16809
17724
|
: "Advisor is disabled.";
|
|
16810
17725
|
}
|
|
16811
17726
|
if (stats.advisors.length <= 1) {
|
|
16812
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
|
+
}
|
|
16813
17733
|
const contextLine =
|
|
16814
17734
|
s.contextWindow > 0
|
|
16815
17735
|
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
@@ -16818,6 +17738,7 @@ export class AgentSession {
|
|
|
16818
17738
|
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
16819
17739
|
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
16820
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("_", " ")}.`;
|
|
16821
17742
|
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
16822
17743
|
}
|
|
16823
17744
|
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
@@ -16826,7 +17747,9 @@ export class AgentSession {
|
|
|
16826
17747
|
s.contextWindow > 0
|
|
16827
17748
|
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
16828
17749
|
: `${s.contextTokens.toLocaleString()}`;
|
|
16829
|
-
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
|
+
);
|
|
16830
17753
|
}
|
|
16831
17754
|
lines.push(
|
|
16832
17755
|
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
@@ -16835,37 +17758,51 @@ export class AgentSession {
|
|
|
16835
17758
|
}
|
|
16836
17759
|
|
|
16837
17760
|
/**
|
|
16838
|
-
* Estimate the advisor's current context tokens.
|
|
16839
|
-
*
|
|
16840
|
-
*
|
|
16841
|
-
*
|
|
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.
|
|
16842
17766
|
*/
|
|
16843
17767
|
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
16844
|
-
let
|
|
16845
|
-
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
17768
|
+
let usageAnchorStartIndex = 0;
|
|
16846
17769
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16847
|
-
const
|
|
16848
|
-
if (
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
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;
|
|
16855
17790
|
}
|
|
16856
17791
|
}
|
|
16857
|
-
|
|
17792
|
+
|
|
17793
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
17794
|
+
if (!lastUsage || lastUsageIndex === undefined) {
|
|
16858
17795
|
let estimated = 0;
|
|
16859
17796
|
for (const message of messages) {
|
|
16860
|
-
estimated += estimateTokens(message);
|
|
17797
|
+
estimated += estimateTokens(message, estimateOptions);
|
|
16861
17798
|
}
|
|
16862
17799
|
return estimated;
|
|
16863
17800
|
}
|
|
16864
17801
|
let trailingTokens = 0;
|
|
16865
17802
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
16866
|
-
trailingTokens += estimateTokens(messages[i]);
|
|
17803
|
+
trailingTokens += estimateTokens(messages[i], estimateOptions);
|
|
16867
17804
|
}
|
|
16868
|
-
return
|
|
17805
|
+
return calculateContextTokens(lastUsage) + trailingTokens;
|
|
16869
17806
|
}
|
|
16870
17807
|
|
|
16871
17808
|
/**
|