@sayknow-cli/coding-agent 0.5.2 → 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 +55 -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 +9 -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/tools/ask.d.ts +5 -0
- package/package.json +13 -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/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 +60 -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/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/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 +25 -18
- package/src/session/agent-session.ts +89 -14
- 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/tools/ask.ts +5 -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
|
@@ -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. */
|
|
@@ -1748,6 +1750,7 @@ export class AgentSession {
|
|
|
1748
1750
|
// Handoff state
|
|
1749
1751
|
#handoffAbortController: AbortController | undefined = undefined;
|
|
1750
1752
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
|
|
1753
|
+
#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
1751
1754
|
|
|
1752
1755
|
// Retry state
|
|
1753
1756
|
#retryAbortController: AbortController | undefined = undefined;
|
|
@@ -1759,6 +1762,7 @@ export class AgentSession {
|
|
|
1759
1762
|
#overflowMaintenanceAttempts = 0;
|
|
1760
1763
|
#defaultFallbackExhaustedLastTurn = false;
|
|
1761
1764
|
#fallbackInvocationId = 0;
|
|
1765
|
+
#fallbackSuppressedSelectors = new Set<string>();
|
|
1762
1766
|
// Todo completion reminder state
|
|
1763
1767
|
#todoReminderCount = 0;
|
|
1764
1768
|
#todoWriteFailureCount = 0;
|
|
@@ -1853,6 +1857,7 @@ export class AgentSession {
|
|
|
1853
1857
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1854
1858
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1855
1859
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1860
|
+
#explicitlyDisabledTools: boolean;
|
|
1856
1861
|
#baseSystemPrompt: string[];
|
|
1857
1862
|
#initialWorkspaceTree: WorkspaceTree | undefined;
|
|
1858
1863
|
/** Throttle cache for the per-turn volatile workspace-tree scan (see #buildVolatileProjectContextMessage). */
|
|
@@ -2043,6 +2048,12 @@ export class AgentSession {
|
|
|
2043
2048
|
});
|
|
2044
2049
|
}
|
|
2045
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
|
+
|
|
2046
2057
|
/**
|
|
2047
2058
|
* Reject a turn start while a handoff transition owns the session. Handoff never
|
|
2048
2059
|
* routes its own generation/injection through these turn-start chokepoints, and
|
|
@@ -2068,6 +2079,7 @@ export class AgentSession {
|
|
|
2068
2079
|
* orchestrator does not hold it), so there is no self-deadlock.
|
|
2069
2080
|
*/
|
|
2070
2081
|
#sessionTransitionKind: string | undefined;
|
|
2082
|
+
#sessionTurnAdmissionFenced = false;
|
|
2071
2083
|
|
|
2072
2084
|
#beginSessionTransition(kind: string): void {
|
|
2073
2085
|
if (this.#sessionTransitionKind !== undefined) {
|
|
@@ -2108,6 +2120,7 @@ export class AgentSession {
|
|
|
2108
2120
|
code: "busy",
|
|
2109
2121
|
});
|
|
2110
2122
|
}
|
|
2123
|
+
if (this.#sessionTurnAdmissionFenced) throw this.#sessionTransitionAdmissionBusyError();
|
|
2111
2124
|
|
|
2112
2125
|
const entry: SessionAdmissionEntry = {
|
|
2113
2126
|
kind,
|
|
@@ -2136,6 +2149,13 @@ export class AgentSession {
|
|
|
2136
2149
|
code: "busy",
|
|
2137
2150
|
});
|
|
2138
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
|
+
}
|
|
2139
2159
|
|
|
2140
2160
|
const release = () => {
|
|
2141
2161
|
if (entry.released) return;
|
|
@@ -2372,6 +2392,7 @@ export class AgentSession {
|
|
|
2372
2392
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2373
2393
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2374
2394
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2395
|
+
this.#explicitlyDisabledTools = config.explicitlyDisabledTools === true;
|
|
2375
2396
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
2376
2397
|
this.#onPayload = config.onPayload;
|
|
2377
2398
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
@@ -3779,8 +3800,9 @@ export class AgentSession {
|
|
|
3779
3800
|
this.#resolveTtsrResume();
|
|
3780
3801
|
}
|
|
3781
3802
|
this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
|
|
3782
|
-
if (this.#handoffAbortController) {
|
|
3803
|
+
if (this.#handoffAbortController || this.#suppressNextPostTurnMaintenanceAfterAutoHandoff) {
|
|
3783
3804
|
this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
|
|
3805
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = false;
|
|
3784
3806
|
}
|
|
3785
3807
|
if (
|
|
3786
3808
|
assistantMsg.stopReason !== "error" &&
|
|
@@ -5872,6 +5894,7 @@ export class AgentSession {
|
|
|
5872
5894
|
}
|
|
5873
5895
|
|
|
5874
5896
|
async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
|
|
5897
|
+
if (this.#explicitlyDisabledTools) return [];
|
|
5875
5898
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
5876
5899
|
const previousSelectedDiscoveredBuiltinToolNames = this.#getSelectedDiscoveredBuiltinToolNames();
|
|
5877
5900
|
const nextActiveToolNames = this.getActiveToolNames();
|
|
@@ -6099,7 +6122,9 @@ export class AgentSession {
|
|
|
6099
6122
|
nextSelectedDiscoveredBuiltinToolNames?: string[];
|
|
6100
6123
|
},
|
|
6101
6124
|
): Promise<void> {
|
|
6102
|
-
toolNames =
|
|
6125
|
+
toolNames = this.#explicitlyDisabledTools
|
|
6126
|
+
? []
|
|
6127
|
+
: [...new Set([...toolNames.map(name => name.toLowerCase()), ...this.#mandatoryMCPToolNames])];
|
|
6103
6128
|
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
6104
6129
|
const previousSelectedDiscoveredBuiltinToolNames =
|
|
6105
6130
|
options?.previousSelectedDiscoveredBuiltinToolNames ?? this.#getSelectedDiscoveredBuiltinToolNames();
|
|
@@ -6209,6 +6234,11 @@ export class AgentSession {
|
|
|
6209
6234
|
await this.#applyActiveToolsByName(nextActive);
|
|
6210
6235
|
}
|
|
6211
6236
|
|
|
6237
|
+
/** Whether the caller explicitly selected no tools for this session. */
|
|
6238
|
+
hasExplicitlyDisabledTools(): boolean {
|
|
6239
|
+
return this.#explicitlyDisabledTools;
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6212
6242
|
/**
|
|
6213
6243
|
* Set active tools by name.
|
|
6214
6244
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
@@ -6220,6 +6250,11 @@ export class AgentSession {
|
|
|
6220
6250
|
}
|
|
6221
6251
|
|
|
6222
6252
|
async #restoreMCPSelectionsForSessionContext(sessionContext: SessionContext): Promise<void> {
|
|
6253
|
+
if (this.#explicitlyDisabledTools) {
|
|
6254
|
+
this.#selectedDiscoveredToolNames.clear();
|
|
6255
|
+
await this.#applyActiveToolsByName([], { persistMCPSelection: false });
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6223
6258
|
if (!this.#mcpDiscoveryEnabled && this.#resolveEffectiveDiscoveryMode() !== "all") {
|
|
6224
6259
|
await this.#attachAskToolIfWorkflowActive();
|
|
6225
6260
|
return;
|
|
@@ -7073,6 +7108,7 @@ export class AgentSession {
|
|
|
7073
7108
|
this.#toolRegistry.set(askTool.name, askTool);
|
|
7074
7109
|
}
|
|
7075
7110
|
|
|
7111
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7076
7112
|
try {
|
|
7077
7113
|
if ((this.#workflowGateEmitter?.listPendingGates?.().length ?? 0) > 0) {
|
|
7078
7114
|
this.#attachAskTool();
|
|
@@ -7118,6 +7154,7 @@ export class AgentSession {
|
|
|
7118
7154
|
}
|
|
7119
7155
|
|
|
7120
7156
|
#attachAskTool(): void {
|
|
7157
|
+
if (this.#explicitlyDisabledTools) return;
|
|
7121
7158
|
const askTool = this.#toolRegistry.get("ask");
|
|
7122
7159
|
if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
|
|
7123
7160
|
this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
|
|
@@ -7200,6 +7237,7 @@ export class AgentSession {
|
|
|
7200
7237
|
}
|
|
7201
7238
|
|
|
7202
7239
|
async #activatePendingSkcGoalModeRequest(): Promise<boolean> {
|
|
7240
|
+
if (this.#explicitlyDisabledTools) return false;
|
|
7203
7241
|
if (!this.settings.get("goal.enabled")) return false;
|
|
7204
7242
|
const pendingGoal = await consumePendingGoalModeRequest(
|
|
7205
7243
|
this.sessionManager.getCwd(),
|
|
@@ -7212,9 +7250,10 @@ export class AgentSession {
|
|
|
7212
7250
|
}
|
|
7213
7251
|
|
|
7214
7252
|
const previousTools = this.getActiveToolNames();
|
|
7215
|
-
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
7216
7253
|
await this.#goalRuntime.createGoal({ objective: pendingGoal.objective, provenance: pendingGoal.provenance });
|
|
7217
|
-
|
|
7254
|
+
if (!this.#explicitlyDisabledTools) {
|
|
7255
|
+
await this.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
7256
|
+
}
|
|
7218
7257
|
if (this.isStreaming) {
|
|
7219
7258
|
await this.sendGoalModeContext({ deliverAs: "steer" });
|
|
7220
7259
|
}
|
|
@@ -9472,7 +9511,6 @@ export class AgentSession {
|
|
|
9472
9511
|
persistMCPSelection: false,
|
|
9473
9512
|
nextSelectedDiscoveredBuiltinToolNames: [],
|
|
9474
9513
|
});
|
|
9475
|
-
} else {
|
|
9476
9514
|
}
|
|
9477
9515
|
}
|
|
9478
9516
|
this.#todoReminderCount = 0;
|
|
@@ -10066,7 +10104,13 @@ export class AgentSession {
|
|
|
10066
10104
|
});
|
|
10067
10105
|
if (!resolved.model) continue;
|
|
10068
10106
|
|
|
10069
|
-
if (
|
|
10107
|
+
if (
|
|
10108
|
+
roleModels.some(
|
|
10109
|
+
candidate =>
|
|
10110
|
+
modelsAreEqual(candidate.model, resolved.model) && candidate.thinkingLevel === resolved.thinkingLevel,
|
|
10111
|
+
)
|
|
10112
|
+
)
|
|
10113
|
+
continue;
|
|
10070
10114
|
roleModels.push({
|
|
10071
10115
|
role,
|
|
10072
10116
|
model: resolved.model,
|
|
@@ -11626,6 +11670,11 @@ export class AgentSession {
|
|
|
11626
11670
|
this.#resourceSampler = sampler;
|
|
11627
11671
|
}
|
|
11628
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
|
+
|
|
11629
11678
|
setRetainedMemorySampler(sampler: (() => RetainedMemorySample) | undefined): void {
|
|
11630
11679
|
this.#retainedMemorySampler = sampler;
|
|
11631
11680
|
}
|
|
@@ -11659,26 +11708,31 @@ export class AgentSession {
|
|
|
11659
11708
|
materializedResidentBytes: this.#streamingEditFileCache.totalBytes,
|
|
11660
11709
|
tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
|
|
11661
11710
|
tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
|
|
11711
|
+
transcriptFileBytes: this.sessionManager.getTranscriptFileBytes(),
|
|
11662
11712
|
};
|
|
11663
11713
|
}
|
|
11664
11714
|
|
|
11665
11715
|
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
11666
11716
|
const model = this.model;
|
|
11667
11717
|
if (!model) return;
|
|
11668
|
-
const contextWindow = model.contextWindow ?? 0;
|
|
11669
|
-
if (contextWindow <= 0) return;
|
|
11670
11718
|
// F6: non-disableable emergency floor — compact before OOM even when token-based
|
|
11671
11719
|
// compaction is disabled or its threshold is set too high (weak-hardware protection).
|
|
11672
11720
|
const emergencyReason = emergencyCompactionReason(this.#resourceSampler());
|
|
11673
11721
|
if (emergencyReason) {
|
|
11674
11722
|
logger.warn("Emergency compaction triggered (resource floor exceeded)", { reason: emergencyReason });
|
|
11675
|
-
await this.#runAutoCompaction("overflow", false, false, {
|
|
11723
|
+
const status = await this.#runAutoCompaction("overflow", false, false, {
|
|
11676
11724
|
continueAfterMaintenance: false,
|
|
11677
11725
|
deferHandoffMaintenance: false,
|
|
11678
11726
|
force: true,
|
|
11679
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
|
+
}
|
|
11680
11732
|
return;
|
|
11681
11733
|
}
|
|
11734
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
11735
|
+
if (contextWindow <= 0) return;
|
|
11682
11736
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11683
11737
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
11684
11738
|
|
|
@@ -11712,6 +11766,7 @@ export class AgentSession {
|
|
|
11712
11766
|
await this.#runAutoCompaction("threshold", false, false, {
|
|
11713
11767
|
continueAfterMaintenance: false,
|
|
11714
11768
|
deferHandoffMaintenance: false,
|
|
11769
|
+
suppressNextPostTurnMaintenanceAfterHandoff: true,
|
|
11715
11770
|
});
|
|
11716
11771
|
}
|
|
11717
11772
|
}
|
|
@@ -11803,6 +11858,7 @@ export class AgentSession {
|
|
|
11803
11858
|
return;
|
|
11804
11859
|
}
|
|
11805
11860
|
|
|
11861
|
+
if (this.#explicitlyDisabledTools) return;
|
|
11806
11862
|
this.#attachAskTool();
|
|
11807
11863
|
|
|
11808
11864
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
@@ -12702,6 +12758,7 @@ export class AgentSession {
|
|
|
12702
12758
|
force?: boolean;
|
|
12703
12759
|
signal?: AbortSignal;
|
|
12704
12760
|
beforeTerminalOverflowNoop?: () => void;
|
|
12761
|
+
suppressNextPostTurnMaintenanceAfterHandoff?: boolean;
|
|
12705
12762
|
},
|
|
12706
12763
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12707
12764
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12783,6 +12840,9 @@ export class AgentSession {
|
|
|
12783
12840
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12784
12841
|
|
|
12785
12842
|
if (handoffResult) {
|
|
12843
|
+
if (options?.suppressNextPostTurnMaintenanceAfterHandoff) {
|
|
12844
|
+
this.#suppressNextPostTurnMaintenanceAfterAutoHandoff = true;
|
|
12845
|
+
}
|
|
12786
12846
|
await this.#emitSessionEvent({
|
|
12787
12847
|
type: "auto_compaction_end",
|
|
12788
12848
|
action,
|
|
@@ -13502,12 +13562,15 @@ export class AgentSession {
|
|
|
13502
13562
|
if (controller.chain.entries.length > 1) await this.#advanceDefaultFallback(controller, "new_turn", 0);
|
|
13503
13563
|
return;
|
|
13504
13564
|
}
|
|
13565
|
+
const primarySelector = controller.chain.entries[0];
|
|
13505
13566
|
if (
|
|
13506
13567
|
this.settings.get("retry.fallbackRevertPolicy") === "cooldown-expiry" &&
|
|
13507
|
-
controller.activeIndex > 0
|
|
13508
|
-
|
|
13509
|
-
|
|
13568
|
+
controller.activeIndex > 0 &&
|
|
13569
|
+
primarySelector &&
|
|
13570
|
+
this.#fallbackSuppressedSelectors.has(primarySelector) &&
|
|
13571
|
+
!this.#modelRegistry.isSelectorSuppressed(primarySelector)
|
|
13510
13572
|
) {
|
|
13573
|
+
this.#fallbackSuppressedSelectors.delete(primarySelector);
|
|
13511
13574
|
controller.resetForNewTurn();
|
|
13512
13575
|
}
|
|
13513
13576
|
}
|
|
@@ -13560,7 +13623,9 @@ export class AgentSession {
|
|
|
13560
13623
|
const existing = this.#defaultFallbackController;
|
|
13561
13624
|
if (
|
|
13562
13625
|
existing &&
|
|
13563
|
-
(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"))
|
|
13564
13629
|
) {
|
|
13565
13630
|
return existing;
|
|
13566
13631
|
}
|
|
@@ -13846,6 +13911,7 @@ export class AgentSession {
|
|
|
13846
13911
|
|
|
13847
13912
|
if (managedFallback && trigger.class === "rate_limit" && trigger.retryAfterMs !== undefined && failedSelector) {
|
|
13848
13913
|
this.#modelRegistry.suppressSelector(failedSelector, Date.now() + trigger.retryAfterMs);
|
|
13914
|
+
this.#fallbackSuppressedSelectors.add(failedSelector);
|
|
13849
13915
|
}
|
|
13850
13916
|
|
|
13851
13917
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
@@ -15074,14 +15140,22 @@ export class AgentSession {
|
|
|
15074
15140
|
* Switch to a different session file.
|
|
15075
15141
|
* Aborts current operation, loads messages, restores model/thinking.
|
|
15076
15142
|
* Listeners are preserved and will continue receiving events.
|
|
15143
|
+
* `requireIdle` rejects an active turn and fences new admissions for the transition.
|
|
15077
15144
|
* @returns true if switch completed, false if cancelled by hook
|
|
15078
15145
|
*/
|
|
15079
15146
|
async switchSession(
|
|
15080
15147
|
sessionPath: string,
|
|
15081
|
-
options?: { transition?: SessionSwitchEvent["transition"] },
|
|
15148
|
+
options?: { transition?: SessionSwitchEvent["transition"]; requireIdle?: boolean },
|
|
15082
15149
|
): Promise<boolean> {
|
|
15083
15150
|
this.#beginSessionTransition("switch-session");
|
|
15084
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
|
+
}
|
|
15085
15159
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
15086
15160
|
const switchingToDifferentSession = previousSessionFile
|
|
15087
15161
|
? path.resolve(previousSessionFile) !== path.resolve(sessionPath)
|
|
@@ -15307,6 +15381,7 @@ export class AgentSession {
|
|
|
15307
15381
|
throw error;
|
|
15308
15382
|
}
|
|
15309
15383
|
} finally {
|
|
15384
|
+
this.#sessionTurnAdmissionFenced = false;
|
|
15310
15385
|
this.#endSessionTransition();
|
|
15311
15386
|
}
|
|
15312
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();
|
|
@@ -4350,6 +4350,7 @@ export class SessionManager {
|
|
|
4350
4350
|
#sessionName: string | undefined;
|
|
4351
4351
|
#titleSource: "auto" | "user" | undefined;
|
|
4352
4352
|
#sessionFile: string | undefined;
|
|
4353
|
+
#transcriptStatFailureReported = false;
|
|
4353
4354
|
#flushed: boolean = false;
|
|
4354
4355
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
4355
4356
|
#ensuredOnDisk: boolean = false;
|
|
@@ -6573,6 +6574,28 @@ export class SessionManager {
|
|
|
6573
6574
|
getSessionFile(): string | undefined {
|
|
6574
6575
|
return this.#sessionFile;
|
|
6575
6576
|
}
|
|
6577
|
+
/**
|
|
6578
|
+
* Managed on-disk transcript size in bytes. Returns undefined when the
|
|
6579
|
+
* destination is explicit, the file does not exist yet, or its identity
|
|
6580
|
+
* cannot be verified.
|
|
6581
|
+
*/
|
|
6582
|
+
getTranscriptFileBytes(): number | undefined {
|
|
6583
|
+
if (this.destination.kind !== "managed" || !this.#sessionFile) return undefined;
|
|
6584
|
+
try {
|
|
6585
|
+
const size = this.#managedTranscriptStore().sizeSync(path.basename(this.#sessionFile));
|
|
6586
|
+
this.#transcriptStatFailureReported = false;
|
|
6587
|
+
return size ?? undefined;
|
|
6588
|
+
} catch (error) {
|
|
6589
|
+
if (!this.#transcriptStatFailureReported) {
|
|
6590
|
+
this.#transcriptStatFailureReported = true;
|
|
6591
|
+
logger.warn("Managed transcript size sampling failed", {
|
|
6592
|
+
sessionFile: this.#sessionFile,
|
|
6593
|
+
error: toError(error).message,
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6596
|
+
return undefined;
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6576
6599
|
|
|
6577
6600
|
acquireMemoryGuardParticipantIngressLease(): MemoryGuardParticipantIngressLease {
|
|
6578
6601
|
if (this.#memoryGuardParticipantIngressToken)
|
|
@@ -7093,7 +7116,17 @@ export class SessionManager {
|
|
|
7093
7116
|
new Map(),
|
|
7094
7117
|
);
|
|
7095
7118
|
const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
|
|
7096
|
-
|
|
7119
|
+
try {
|
|
7120
|
+
this.#appendManagedRecordSync(persistedEntry);
|
|
7121
|
+
} catch (appendError) {
|
|
7122
|
+
// A managed append-only transcript can hit the per-file limit
|
|
7123
|
+
// even after compaction reduced the live entries. Only the
|
|
7124
|
+
// authority-bound append error may authorize a full rewrite.
|
|
7125
|
+
if (!(appendError instanceof Error) || appendError.message !== "content_too_large") {
|
|
7126
|
+
throw appendError;
|
|
7127
|
+
}
|
|
7128
|
+
this.#rewriteFileSync();
|
|
7129
|
+
}
|
|
7097
7130
|
return;
|
|
7098
7131
|
}
|
|
7099
7132
|
const writer = this.#ensurePersistWriter();
|
|
@@ -7119,16 +7152,29 @@ export class SessionManager {
|
|
|
7119
7152
|
}
|
|
7120
7153
|
|
|
7121
7154
|
#appendEntry(entry: SessionEntry): void {
|
|
7155
|
+
if (this.#persistError) throw this.#persistError;
|
|
7122
7156
|
this.#assertRecoveryHydrationWritable();
|
|
7123
7157
|
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
7124
7158
|
const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
|
|
7159
|
+
const previousLeafId = this.#leafId;
|
|
7125
7160
|
this.#fileEntries.push(residentEntry);
|
|
7126
7161
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
7127
7162
|
this.#leafId = residentEntry.id;
|
|
7128
7163
|
this.#bumpEntryRevision();
|
|
7129
7164
|
this.#leafRevision++;
|
|
7130
7165
|
if (entry.type === "label") this.#labelRevision++;
|
|
7131
|
-
|
|
7166
|
+
try {
|
|
7167
|
+
this._persist(residentEntry);
|
|
7168
|
+
} catch (error) {
|
|
7169
|
+
this.#fileEntries.pop();
|
|
7170
|
+
this.#byId.delete(residentEntry.id);
|
|
7171
|
+
this.#leafId = previousLeafId;
|
|
7172
|
+
this.#entryRevision--;
|
|
7173
|
+
this.#leafRevision--;
|
|
7174
|
+
if (entry.type === "label") this.#labelRevision--;
|
|
7175
|
+
this.#resetMaterializedCaches();
|
|
7176
|
+
throw error;
|
|
7177
|
+
}
|
|
7132
7178
|
// Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
|
|
7133
7179
|
// malformed task-result or assistant usage shape reaching the append path must not
|
|
7134
7180
|
// crash or poison getUsageStatistics() either.
|
|
@@ -9395,7 +9441,14 @@ export class SessionManager {
|
|
|
9395
9441
|
*/
|
|
9396
9442
|
static inventorySessionsStrict(
|
|
9397
9443
|
cwd: string,
|
|
9398
|
-
options?: {
|
|
9444
|
+
options?: {
|
|
9445
|
+
sessionDir?: string;
|
|
9446
|
+
storage?: SessionStorage;
|
|
9447
|
+
destination?: SessionDestination;
|
|
9448
|
+
maxCandidates?: number;
|
|
9449
|
+
maxTotalBytes?: number;
|
|
9450
|
+
filterOtherWorkspaces?: boolean;
|
|
9451
|
+
},
|
|
9399
9452
|
): StrictInventoryResult {
|
|
9400
9453
|
const storage = options?.storage ?? new FileSessionStorage();
|
|
9401
9454
|
const destination = options?.destination;
|
|
@@ -9419,20 +9472,27 @@ export class SessionManager {
|
|
|
9419
9472
|
});
|
|
9420
9473
|
if (resolved.kind === "error")
|
|
9421
9474
|
return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
|
|
9422
|
-
const listing = listManagedCandidates(resolved.scope);
|
|
9475
|
+
const listing = listManagedCandidates(resolved.scope, { maxCandidates: options?.maxCandidates });
|
|
9423
9476
|
if (listing.kind === "error")
|
|
9424
9477
|
return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
|
|
9425
9478
|
const failures: StrictInventoryFailure[] = [];
|
|
9426
9479
|
const candidates: StrictInventoryCandidate[] = [];
|
|
9480
|
+
let remainingBytes = options?.maxTotalBytes;
|
|
9427
9481
|
for (const managedCandidate of listing.owned) {
|
|
9428
9482
|
const candidate = inventoryReadCandidate(
|
|
9429
9483
|
storage,
|
|
9430
9484
|
managedCandidate.path,
|
|
9431
9485
|
cwd,
|
|
9432
9486
|
path.dirname(managedCandidate.path),
|
|
9487
|
+
remainingBytes,
|
|
9488
|
+
options?.filterOtherWorkspaces,
|
|
9433
9489
|
);
|
|
9434
9490
|
if ("failures" in candidate) failures.push(...candidate.failures);
|
|
9435
|
-
else
|
|
9491
|
+
else {
|
|
9492
|
+
if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
|
|
9493
|
+
if ("filtered" in candidate) continue;
|
|
9494
|
+
candidates.push(candidate.candidate);
|
|
9495
|
+
}
|
|
9436
9496
|
}
|
|
9437
9497
|
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
9438
9498
|
store.assertBound();
|
|
@@ -9450,11 +9510,27 @@ export class SessionManager {
|
|
|
9450
9510
|
failures: [{ kind: "scan", message: "Strict scoped session scan is unavailable", path: dir }],
|
|
9451
9511
|
};
|
|
9452
9512
|
}
|
|
9453
|
-
const strictScan = storage.listFilesStrictSync.bind(storage);
|
|
9454
9513
|
|
|
9455
9514
|
let files: string[];
|
|
9456
9515
|
try {
|
|
9457
|
-
|
|
9516
|
+
if (options?.maxCandidates !== undefined) {
|
|
9517
|
+
if (!storage.listFilesStrictBoundedSync) {
|
|
9518
|
+
return {
|
|
9519
|
+
kind: "failure",
|
|
9520
|
+
failures: [{ kind: "scan", message: "Bounded strict session scan is unavailable", path: dir }],
|
|
9521
|
+
};
|
|
9522
|
+
}
|
|
9523
|
+
const bounded = storage.listFilesStrictBoundedSync(dir, "*.jsonl", options.maxCandidates);
|
|
9524
|
+
if (bounded.truncated) {
|
|
9525
|
+
return {
|
|
9526
|
+
kind: "failure",
|
|
9527
|
+
failures: [{ kind: "scan", message: "Strict session candidate limit exceeded", path: dir }],
|
|
9528
|
+
};
|
|
9529
|
+
}
|
|
9530
|
+
files = bounded.files;
|
|
9531
|
+
} else {
|
|
9532
|
+
files = storage.listFilesStrictSync(dir, "*.jsonl");
|
|
9533
|
+
}
|
|
9458
9534
|
} catch (err) {
|
|
9459
9535
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
9460
9536
|
// Only a confirmed ENOENT proves the scoped root is genuinely absent,
|
|
@@ -9487,12 +9563,22 @@ export class SessionManager {
|
|
|
9487
9563
|
|
|
9488
9564
|
const failures: StrictInventoryFailure[] = [];
|
|
9489
9565
|
const candidates: StrictInventoryCandidate[] = [];
|
|
9566
|
+
let remainingBytes = options?.maxTotalBytes;
|
|
9490
9567
|
for (const file of files) {
|
|
9491
|
-
const candidate = inventoryReadCandidate(
|
|
9568
|
+
const candidate = inventoryReadCandidate(
|
|
9569
|
+
storage,
|
|
9570
|
+
file,
|
|
9571
|
+
cwd,
|
|
9572
|
+
dir,
|
|
9573
|
+
remainingBytes,
|
|
9574
|
+
options?.filterOtherWorkspaces,
|
|
9575
|
+
);
|
|
9492
9576
|
if ("failures" in candidate) {
|
|
9493
9577
|
failures.push(...candidate.failures);
|
|
9494
9578
|
continue;
|
|
9495
9579
|
}
|
|
9580
|
+
if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
|
|
9581
|
+
if ("filtered" in candidate) continue;
|
|
9496
9582
|
candidates.push(candidate.candidate);
|
|
9497
9583
|
}
|
|
9498
9584
|
if (failures.length > 0) {
|
|
@@ -9521,7 +9607,12 @@ function inventoryReadCandidate(
|
|
|
9521
9607
|
file: string,
|
|
9522
9608
|
expectedCwd: string,
|
|
9523
9609
|
sessionDir: string,
|
|
9524
|
-
|
|
9610
|
+
maxBytes?: number,
|
|
9611
|
+
filterOtherWorkspaces = false,
|
|
9612
|
+
):
|
|
9613
|
+
| { candidate: StrictInventoryCandidate; bytes: number }
|
|
9614
|
+
| { filtered: true; bytes: number }
|
|
9615
|
+
| { failures: StrictInventoryFailure[] } {
|
|
9525
9616
|
const failures: StrictInventoryFailure[] = [];
|
|
9526
9617
|
const resolvedFile = path.resolve(file);
|
|
9527
9618
|
if (!pathIsWithin(path.resolve(sessionDir), resolvedFile)) {
|
|
@@ -9534,7 +9625,14 @@ function inventoryReadCandidate(
|
|
|
9534
9625
|
}
|
|
9535
9626
|
let snapshot: SessionStorageSnapshot;
|
|
9536
9627
|
try {
|
|
9537
|
-
|
|
9628
|
+
if (maxBytes !== undefined) {
|
|
9629
|
+
if (maxBytes < 0 || !storage.readSnapshotBoundedSync) {
|
|
9630
|
+
return { failures: [{ kind: "read", message: "Bounded exact session read is unavailable", path: file }] };
|
|
9631
|
+
}
|
|
9632
|
+
snapshot = storage.readSnapshotBoundedSync(file, maxBytes);
|
|
9633
|
+
} else {
|
|
9634
|
+
snapshot = storage.readSnapshotSync(file);
|
|
9635
|
+
}
|
|
9538
9636
|
} catch (err) {
|
|
9539
9637
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
9540
9638
|
if (code === "ELOOP" || code === "SYMLINK") {
|
|
@@ -9576,8 +9674,12 @@ function inventoryReadCandidate(
|
|
|
9576
9674
|
}
|
|
9577
9675
|
const canonicalHeaderCwd = resolveEquivalentPath(header.cwd);
|
|
9578
9676
|
if (canonicalHeaderCwd !== resolveEquivalentPath(expectedCwd)) {
|
|
9677
|
+
if (filterOtherWorkspaces) return { filtered: true, bytes: snapshot.bytes.byteLength };
|
|
9579
9678
|
failures.push({ kind: "cwd", message: "Candidate cwd does not match the scoped workspace", path: file });
|
|
9580
9679
|
return { failures };
|
|
9581
9680
|
}
|
|
9582
|
-
return {
|
|
9681
|
+
return {
|
|
9682
|
+
candidate: { path: resolvedFile, id: header.id, cwd: header.cwd, identity: snapshot.stat },
|
|
9683
|
+
bytes: snapshot.bytes.byteLength,
|
|
9684
|
+
};
|
|
9583
9685
|
}
|