@sayknow-cli/coding-agent 0.5.2 → 0.5.8
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 +66 -16
- package/dist/types/cli/update-cli.d.ts +8 -1
- 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 +73 -0
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- 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/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -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/launch-worktree.d.ts +17 -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/system-prompt.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +5 -0
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +13 -7
- package/scripts/compile-args.ts +8 -4
- 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/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +84 -16
- package/src/config/model-registry.ts +60 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +79 -0
- package/src/config/settings.ts +13 -2
- package/src/dap/client.ts +78 -30
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/welcome.ts +5 -0
- 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/types.ts +5 -1
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +27 -18
- package/src/session/agent-session.ts +155 -15
- 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/launch-worktree.ts +277 -4
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +100 -1
- package/src/slash-commands/types.ts +5 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/ask.ts +5 -0
- package/src/tools/fetch.ts +7 -2
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- 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
|
@@ -42,10 +42,13 @@ import {
|
|
|
42
42
|
} from "@sayknow-cli/agent-core";
|
|
43
43
|
import { normalizeMessagesForProvider } from "@sayknow-cli/agent-core/agent-loop";
|
|
44
44
|
import {
|
|
45
|
+
type AdaptiveCompactionOptions,
|
|
46
|
+
AdaptiveCompactionTracker,
|
|
45
47
|
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
|
46
48
|
CompactionCancelledError,
|
|
47
49
|
type CompactionPreparation,
|
|
48
50
|
type CompactionResult,
|
|
51
|
+
type CompactionSettings as CoreCompactionSettings,
|
|
49
52
|
calculateContextTokens,
|
|
50
53
|
calculatePromptTokens,
|
|
51
54
|
collectEntriesForBranchSummary,
|
|
@@ -545,6 +548,8 @@ export interface AgentSessionConfig {
|
|
|
545
548
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
546
549
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
547
550
|
requestedToolNames?: ReadonlySet<string>;
|
|
551
|
+
/** True only when the caller explicitly selected no tools. */
|
|
552
|
+
explicitlyDisabledTools?: boolean;
|
|
548
553
|
/** Optional per-session allowlist for tools exposed through search_tool_bm25. */
|
|
549
554
|
discoverableToolAllowedNames?: readonly string[];
|
|
550
555
|
/** Optional accessor for live MCP server instructions, injected as untrusted user-role request data. */
|
|
@@ -1748,6 +1753,7 @@ export class AgentSession {
|
|
|
1748
1753
|
// Handoff state
|
|
1749
1754
|
#handoffAbortController: AbortController | undefined = undefined;
|
|
1750
1755
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
|
|
1756
|
+
#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
1751
1757
|
|
|
1752
1758
|
// Retry state
|
|
1753
1759
|
#retryAbortController: AbortController | undefined = undefined;
|
|
@@ -1759,6 +1765,7 @@ export class AgentSession {
|
|
|
1759
1765
|
#overflowMaintenanceAttempts = 0;
|
|
1760
1766
|
#defaultFallbackExhaustedLastTurn = false;
|
|
1761
1767
|
#fallbackInvocationId = 0;
|
|
1768
|
+
#fallbackSuppressedSelectors = new Set<string>();
|
|
1762
1769
|
// Todo completion reminder state
|
|
1763
1770
|
#todoReminderCount = 0;
|
|
1764
1771
|
#todoWriteFailureCount = 0;
|
|
@@ -1853,6 +1860,7 @@ export class AgentSession {
|
|
|
1853
1860
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1854
1861
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1855
1862
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1863
|
+
#explicitlyDisabledTools: boolean;
|
|
1856
1864
|
#baseSystemPrompt: string[];
|
|
1857
1865
|
#initialWorkspaceTree: WorkspaceTree | undefined;
|
|
1858
1866
|
/** Throttle cache for the per-turn volatile workspace-tree scan (see #buildVolatileProjectContextMessage). */
|
|
@@ -2043,6 +2051,12 @@ export class AgentSession {
|
|
|
2043
2051
|
});
|
|
2044
2052
|
}
|
|
2045
2053
|
|
|
2054
|
+
#sessionTransitionAdmissionBusyError(): AgentBusyError {
|
|
2055
|
+
return Object.assign(new AgentBusyError("Cannot start a turn while a session transition is in progress."), {
|
|
2056
|
+
code: "busy",
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2046
2060
|
/**
|
|
2047
2061
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
2048
2062
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -2068,6 +2082,7 @@ export class AgentSession {
|
|
|
2068
2082
|
* orchestrator does not hold it), so there is no self-deadlock.
|
|
2069
2083
|
*/
|
|
2070
2084
|
#sessionTransitionKind: string | undefined;
|
|
2085
|
+
#sessionTurnAdmissionFenced = false;
|
|
2071
2086
|
|
|
2072
2087
|
#beginSessionTransition(kind: string): void {
|
|
2073
2088
|
if (this.#sessionTransitionKind !== undefined) {
|
|
@@ -2108,6 +2123,7 @@ export class AgentSession {
|
|
|
2108
2123
|
code: "busy",
|
|
2109
2124
|
});
|
|
2110
2125
|
}
|
|
2126
|
+
if (this.#sessionTurnAdmissionFenced) throw this.#sessionTransitionAdmissionBusyError();
|
|
2111
2127
|
|
|
2112
2128
|
const entry: SessionAdmissionEntry = {
|
|
2113
2129
|
kind,
|
|
@@ -2136,6 +2152,13 @@ export class AgentSession {
|
|
|
2136
2152
|
code: "busy",
|
|
2137
2153
|
});
|
|
2138
2154
|
}
|
|
2155
|
+
if (this.#sessionTurnAdmissionFenced) {
|
|
2156
|
+
entry.released = true;
|
|
2157
|
+
entry.settled.resolve();
|
|
2158
|
+
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
2159
|
+
this.#activateNextSessionAdmission();
|
|
2160
|
+
throw this.#sessionTransitionAdmissionBusyError();
|
|
2161
|
+
}
|
|
2139
2162
|
|
|
2140
2163
|
const release = () => {
|
|
2141
2164
|
if (entry.released) return;
|
|
@@ -2372,6 +2395,7 @@ export class AgentSession {
|
|
|
2372
2395
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2373
2396
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2374
2397
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2398
|
+
this.#explicitlyDisabledTools = config.explicitlyDisabledTools === true;
|
|
2375
2399
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2376
2400
|
this.#onPayload = config.onPayload;
|
|
2377
2401
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
@@ -3779,8 +3803,9 @@ export class AgentSession {
|
|
|
3779
3803
|
this.#resolveTtsrResume();
|
|
3780
3804
|
}
|
|
3781
3805
|
this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
|
|
3782
|
-
if (this.#handoffAbortController) {
|
|
3806
|
+
if (this.#handoffAbortController || this.#suppressNextPostTurnMaintenanceAfterAutoHandoff) {
|
|
3783
3807
|
this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
|
|
3808
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
3784
3809
|
}
|
|
3785
3810
|
if (
|
|
3786
3811
|
assistantMsg.stopReason !== "error" &&
|
|
@@ -5872,6 +5897,7 @@ export class AgentSession {
|
|
|
5872
5897
|
}
|
|
5873
5898
|
|
|
5874
5899
|
async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
|
|
5900
|
+
if (this.#explicitlyDisabledTools) return [];
|
|
5875
5901
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
5876
5902
|
const previousSelectedDiscoveredBuiltinToolNames = this.#getSelectedDiscoveredBuiltinToolNames();
|
|
5877
5903
|
const nextActiveToolNames = this.getActiveToolNames();
|
|
@@ -6099,7 +6125,9 @@ export class AgentSession {
|
|
|
6099
6125
|
nextSelectedDiscoveredBuiltinToolNames?: string[];
|
|
6100
6126
|
},
|
|
6101
6127
|
): Promise<void> {
|
|
6102
|
-
toolNames =
|
|
6128
|
+
toolNames = this.#explicitlyDisabledTools
|
|
6129
|
+
? []
|
|
6130
|
+
: [...new Set([...toolNames.map(name => name.toLowerCase()), ...this.#mandatoryMCPToolNames])];
|
|
6103
6131
|
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
6104
6132
|
const previousSelectedDiscoveredBuiltinToolNames =
|
|
6105
6133
|
options?.previousSelectedDiscoveredBuiltinToolNames ?? this.#getSelectedDiscoveredBuiltinToolNames();
|
|
@@ -6209,6 +6237,11 @@ export class AgentSession {
|
|
|
6209
6237
|
await this.#applyActiveToolsByName(nextActive);
|
|
6210
6238
|
}
|
|
6211
6239
|
|
|
6240
|
+
/** Whether the caller explicitly selected no tools for this session. */
|
|
6241
|
+
hasExplicitlyDisabledTools(): boolean {
|
|
6242
|
+
return this.#explicitlyDisabledTools;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6212
6245
|
/**
|
|
6213
6246
|
* Set active tools by name.
|
|
6214
6247
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
@@ -6220,6 +6253,11 @@ export class AgentSession {
|
|
|
6220
6253
|
}
|
|
6221
6254
|
|
|
6222
6255
|
async #restoreMCPSelectionsForSessionContext(sessionContext: SessionContext): Promise<void> {
|
|
6256
|
+
if (this.#explicitlyDisabledTools) {
|
|
6257
|
+
this.#selectedDiscoveredToolNames.clear();
|
|
6258
|
+
await this.#applyActiveToolsByName([], { persistMCPSelection: false });
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6223
6261
|
if (!this.#mcpDiscoveryEnabled && this.#resolveEffectiveDiscoveryMode() !== "all") {
|
|
6224
6262
|
await this.#attachAskToolIfWorkflowActive();
|
|
6225
6263
|
return;
|
|
@@ -7073,6 +7111,7 @@ export class AgentSession {
|
|
|
7073
7111
|
this.#toolRegistry.set(askTool.name, askTool);
|
|
7074
7112
|
}
|
|
7075
7113
|
|
|
7114
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7076
7115
|
try {
|
|
7077
7116
|
if ((this.#workflowGateEmitter?.listPendingGates?.().length ?? 0) > 0) {
|
|
7078
7117
|
this.#attachAskTool();
|
|
@@ -7118,6 +7157,7 @@ export class AgentSession {
|
|
|
7118
7157
|
}
|
|
7119
7158
|
|
|
7120
7159
|
#attachAskTool(): void {
|
|
7160
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7121
7161
|
const askTool = this.#toolRegistry.get("ask");
|
|
7122
7162
|
if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
|
|
7123
7163
|
this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
|
|
@@ -7200,6 +7240,7 @@ export class AgentSession {
|
|
|
7200
7240
|
}
|
|
7201
7241
|
|
|
7202
7242
|
async #activatePendingSkcGoalModeRequest(): Promise<boolean> {
|
|
7243
|
+
if (this.#explicitlyDisabledTools) return false;
|
|
7203
7244
|
if (!this.settings.get("goal.enabled")) return false;
|
|
7204
7245
|
const pendingGoal = await consumePendingGoalModeRequest(
|
|
7205
7246
|
this.sessionManager.getCwd(),
|
|
@@ -7212,9 +7253,10 @@ export class AgentSession {
|
|
|
7212
7253
|
}
|
|
7213
7254
|
|
|
7214
7255
|
const previousTools = this.getActiveToolNames();
|
|
7215
|
-
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
7216
7256
|
await this.#goalRuntime.createGoal({ objective: pendingGoal.objective, provenance: pendingGoal.provenance });
|
|
7217
|
-
|
|
7257
|
+
if (!this.#explicitlyDisabledTools) {
|
|
7258
|
+
await this.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
7259
|
+
}
|
|
7218
7260
|
if (this.isStreaming) {
|
|
7219
7261
|
await this.sendGoalModeContext({ deliverAs: "steer" });
|
|
7220
7262
|
}
|
|
@@ -9472,7 +9514,6 @@ export class AgentSession {
|
|
|
9472
9514
|
persistMCPSelection: false,
|
|
9473
9515
|
nextSelectedDiscoveredBuiltinToolNames: [],
|
|
9474
9516
|
});
|
|
9475
|
-
} else {
|
|
9476
9517
|
}
|
|
9477
9518
|
}
|
|
9478
9519
|
this.#todoReminderCount = 0;
|
|
@@ -10066,7 +10107,13 @@ export class AgentSession {
|
|
|
10066
10107
|
});
|
|
10067
10108
|
if (!resolved.model) continue;
|
|
10068
10109
|
|
|
10069
|
-
if (
|
|
10110
|
+
if (
|
|
10111
|
+
roleModels.some(
|
|
10112
|
+
candidate =>
|
|
10113
|
+
modelsAreEqual(candidate.model, resolved.model) && candidate.thinkingLevel === resolved.thinkingLevel,
|
|
10114
|
+
)
|
|
10115
|
+
)
|
|
10116
|
+
continue;
|
|
10070
10117
|
roleModels.push({
|
|
10071
10118
|
role,
|
|
10072
10119
|
model: resolved.model,
|
|
@@ -10726,6 +10773,43 @@ export class AgentSession {
|
|
|
10726
10773
|
return 0;
|
|
10727
10774
|
}
|
|
10728
10775
|
|
|
10776
|
+
/**
|
|
10777
|
+
* Call-rate tracker backing opt-in adaptive compaction. Always maintained so
|
|
10778
|
+
* enabling the setting mid-session has immediate, honest state; the recorded
|
|
10779
|
+
* rate only reaches a threshold decision while `compaction.adaptive.enabled`
|
|
10780
|
+
* is true.
|
|
10781
|
+
*/
|
|
10782
|
+
#adaptiveCompaction = new AdaptiveCompactionTracker();
|
|
10783
|
+
|
|
10784
|
+
#adaptiveCompactionOptions(): AdaptiveCompactionOptions {
|
|
10785
|
+
return {
|
|
10786
|
+
enabled: this.settings.get("compaction.adaptive.enabled"),
|
|
10787
|
+
baseThresholdPercent: this.settings.get("compaction.adaptive.baseThresholdPercent"),
|
|
10788
|
+
aggression: this.settings.get("compaction.adaptive.aggression"),
|
|
10789
|
+
turnWindow: this.settings.get("compaction.adaptive.turnWindow"),
|
|
10790
|
+
minThresholdPercent: this.settings.get("compaction.adaptive.minThresholdPercent"),
|
|
10791
|
+
};
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
/**
|
|
10795
|
+
* Compaction settings for a threshold decision.
|
|
10796
|
+
*
|
|
10797
|
+
* `getGroup("compaction")` flattens `compaction.adaptive.*` into dotted keys,
|
|
10798
|
+
* so the nested options object is assembled explicitly here. The tracker
|
|
10799
|
+
* window follows the configured minutes so a settings change takes effect
|
|
10800
|
+
* without restarting the session.
|
|
10801
|
+
*/
|
|
10802
|
+
#compactionSettingsWithAdaptive(): CoreCompactionSettings {
|
|
10803
|
+
const adaptive = this.#adaptiveCompactionOptions();
|
|
10804
|
+
const windowMinutes = Number.isFinite(adaptive.turnWindow) ? Math.max(1, adaptive.turnWindow) : 15;
|
|
10805
|
+
this.#adaptiveCompaction.setWindowMs(windowMinutes * 60_000);
|
|
10806
|
+
return {
|
|
10807
|
+
...this.settings.getGroup("compaction"),
|
|
10808
|
+
adaptive,
|
|
10809
|
+
adaptiveState: this.#adaptiveCompaction.decisionState(),
|
|
10810
|
+
};
|
|
10811
|
+
}
|
|
10812
|
+
|
|
10729
10813
|
/**
|
|
10730
10814
|
* Evidence-gated below-threshold maintenance pruning (Finding 13). Runs
|
|
10731
10815
|
* #pruneToolOutputs ONCE, below the compaction threshold, only when it is
|
|
@@ -11392,13 +11476,15 @@ export class AgentSession {
|
|
|
11392
11476
|
}
|
|
11393
11477
|
return this.#scheduleOverflowRetryContinuation(generation);
|
|
11394
11478
|
}
|
|
11395
|
-
const compactionSettings = this
|
|
11479
|
+
const compactionSettings = this.#compactionSettingsWithAdaptive();
|
|
11396
11480
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
11397
11481
|
|
|
11398
11482
|
// Case 2: Threshold - turn succeeded but context is getting large
|
|
11399
11483
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
11400
11484
|
if (assistantMessage.stopReason === "error") return false;
|
|
11401
11485
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
11486
|
+
this.#adaptiveCompaction.recordCall(contextTokens);
|
|
11487
|
+
compactionSettings.adaptiveState = this.#adaptiveCompaction.decisionState();
|
|
11402
11488
|
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
11403
11489
|
// Auto maintenance should track actual context fullness.
|
|
11404
11490
|
const autoCompactionOutputReserveTokens = 0;
|
|
@@ -11626,6 +11712,11 @@ export class AgentSession {
|
|
|
11626
11712
|
this.#resourceSampler = sampler;
|
|
11627
11713
|
}
|
|
11628
11714
|
|
|
11715
|
+
/** Test seam: drive the pre-prompt emergency/threshold check directly. */
|
|
11716
|
+
runPrePromptContextCheckForTests(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
11717
|
+
return this.#checkEstimatedContextBeforePrompt(pendingMessages);
|
|
11718
|
+
}
|
|
11719
|
+
|
|
11629
11720
|
setRetainedMemorySampler(sampler: (() => RetainedMemorySample) | undefined): void {
|
|
11630
11721
|
this.#retainedMemorySampler = sampler;
|
|
11631
11722
|
}
|
|
@@ -11659,26 +11750,31 @@ export class AgentSession {
|
|
|
11659
11750
|
materializedResidentBytes: this.#streamingEditFileCache.totalBytes,
|
|
11660
11751
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
11661
11752
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
11753
|
+
transcriptFileBytes: this.sessionManager.getTranscriptFileBytes(),
|
|
11662
11754
|
};
|
|
11663
11755
|
}
|
|
11664
11756
|
|
|
11665
11757
|
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
11666
11758
|
const model = this.model;
|
|
11667
11759
|
if (!model) return;
|
|
11668
|
-
const contextWindow = model.contextWindow ?? 0;
|
|
11669
|
-
if (contextWindow <= 0) return;
|
|
11670
11760
|
// F6: non-disableable emergency floor — compact before OOM even when token-based
|
|
11671
11761
|
// compaction is disabled or its threshold is set too high (weak-hardware protection).
|
|
11672
11762
|
const emergencyReason = emergencyCompactionReason(this.#resourceSampler());
|
|
11673
11763
|
if (emergencyReason) {
|
|
11674
11764
|
logger.warn("Emergency compaction triggered (resource floor exceeded)", { reason: emergencyReason });
|
|
11675
|
-
await this.#runAutoCompaction("overflow", false, false, {
|
|
11765
|
+
const status = await this.#runAutoCompaction("overflow", false, false, {
|
|
11676
11766
|
continueAfterMaintenance: false,
|
|
11677
11767
|
deferHandoffMaintenance: false,
|
|
11678
11768
|
force: true,
|
|
11679
11769
|
});
|
|
11770
|
+
if (emergencyReason === "transcriptFile" && status.kind === "skipped") {
|
|
11771
|
+
logger.warn("Transcript emergency compaction was skipped; rewriting exact live entries");
|
|
11772
|
+
await this.sessionManager.rewriteEntries();
|
|
11773
|
+
}
|
|
11680
11774
|
return;
|
|
11681
11775
|
}
|
|
11776
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
11777
|
+
if (contextWindow <= 0) return;
|
|
11682
11778
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11683
11779
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
11684
11780
|
|
|
@@ -11712,6 +11808,7 @@ export class AgentSession {
|
|
|
11712
11808
|
await this.#runAutoCompaction("threshold", false, false, {
|
|
11713
11809
|
continueAfterMaintenance: false,
|
|
11714
11810
|
deferHandoffMaintenance: false,
|
|
11811
|
+
suppressNextPostTurnMaintenanceAfterHandoff: true,
|
|
11715
11812
|
});
|
|
11716
11813
|
}
|
|
11717
11814
|
}
|
|
@@ -11803,6 +11900,7 @@ export class AgentSession {
|
|
|
11803
11900
|
return;
|
|
11804
11901
|
}
|
|
11805
11902
|
|
|
11903
|
+
if (this.#explicitlyDisabledTools) return;
|
|
11806
11904
|
this.#attachAskTool();
|
|
11807
11905
|
|
|
11808
11906
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
@@ -12691,6 +12789,11 @@ export class AgentSession {
|
|
|
12691
12789
|
|
|
12692
12790
|
/**
|
|
12693
12791
|
* Internal: Run auto-compaction with events.
|
|
12792
|
+
*
|
|
12793
|
+
* Wraps the run so a completed compaction resets the adaptive call-rate
|
|
12794
|
+
* tracker in exactly one place. The post-compaction context size is reported
|
|
12795
|
+
* as 0 because the next recorded call carries the real provider number; the
|
|
12796
|
+
* tracker only uses this value as a fallback for callers that pass none.
|
|
12694
12797
|
*/
|
|
12695
12798
|
async #runAutoCompaction(
|
|
12696
12799
|
reason: "overflow" | "threshold" | "idle",
|
|
@@ -12702,6 +12805,25 @@ export class AgentSession {
|
|
|
12702
12805
|
force?: boolean;
|
|
12703
12806
|
signal?: AbortSignal;
|
|
12704
12807
|
beforeTerminalOverflowNoop?: () => void;
|
|
12808
|
+
suppressNextPostTurnMaintenanceAfterHandoff?: boolean;
|
|
12809
|
+
},
|
|
12810
|
+
): Promise<AutoCompactionTerminalStatus> {
|
|
12811
|
+
const status = await this.#runAutoCompactionInner(reason, willRetry, deferred, options);
|
|
12812
|
+
if (status.kind === "compacted") this.#adaptiveCompaction.recordCompact(0);
|
|
12813
|
+
return status;
|
|
12814
|
+
}
|
|
12815
|
+
|
|
12816
|
+
async #runAutoCompactionInner(
|
|
12817
|
+
reason: "overflow" | "threshold" | "idle",
|
|
12818
|
+
willRetry: boolean,
|
|
12819
|
+
deferred = false,
|
|
12820
|
+
options?: {
|
|
12821
|
+
continueAfterMaintenance?: boolean;
|
|
12822
|
+
deferHandoffMaintenance?: boolean;
|
|
12823
|
+
force?: boolean;
|
|
12824
|
+
signal?: AbortSignal;
|
|
12825
|
+
beforeTerminalOverflowNoop?: () => void;
|
|
12826
|
+
suppressNextPostTurnMaintenanceAfterHandoff?: boolean;
|
|
12705
12827
|
},
|
|
12706
12828
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12707
12829
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12783,6 +12905,9 @@ export class AgentSession {
|
|
|
12783
12905
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12784
12906
|
|
|
12785
12907
|
if (handoffResult) {
|
|
12908
|
+
if (options?.suppressNextPostTurnMaintenanceAfterHandoff) {
|
|
12909
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = true;
|
|
12910
|
+
}
|
|
12786
12911
|
await this.#emitSessionEvent({
|
|
12787
12912
|
type: "auto_compaction_end",
|
|
12788
12913
|
action,
|
|
@@ -13502,12 +13627,15 @@ export class AgentSession {
|
|
|
13502
13627
|
if (controller.chain.entries.length > 1) await this.#advanceDefaultFallback(controller, "new_turn", 0);
|
|
13503
13628
|
return;
|
|
13504
13629
|
}
|
|
13630
|
+
const primarySelector = controller.chain.entries[0];
|
|
13505
13631
|
if (
|
|
13506
13632
|
this.settings.get("retry.fallbackRevertPolicy") === "cooldown-expiry" &&
|
|
13507
|
-
controller.activeIndex > 0
|
|
13508
|
-
|
|
13509
|
-
|
|
13633
|
+
controller.activeIndex > 0 &&
|
|
13634
|
+
primarySelector &&
|
|
13635
|
+
this.#fallbackSuppressedSelectors.has(primarySelector) &&
|
|
13636
|
+
!this.#modelRegistry.isSelectorSuppressed(primarySelector)
|
|
13510
13637
|
) {
|
|
13638
|
+
this.#fallbackSuppressedSelectors.delete(primarySelector);
|
|
13511
13639
|
controller.resetForNewTurn();
|
|
13512
13640
|
}
|
|
13513
13641
|
}
|
|
@@ -13560,7 +13688,9 @@ export class AgentSession {
|
|
|
13560
13688
|
const existing = this.#defaultFallbackController;
|
|
13561
13689
|
if (
|
|
13562
13690
|
existing &&
|
|
13563
|
-
(existing.chain.origin === "runtime" ||
|
|
13691
|
+
(existing.chain.origin === "runtime" ||
|
|
13692
|
+
(existing.activeIndex > 0 && existing.chain.entries.length > 1) ||
|
|
13693
|
+
existing.chain.entries.join("\u0000") === chain.entries.join("\u0000"))
|
|
13564
13694
|
) {
|
|
13565
13695
|
return existing;
|
|
13566
13696
|
}
|
|
@@ -13846,6 +13976,7 @@ export class AgentSession {
|
|
|
13846
13976
|
|
|
13847
13977
|
if (managedFallback && trigger.class === "rate_limit" && trigger.retryAfterMs !== undefined && failedSelector) {
|
|
13848
13978
|
this.#modelRegistry.suppressSelector(failedSelector, Date.now() + trigger.retryAfterMs);
|
|
13979
|
+
this.#fallbackSuppressedSelectors.add(failedSelector);
|
|
13849
13980
|
}
|
|
13850
13981
|
|
|
13851
13982
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
@@ -15074,14 +15205,22 @@ export class AgentSession {
|
|
|
15074
15205
|
* Switch to a different session file.
|
|
15075
15206
|
* Aborts current operation, loads messages, restores model/thinking.
|
|
15076
15207
|
* Listeners are preserved and will continue receiving events.
|
|
15208
|
+
* `requireIdle` rejects an active turn and fences new admissions for the transition.
|
|
15077
15209
|
* @returns true if switch completed, false if cancelled by hook
|
|
15078
15210
|
*/
|
|
15079
15211
|
async switchSession(
|
|
15080
15212
|
sessionPath: string,
|
|
15081
|
-
options?: { transition?: SessionSwitchEvent["transition"] },
|
|
15213
|
+
options?: { transition?: SessionSwitchEvent["transition"]; requireIdle?: boolean },
|
|
15082
15214
|
): Promise<boolean> {
|
|
15083
15215
|
this.#beginSessionTransition("switch-session");
|
|
15084
15216
|
try {
|
|
15217
|
+
if (options?.requireIdle) {
|
|
15218
|
+
this.#sessionTurnAdmissionFenced = true;
|
|
15219
|
+
if (this.isStreaming || this.#activeSessionAdmission !== undefined)
|
|
15220
|
+
throw Object.assign(new AgentBusyError("Cannot switch sessions while a response or turn is active."), {
|
|
15221
|
+
code: "busy",
|
|
15222
|
+
});
|
|
15223
|
+
}
|
|
15085
15224
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
15086
15225
|
const switchingToDifferentSession = previousSessionFile
|
|
15087
15226
|
? path.resolve(previousSessionFile) !== path.resolve(sessionPath)
|
|
@@ -15307,6 +15446,7 @@ export class AgentSession {
|
|
|
15307
15446
|
throw error;
|
|
15308
15447
|
}
|
|
15309
15448
|
} finally {
|
|
15449
|
+
this.#sessionTurnAdmissionFenced = false;
|
|
15310
15450
|
this.#endSessionTransition();
|
|
15311
15451
|
}
|
|
15312
15452
|
}
|
|
@@ -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();
|