@sayknow-cli/coding-agent 0.5.1 → 0.5.6
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 +185 -14
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +169 -4
- package/package.json +16 -7
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/commands/session.ts +88 -2
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +69 -1
- package/src/config/model-registry.ts +72 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +9 -0
- package/src/config/settings.ts +13 -2
- package/src/config/telegram-autostart.ts +11 -4
- package/src/dap/client.ts +78 -30
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/main.ts +1 -1
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/modes/types.ts +5 -1
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +25 -18
- package/src/session/agent-session.ts +120 -25
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +109 -5
- package/src/slash-commands/types.ts +5 -0
- package/src/tools/ask.ts +188 -10
- package/src/tools/eval.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
package/src/sdk/session.ts
CHANGED
|
@@ -2272,13 +2272,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2272
2272
|
};
|
|
2273
2273
|
};
|
|
2274
2274
|
|
|
2275
|
+
const explicitlyDisabledTools = options.toolNames?.length === 0;
|
|
2275
2276
|
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
|
|
2276
2277
|
const hasExplicitToolNames = options.toolNames !== undefined;
|
|
2277
2278
|
const requestedToolNames = hasExplicitToolNames
|
|
2278
2279
|
? [
|
|
2279
2280
|
...new Set([
|
|
2280
2281
|
...options.toolNames!.map(name => name.toLowerCase()),
|
|
2281
|
-
...(settings.get("goal.enabled") ? ["goal"] : []),
|
|
2282
|
+
...(settings.get("goal.enabled") && !explicitlyDisabledTools ? ["goal"] : []),
|
|
2282
2283
|
]),
|
|
2283
2284
|
]
|
|
2284
2285
|
: toolNamesFromRegistry;
|
|
@@ -2298,7 +2299,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2298
2299
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
2299
2300
|
);
|
|
2300
2301
|
const requestedActiveToolNames = normalizedRequested;
|
|
2301
|
-
const initialRequestedActiveToolNames =
|
|
2302
|
+
const initialRequestedActiveToolNames = hasExplicitToolNames
|
|
2302
2303
|
? requestedActiveToolNames
|
|
2303
2304
|
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
|
|
2304
2305
|
const discoverableMCPToolNames = new Set<string>();
|
|
@@ -2348,11 +2349,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2348
2349
|
];
|
|
2349
2350
|
hasExplicitMCPToolSelection =
|
|
2350
2351
|
hasExplicitToolNames && (options.toolNames!.length === 0 || selectableExplicitMCPToolNames.length > 0);
|
|
2351
|
-
initialSelectedMCPToolNames =
|
|
2352
|
-
?
|
|
2353
|
-
:
|
|
2354
|
-
?
|
|
2355
|
-
:
|
|
2352
|
+
initialSelectedMCPToolNames = explicitlyDisabledTools
|
|
2353
|
+
? []
|
|
2354
|
+
: existingSession.hasPersistedMCPToolSelection
|
|
2355
|
+
? restoredSelectedMCPToolNames
|
|
2356
|
+
: hasExplicitMCPToolSelection
|
|
2357
|
+
? selectableExplicitMCPToolNames
|
|
2358
|
+
: defaultSelectedMCPToolNames;
|
|
2356
2359
|
initialToolNames = [
|
|
2357
2360
|
...new Set([
|
|
2358
2361
|
...initialRequestedActiveToolNames.filter(name => !discoverableMCPToolNames.has(name)),
|
|
@@ -2361,14 +2364,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2361
2364
|
];
|
|
2362
2365
|
}
|
|
2363
2366
|
|
|
2364
|
-
// Custom, extension-registered, and plugin-bundle MCP tools
|
|
2365
|
-
//
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2367
|
+
// Custom, extension-registered, and plugin-bundle MCP tools bypass
|
|
2368
|
+
// built-in filtering, but not an explicit zero-tool selection.
|
|
2369
|
+
const alwaysInclude: string[] = explicitlyDisabledTools
|
|
2370
|
+
? []
|
|
2371
|
+
: [
|
|
2372
|
+
...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
|
|
2373
|
+
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
|
|
2374
|
+
...pluginMcpToolNames,
|
|
2375
|
+
];
|
|
2372
2376
|
const pluginMcpToolNameSet = new Set(pluginMcpToolNames);
|
|
2373
2377
|
for (const name of alwaysInclude) {
|
|
2374
2378
|
if (mcpDiscoveryEnabled && discoverableMCPToolNames.has(name) && !pluginMcpToolNameSet.has(name)) {
|
|
@@ -2414,9 +2418,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2414
2418
|
allowedDiscoveredBuiltinNames,
|
|
2415
2419
|
essentialBuiltinNames,
|
|
2416
2420
|
);
|
|
2417
|
-
initialSelectedDiscoveredBuiltinToolNames =
|
|
2418
|
-
?
|
|
2419
|
-
:
|
|
2421
|
+
initialSelectedDiscoveredBuiltinToolNames = explicitlyDisabledTools
|
|
2422
|
+
? []
|
|
2423
|
+
: existingSession.hasPersistedDiscoveredBuiltinToolSelection
|
|
2424
|
+
? restoredDiscoveredNames
|
|
2425
|
+
: explicitlyRequestedDiscoveredBuiltinToolNames;
|
|
2420
2426
|
initialToolNames = [...new Set([...baselineInitialToolNames, ...initialSelectedDiscoveredBuiltinToolNames])];
|
|
2421
2427
|
hasExplicitDiscoveredBuiltinToolSelection =
|
|
2422
2428
|
hasExplicitToolNames &&
|
|
@@ -2717,6 +2723,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2717
2723
|
workspaceTree: resolvedWorkspaceTree,
|
|
2718
2724
|
reloadSshTool,
|
|
2719
2725
|
requestedToolNames: requestedToolNameSet,
|
|
2726
|
+
explicitlyDisabledTools,
|
|
2720
2727
|
discoverableToolAllowedNames: options.discoverableToolAllowedNames,
|
|
2721
2728
|
mcpDiscoveryEnabled,
|
|
2722
2729
|
discoveryMode: effectiveDiscoveryMode,
|
|
@@ -545,6 +545,8 @@ export interface AgentSessionConfig {
|
|
|
545
545
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
546
546
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
547
547
|
requestedToolNames?: ReadonlySet<string>;
|
|
548
|
+
/** True only when the caller explicitly selected no tools. */
|
|
549
|
+
explicitlyDisabledTools?: boolean;
|
|
548
550
|
/** Optional per-session allowlist for tools exposed through search_tool_bm25. */
|
|
549
551
|
discoverableToolAllowedNames?: readonly string[];
|
|
550
552
|
/** Optional accessor for live MCP server instructions, injected as untrusted user-role request data. */
|
|
@@ -1547,6 +1549,27 @@ type SessionAdmissionLease = {
|
|
|
1547
1549
|
release(): void;
|
|
1548
1550
|
};
|
|
1549
1551
|
|
|
1552
|
+
const TODO_WRITE_INFRASTRUCTURE_ERROR =
|
|
1553
|
+
/(?:HTTP2StreamReset|ECONNRESET|ECONNREFUSED|ETIMEDOUT|network error|transport error|connection reset|stream reset|timed out|timeout|fetching https?:)/iu;
|
|
1554
|
+
const TODO_WRITE_ERROR_CONTEXT_LIMIT = 1_000;
|
|
1555
|
+
|
|
1556
|
+
export function buildTodoWriteFailureReminder(errorText: string | undefined, failureCount: number): string {
|
|
1557
|
+
const normalizedError = errorText?.trim().slice(0, TODO_WRITE_ERROR_CONTEXT_LIMIT);
|
|
1558
|
+
const shouldRetry =
|
|
1559
|
+
failureCount === 1 && (!normalizedError || !TODO_WRITE_INFRASTRUCTURE_ERROR.test(normalizedError));
|
|
1560
|
+
const directive = shouldRetry
|
|
1561
|
+
? "Correct the todo_write payload and retry once with a minimal argument object. If that retry fails, do not call todo_write again in this user turn; continue the requested work using a checklist in reasoning."
|
|
1562
|
+
: "Do not call todo_write again in this user turn. Continue the requested work now, track the remaining steps in reasoning, and report progress directly.";
|
|
1563
|
+
|
|
1564
|
+
return [
|
|
1565
|
+
"<system-reminder>",
|
|
1566
|
+
"todo_write failed, so todo progress is not visible to the user.",
|
|
1567
|
+
normalizedError ? `Failure: ${normalizedError}` : "Failure: todo_write returned an error.",
|
|
1568
|
+
directive,
|
|
1569
|
+
"</system-reminder>",
|
|
1570
|
+
].join("\n");
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1550
1573
|
export class AgentSession {
|
|
1551
1574
|
readonly agent: Agent;
|
|
1552
1575
|
readonly sessionManager: SessionManager;
|
|
@@ -1727,6 +1750,7 @@ export class AgentSession {
|
|
|
1727
1750
|
// Handoff state
|
|
1728
1751
|
#handoffAbortController: AbortController | undefined = undefined;
|
|
1729
1752
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
|
|
1753
|
+
#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
1730
1754
|
|
|
1731
1755
|
// Retry state
|
|
1732
1756
|
#retryAbortController: AbortController | undefined = undefined;
|
|
@@ -1738,8 +1762,10 @@ export class AgentSession {
|
|
|
1738
1762
|
#overflowMaintenanceAttempts = 0;
|
|
1739
1763
|
#defaultFallbackExhaustedLastTurn = false;
|
|
1740
1764
|
#fallbackInvocationId = 0;
|
|
1765
|
+
#fallbackSuppressedSelectors = new Set<string>();
|
|
1741
1766
|
// Todo completion reminder state
|
|
1742
1767
|
#todoReminderCount = 0;
|
|
1768
|
+
#todoWriteFailureCount = 0;
|
|
1743
1769
|
#deepInterviewUserIntentEpoch = 0;
|
|
1744
1770
|
#deepInterviewTurnOwnerEpoch = 0;
|
|
1745
1771
|
#deepInterviewGenuineUserMessageEpochs = new WeakMap<object, number>();
|
|
@@ -1831,6 +1857,7 @@ export class AgentSession {
|
|
|
1831
1857
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1832
1858
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1833
1859
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1860
|
+
#explicitlyDisabledTools: boolean;
|
|
1834
1861
|
#baseSystemPrompt: string[];
|
|
1835
1862
|
#initialWorkspaceTree: WorkspaceTree | undefined;
|
|
1836
1863
|
/** Throttle cache for the per-turn volatile workspace-tree scan (see #buildVolatileProjectContextMessage). */
|
|
@@ -2021,6 +2048,12 @@ export class AgentSession {
|
|
|
2021
2048
|
});
|
|
2022
2049
|
}
|
|
2023
2050
|
|
|
2051
|
+
#sessionTransitionAdmissionBusyError(): AgentBusyError {
|
|
2052
|
+
return Object.assign(new AgentBusyError("Cannot start a turn while a session transition is in progress."), {
|
|
2053
|
+
code: "busy",
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2024
2057
|
/**
|
|
2025
2058
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
2026
2059
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -2046,6 +2079,7 @@ export class AgentSession {
|
|
|
2046
2079
|
* orchestrator does not hold it), so there is no self-deadlock.
|
|
2047
2080
|
*/
|
|
2048
2081
|
#sessionTransitionKind: string | undefined;
|
|
2082
|
+
#sessionTurnAdmissionFenced = false;
|
|
2049
2083
|
|
|
2050
2084
|
#beginSessionTransition(kind: string): void {
|
|
2051
2085
|
if (this.#sessionTransitionKind !== undefined) {
|
|
@@ -2086,6 +2120,7 @@ export class AgentSession {
|
|
|
2086
2120
|
code: "busy",
|
|
2087
2121
|
});
|
|
2088
2122
|
}
|
|
2123
|
+
if (this.#sessionTurnAdmissionFenced) throw this.#sessionTransitionAdmissionBusyError();
|
|
2089
2124
|
|
|
2090
2125
|
const entry: SessionAdmissionEntry = {
|
|
2091
2126
|
kind,
|
|
@@ -2114,6 +2149,13 @@ export class AgentSession {
|
|
|
2114
2149
|
code: "busy",
|
|
2115
2150
|
});
|
|
2116
2151
|
}
|
|
2152
|
+
if (this.#sessionTurnAdmissionFenced) {
|
|
2153
|
+
entry.released = true;
|
|
2154
|
+
entry.settled.resolve();
|
|
2155
|
+
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
2156
|
+
this.#activateNextSessionAdmission();
|
|
2157
|
+
throw this.#sessionTransitionAdmissionBusyError();
|
|
2158
|
+
}
|
|
2117
2159
|
|
|
2118
2160
|
const release = () => {
|
|
2119
2161
|
if (entry.released) return;
|
|
@@ -2350,6 +2392,7 @@ export class AgentSession {
|
|
|
2350
2392
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2351
2393
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2352
2394
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2395
|
+
this.#explicitlyDisabledTools = config.explicitlyDisabledTools === true;
|
|
2353
2396
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2354
2397
|
this.#onPayload = config.onPayload;
|
|
2355
2398
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
@@ -3757,8 +3800,9 @@ export class AgentSession {
|
|
|
3757
3800
|
this.#resolveTtsrResume();
|
|
3758
3801
|
}
|
|
3759
3802
|
this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
|
|
3760
|
-
if (this.#handoffAbortController) {
|
|
3803
|
+
if (this.#handoffAbortController || this.#suppressNextPostTurnMaintenanceAfterAutoHandoff) {
|
|
3761
3804
|
this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
|
|
3805
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
3762
3806
|
}
|
|
3763
3807
|
if (
|
|
3764
3808
|
assistantMsg.stopReason !== "error" &&
|
|
@@ -3794,24 +3838,21 @@ export class AgentSession {
|
|
|
3794
3838
|
if (toolName === "edit" && details?.path) {
|
|
3795
3839
|
this.#invalidateFileCacheForPath(details.path);
|
|
3796
3840
|
}
|
|
3797
|
-
if (toolName === "todo_write" && !isError
|
|
3798
|
-
this
|
|
3841
|
+
if (toolName === "todo_write" && !isError) {
|
|
3842
|
+
this.#todoWriteFailureCount = 0;
|
|
3843
|
+
if (Array.isArray(details?.phases)) {
|
|
3844
|
+
this.setTodoPhases(details.phases);
|
|
3845
|
+
}
|
|
3799
3846
|
}
|
|
3800
3847
|
if (toolName === "todo_write" && isError) {
|
|
3848
|
+
this.#todoWriteFailureCount++;
|
|
3801
3849
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
3802
|
-
const reminderText = [
|
|
3803
|
-
"<system-reminder>",
|
|
3804
|
-
"todo_write failed, so todo progress is not visible to the user.",
|
|
3805
|
-
errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
|
|
3806
|
-
"Fix the todo payload and call todo_write again before continuing.",
|
|
3807
|
-
"</system-reminder>",
|
|
3808
|
-
].join("\n");
|
|
3809
3850
|
await this.sendCustomMessage(
|
|
3810
3851
|
{
|
|
3811
3852
|
customType: "todo-write-error-reminder",
|
|
3812
|
-
content:
|
|
3853
|
+
content: buildTodoWriteFailureReminder(errorText, this.#todoWriteFailureCount),
|
|
3813
3854
|
display: false,
|
|
3814
|
-
details: { toolName, errorText },
|
|
3855
|
+
details: { toolName, errorText, failureCount: this.#todoWriteFailureCount },
|
|
3815
3856
|
},
|
|
3816
3857
|
{ deliverAs: "nextTurn" },
|
|
3817
3858
|
);
|
|
@@ -5853,6 +5894,7 @@ export class AgentSession {
|
|
|
5853
5894
|
}
|
|
5854
5895
|
|
|
5855
5896
|
async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
|
|
5897
|
+
if (this.#explicitlyDisabledTools) return [];
|
|
5856
5898
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
5857
5899
|
const previousSelectedDiscoveredBuiltinToolNames = this.#getSelectedDiscoveredBuiltinToolNames();
|
|
5858
5900
|
const nextActiveToolNames = this.getActiveToolNames();
|
|
@@ -6080,7 +6122,9 @@ export class AgentSession {
|
|
|
6080
6122
|
nextSelectedDiscoveredBuiltinToolNames?: string[];
|
|
6081
6123
|
},
|
|
6082
6124
|
): Promise<void> {
|
|
6083
|
-
toolNames =
|
|
6125
|
+
toolNames = this.#explicitlyDisabledTools
|
|
6126
|
+
? []
|
|
6127
|
+
: [...new Set([...toolNames.map(name => name.toLowerCase()), ...this.#mandatoryMCPToolNames])];
|
|
6084
6128
|
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
6085
6129
|
const previousSelectedDiscoveredBuiltinToolNames =
|
|
6086
6130
|
options?.previousSelectedDiscoveredBuiltinToolNames ?? this.#getSelectedDiscoveredBuiltinToolNames();
|
|
@@ -6190,6 +6234,11 @@ export class AgentSession {
|
|
|
6190
6234
|
await this.#applyActiveToolsByName(nextActive);
|
|
6191
6235
|
}
|
|
6192
6236
|
|
|
6237
|
+
/** Whether the caller explicitly selected no tools for this session. */
|
|
6238
|
+
hasExplicitlyDisabledTools(): boolean {
|
|
6239
|
+
return this.#explicitlyDisabledTools;
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6193
6242
|
/**
|
|
6194
6243
|
* Set active tools by name.
|
|
6195
6244
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
@@ -6201,6 +6250,11 @@ export class AgentSession {
|
|
|
6201
6250
|
}
|
|
6202
6251
|
|
|
6203
6252
|
async #restoreMCPSelectionsForSessionContext(sessionContext: SessionContext): Promise<void> {
|
|
6253
|
+
if (this.#explicitlyDisabledTools) {
|
|
6254
|
+
this.#selectedDiscoveredToolNames.clear();
|
|
6255
|
+
await this.#applyActiveToolsByName([], { persistMCPSelection: false });
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6204
6258
|
if (!this.#mcpDiscoveryEnabled && this.#resolveEffectiveDiscoveryMode() !== "all") {
|
|
6205
6259
|
await this.#attachAskToolIfWorkflowActive();
|
|
6206
6260
|
return;
|
|
@@ -7054,6 +7108,7 @@ export class AgentSession {
|
|
|
7054
7108
|
this.#toolRegistry.set(askTool.name, askTool);
|
|
7055
7109
|
}
|
|
7056
7110
|
|
|
7111
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7057
7112
|
try {
|
|
7058
7113
|
if ((this.#workflowGateEmitter?.listPendingGates?.().length ?? 0) > 0) {
|
|
7059
7114
|
this.#attachAskTool();
|
|
@@ -7099,6 +7154,7 @@ export class AgentSession {
|
|
|
7099
7154
|
}
|
|
7100
7155
|
|
|
7101
7156
|
#attachAskTool(): void {
|
|
7157
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7102
7158
|
const askTool = this.#toolRegistry.get("ask");
|
|
7103
7159
|
if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
|
|
7104
7160
|
this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
|
|
@@ -7181,6 +7237,7 @@ export class AgentSession {
|
|
|
7181
7237
|
}
|
|
7182
7238
|
|
|
7183
7239
|
async #activatePendingSkcGoalModeRequest(): Promise<boolean> {
|
|
7240
|
+
if (this.#explicitlyDisabledTools) return false;
|
|
7184
7241
|
if (!this.settings.get("goal.enabled")) return false;
|
|
7185
7242
|
const pendingGoal = await consumePendingGoalModeRequest(
|
|
7186
7243
|
this.sessionManager.getCwd(),
|
|
@@ -7193,9 +7250,10 @@ export class AgentSession {
|
|
|
7193
7250
|
}
|
|
7194
7251
|
|
|
7195
7252
|
const previousTools = this.getActiveToolNames();
|
|
7196
|
-
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
7197
7253
|
await this.#goalRuntime.createGoal({ objective: pendingGoal.objective, provenance: pendingGoal.provenance });
|
|
7198
|
-
|
|
7254
|
+
if (!this.#explicitlyDisabledTools) {
|
|
7255
|
+
await this.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
7256
|
+
}
|
|
7199
7257
|
if (this.isStreaming) {
|
|
7200
7258
|
await this.sendGoalModeContext({ deliverAs: "steer" });
|
|
7201
7259
|
}
|
|
@@ -7793,6 +7851,7 @@ export class AgentSession {
|
|
|
7793
7851
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
7794
7852
|
this.#overflowMaintenanceAttempts = 0;
|
|
7795
7853
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
7854
|
+
this.#todoWriteFailureCount = 0;
|
|
7796
7855
|
}
|
|
7797
7856
|
// Flush any pending bash messages before the new prompt
|
|
7798
7857
|
this.#flushPendingBashMessages();
|
|
@@ -9452,7 +9511,6 @@ export class AgentSession {
|
|
|
9452
9511
|
persistMCPSelection: false,
|
|
9453
9512
|
nextSelectedDiscoveredBuiltinToolNames: [],
|
|
9454
9513
|
});
|
|
9455
|
-
} else {
|
|
9456
9514
|
}
|
|
9457
9515
|
}
|
|
9458
9516
|
this.#todoReminderCount = 0;
|
|
@@ -10046,7 +10104,13 @@ export class AgentSession {
|
|
|
10046
10104
|
});
|
|
10047
10105
|
if (!resolved.model) continue;
|
|
10048
10106
|
|
|
10049
|
-
if (
|
|
10107
|
+
if (
|
|
10108
|
+
roleModels.some(
|
|
10109
|
+
candidate =>
|
|
10110
|
+
modelsAreEqual(candidate.model, resolved.model) && candidate.thinkingLevel === resolved.thinkingLevel,
|
|
10111
|
+
)
|
|
10112
|
+
)
|
|
10113
|
+
continue;
|
|
10050
10114
|
roleModels.push({
|
|
10051
10115
|
role,
|
|
10052
10116
|
model: resolved.model,
|
|
@@ -11606,6 +11670,11 @@ export class AgentSession {
|
|
|
11606
11670
|
this.#resourceSampler = sampler;
|
|
11607
11671
|
}
|
|
11608
11672
|
|
|
11673
|
+
/** Test seam: drive the pre-prompt emergency/threshold check directly. */
|
|
11674
|
+
runPrePromptContextCheckForTests(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
11675
|
+
return this.#checkEstimatedContextBeforePrompt(pendingMessages);
|
|
11676
|
+
}
|
|
11677
|
+
|
|
11609
11678
|
setRetainedMemorySampler(sampler: (() => RetainedMemorySample) | undefined): void {
|
|
11610
11679
|
this.#retainedMemorySampler = sampler;
|
|
11611
11680
|
}
|
|
@@ -11639,26 +11708,31 @@ export class AgentSession {
|
|
|
11639
11708
|
materializedResidentBytes: this.#streamingEditFileCache.totalBytes,
|
|
11640
11709
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
11641
11710
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
11711
|
+
transcriptFileBytes: this.sessionManager.getTranscriptFileBytes(),
|
|
11642
11712
|
};
|
|
11643
11713
|
}
|
|
11644
11714
|
|
|
11645
11715
|
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
11646
11716
|
const model = this.model;
|
|
11647
11717
|
if (!model) return;
|
|
11648
|
-
const contextWindow = model.contextWindow ?? 0;
|
|
11649
|
-
if (contextWindow <= 0) return;
|
|
11650
11718
|
// F6: non-disableable emergency floor — compact before OOM even when token-based
|
|
11651
11719
|
// compaction is disabled or its threshold is set too high (weak-hardware protection).
|
|
11652
11720
|
const emergencyReason = emergencyCompactionReason(this.#resourceSampler());
|
|
11653
11721
|
if (emergencyReason) {
|
|
11654
11722
|
logger.warn("Emergency compaction triggered (resource floor exceeded)", { reason: emergencyReason });
|
|
11655
|
-
await this.#runAutoCompaction("overflow", false, false, {
|
|
11723
|
+
const status = await this.#runAutoCompaction("overflow", false, false, {
|
|
11656
11724
|
continueAfterMaintenance: false,
|
|
11657
11725
|
deferHandoffMaintenance: false,
|
|
11658
11726
|
force: true,
|
|
11659
11727
|
});
|
|
11728
|
+
if (emergencyReason === "transcriptFile" && status.kind === "skipped") {
|
|
11729
|
+
logger.warn("Transcript emergency compaction was skipped; rewriting exact live entries");
|
|
11730
|
+
await this.sessionManager.rewriteEntries();
|
|
11731
|
+
}
|
|
11660
11732
|
return;
|
|
11661
11733
|
}
|
|
11734
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
11735
|
+
if (contextWindow <= 0) return;
|
|
11662
11736
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11663
11737
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
11664
11738
|
|
|
@@ -11692,6 +11766,7 @@ export class AgentSession {
|
|
|
11692
11766
|
await this.#runAutoCompaction("threshold", false, false, {
|
|
11693
11767
|
continueAfterMaintenance: false,
|
|
11694
11768
|
deferHandoffMaintenance: false,
|
|
11769
|
+
suppressNextPostTurnMaintenanceAfterHandoff: true,
|
|
11695
11770
|
});
|
|
11696
11771
|
}
|
|
11697
11772
|
}
|
|
@@ -11783,6 +11858,7 @@ export class AgentSession {
|
|
|
11783
11858
|
return;
|
|
11784
11859
|
}
|
|
11785
11860
|
|
|
11861
|
+
if (this.#explicitlyDisabledTools) return;
|
|
11786
11862
|
this.#attachAskTool();
|
|
11787
11863
|
|
|
11788
11864
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
@@ -12682,6 +12758,7 @@ export class AgentSession {
|
|
|
12682
12758
|
force?: boolean;
|
|
12683
12759
|
signal?: AbortSignal;
|
|
12684
12760
|
beforeTerminalOverflowNoop?: () => void;
|
|
12761
|
+
suppressNextPostTurnMaintenanceAfterHandoff?: boolean;
|
|
12685
12762
|
},
|
|
12686
12763
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12687
12764
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12763,6 +12840,9 @@ export class AgentSession {
|
|
|
12763
12840
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12764
12841
|
|
|
12765
12842
|
if (handoffResult) {
|
|
12843
|
+
if (options?.suppressNextPostTurnMaintenanceAfterHandoff) {
|
|
12844
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = true;
|
|
12845
|
+
}
|
|
12766
12846
|
await this.#emitSessionEvent({
|
|
12767
12847
|
type: "auto_compaction_end",
|
|
12768
12848
|
action,
|
|
@@ -13482,12 +13562,15 @@ export class AgentSession {
|
|
|
13482
13562
|
if (controller.chain.entries.length > 1) await this.#advanceDefaultFallback(controller, "new_turn", 0);
|
|
13483
13563
|
return;
|
|
13484
13564
|
}
|
|
13565
|
+
const primarySelector = controller.chain.entries[0];
|
|
13485
13566
|
if (
|
|
13486
13567
|
this.settings.get("retry.fallbackRevertPolicy") === "cooldown-expiry" &&
|
|
13487
|
-
controller.activeIndex > 0
|
|
13488
|
-
|
|
13489
|
-
|
|
13568
|
+
controller.activeIndex > 0 &&
|
|
13569
|
+
primarySelector &&
|
|
13570
|
+
this.#fallbackSuppressedSelectors.has(primarySelector) &&
|
|
13571
|
+
!this.#modelRegistry.isSelectorSuppressed(primarySelector)
|
|
13490
13572
|
) {
|
|
13573
|
+
this.#fallbackSuppressedSelectors.delete(primarySelector);
|
|
13491
13574
|
controller.resetForNewTurn();
|
|
13492
13575
|
}
|
|
13493
13576
|
}
|
|
@@ -13540,7 +13623,9 @@ export class AgentSession {
|
|
|
13540
13623
|
const existing = this.#defaultFallbackController;
|
|
13541
13624
|
if (
|
|
13542
13625
|
existing &&
|
|
13543
|
-
(existing.chain.origin === "runtime" ||
|
|
13626
|
+
(existing.chain.origin === "runtime" ||
|
|
13627
|
+
(existing.activeIndex > 0 && existing.chain.entries.length > 1) ||
|
|
13628
|
+
existing.chain.entries.join("\u0000") === chain.entries.join("\u0000"))
|
|
13544
13629
|
) {
|
|
13545
13630
|
return existing;
|
|
13546
13631
|
}
|
|
@@ -13826,6 +13911,7 @@ export class AgentSession {
|
|
|
13826
13911
|
|
|
13827
13912
|
if (managedFallback && trigger.class === "rate_limit" && trigger.retryAfterMs !== undefined && failedSelector) {
|
|
13828
13913
|
this.#modelRegistry.suppressSelector(failedSelector, Date.now() + trigger.retryAfterMs);
|
|
13914
|
+
this.#fallbackSuppressedSelectors.add(failedSelector);
|
|
13829
13915
|
}
|
|
13830
13916
|
|
|
13831
13917
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
@@ -15054,14 +15140,22 @@ export class AgentSession {
|
|
|
15054
15140
|
* Switch to a different session file.
|
|
15055
15141
|
* Aborts current operation, loads messages, restores model/thinking.
|
|
15056
15142
|
* Listeners are preserved and will continue receiving events.
|
|
15143
|
+
* `requireIdle` rejects an active turn and fences new admissions for the transition.
|
|
15057
15144
|
* @returns true if switch completed, false if cancelled by hook
|
|
15058
15145
|
*/
|
|
15059
15146
|
async switchSession(
|
|
15060
15147
|
sessionPath: string,
|
|
15061
|
-
options?: { transition?: SessionSwitchEvent["transition"] },
|
|
15148
|
+
options?: { transition?: SessionSwitchEvent["transition"]; requireIdle?: boolean },
|
|
15062
15149
|
): Promise<boolean> {
|
|
15063
15150
|
this.#beginSessionTransition("switch-session");
|
|
15064
15151
|
try {
|
|
15152
|
+
if (options?.requireIdle) {
|
|
15153
|
+
this.#sessionTurnAdmissionFenced = true;
|
|
15154
|
+
if (this.isStreaming || this.#activeSessionAdmission !== undefined)
|
|
15155
|
+
throw Object.assign(new AgentBusyError("Cannot switch sessions while a response or turn is active."), {
|
|
15156
|
+
code: "busy",
|
|
15157
|
+
});
|
|
15158
|
+
}
|
|
15065
15159
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
15066
15160
|
const switchingToDifferentSession = previousSessionFile
|
|
15067
15161
|
? path.resolve(previousSessionFile) !== path.resolve(sessionPath)
|
|
@@ -15287,6 +15381,7 @@ export class AgentSession {
|
|
|
15287
15381
|
throw error;
|
|
15288
15382
|
}
|
|
15289
15383
|
} finally {
|
|
15384
|
+
this.#sessionTurnAdmissionFenced = false;
|
|
15290
15385
|
this.#endSessionTransition();
|
|
15291
15386
|
}
|
|
15292
15387
|
}
|
|
@@ -702,7 +702,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
702
702
|
provenance,
|
|
703
703
|
migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
|
|
704
704
|
identity: {
|
|
705
|
-
canonicalPath:
|
|
705
|
+
canonicalPath: canonicalizeTrustedPath(filePath),
|
|
706
706
|
sessionId: header.id,
|
|
707
707
|
...snapshot.identity,
|
|
708
708
|
mtimeMs: Number(named.mtimeMs),
|
|
@@ -740,6 +740,7 @@ function listDirectoryCandidates(
|
|
|
740
740
|
directory: string,
|
|
741
741
|
provenance: "v2" | "legacy",
|
|
742
742
|
scope: ManagedScope,
|
|
743
|
+
maxCandidates?: number,
|
|
743
744
|
): readonly CandidateInspection[] {
|
|
744
745
|
let directoryStat: fs.Stats;
|
|
745
746
|
try {
|
|
@@ -749,8 +750,28 @@ function listDirectoryCandidates(
|
|
|
749
750
|
throw error;
|
|
750
751
|
}
|
|
751
752
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error("unsafe directory");
|
|
752
|
-
|
|
753
|
-
|
|
753
|
+
let entries: fs.Dirent[];
|
|
754
|
+
if (maxCandidates === undefined) {
|
|
755
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
756
|
+
} else {
|
|
757
|
+
entries = [];
|
|
758
|
+
let scannedEntries = 0;
|
|
759
|
+
const handle = fs.opendirSync(directory);
|
|
760
|
+
try {
|
|
761
|
+
for (;;) {
|
|
762
|
+
const entry = handle.readSync();
|
|
763
|
+
if (!entry) break;
|
|
764
|
+
scannedEntries++;
|
|
765
|
+
if (scannedEntries > maxCandidates * 4 + 128) throw new Error("candidate_capacity_exceeded");
|
|
766
|
+
if (!entry.name.endsWith(".jsonl")) continue;
|
|
767
|
+
entries.push(entry);
|
|
768
|
+
if (entries.length > maxCandidates) throw new Error("candidate_capacity_exceeded");
|
|
769
|
+
}
|
|
770
|
+
} finally {
|
|
771
|
+
handle.closeSync();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return entries
|
|
754
775
|
.filter(entry => entry.name.endsWith(".jsonl"))
|
|
755
776
|
.map(entry => {
|
|
756
777
|
const filePath = path.join(directory, entry.name);
|
|
@@ -967,7 +988,10 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
967
988
|
}
|
|
968
989
|
}
|
|
969
990
|
|
|
970
|
-
export function listManagedCandidates(
|
|
991
|
+
export function listManagedCandidates(
|
|
992
|
+
scope: ManagedScope,
|
|
993
|
+
options?: { maxCandidates?: number },
|
|
994
|
+
): ManagedCandidateListing {
|
|
971
995
|
try {
|
|
972
996
|
let root: fs.Stats;
|
|
973
997
|
try {
|
|
@@ -990,8 +1014,19 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
990
1014
|
})),
|
|
991
1015
|
];
|
|
992
1016
|
const seen = new Set<string>();
|
|
1017
|
+
let inspectedCandidates = 0;
|
|
993
1018
|
for (const directory of directories) {
|
|
994
|
-
|
|
1019
|
+
const remainingCandidates =
|
|
1020
|
+
options?.maxCandidates === undefined ? undefined : options.maxCandidates - inspectedCandidates;
|
|
1021
|
+
for (const candidate of listDirectoryCandidates(
|
|
1022
|
+
directory.path,
|
|
1023
|
+
directory.provenance,
|
|
1024
|
+
scope,
|
|
1025
|
+
remainingCandidates,
|
|
1026
|
+
)) {
|
|
1027
|
+
inspectedCandidates++;
|
|
1028
|
+
if (options?.maxCandidates !== undefined && inspectedCandidates > options.maxCandidates)
|
|
1029
|
+
throw new Error("candidate_capacity_exceeded");
|
|
995
1030
|
if ("code" in candidate) {
|
|
996
1031
|
invalid.push({ code: candidate.code });
|
|
997
1032
|
continue;
|
|
@@ -1108,7 +1143,7 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
|
|
|
1108
1143
|
|
|
1109
1144
|
function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: ResumeSessionIdentity): boolean {
|
|
1110
1145
|
return (
|
|
1111
|
-
|
|
1146
|
+
canonicalizeTrustedPath(candidate.identity.canonicalPath) === canonicalizeTrustedPath(expected.canonicalPath) &&
|
|
1112
1147
|
candidate.identity.sessionId === expected.sessionId &&
|
|
1113
1148
|
candidate.identity.dev === expected.dev &&
|
|
1114
1149
|
candidate.identity.ino === expected.ino &&
|
|
@@ -880,6 +880,20 @@ export class ManagedSessionDescendantStore {
|
|
|
880
880
|
};
|
|
881
881
|
}
|
|
882
882
|
|
|
883
|
+
/** Read only the exact managed file size through the retained root authority. */
|
|
884
|
+
sizeSync(relativePath: string): number | null {
|
|
885
|
+
this.#assertBound();
|
|
886
|
+
if (!this.#authority) throw new Error("managed_size_authority_unavailable");
|
|
887
|
+
const relative = this.#relative(this.#resolve(relativePath));
|
|
888
|
+
const result = this.#authority.stat(relative);
|
|
889
|
+
if (!result.ok) {
|
|
890
|
+
if (result.code === "not_found") return null;
|
|
891
|
+
throw new Error(result.code ?? "managed_size_failed");
|
|
892
|
+
}
|
|
893
|
+
if (!result.identity) throw new Error("Managed descendant identity is unavailable");
|
|
894
|
+
return Number(result.identity.size);
|
|
895
|
+
}
|
|
896
|
+
|
|
883
897
|
/** Remove an exact captured file without reopening its pathname as authority. */
|
|
884
898
|
removeExpected(relativePath: string, expected: ManagedFileSnapshot): void {
|
|
885
899
|
this.#assertBound();
|