@gajae-code/coding-agent 0.4.5 → 0.5.1
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 +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -244,7 +244,7 @@ import { parseCommandArgs } from "../utils/command-args";
|
|
|
244
244
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
245
245
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
246
246
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
247
|
-
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
247
|
+
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
248
248
|
import type { AuthStorage } from "./auth-storage";
|
|
249
249
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
250
250
|
import {
|
|
@@ -322,7 +322,10 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
|
322
322
|
|
|
323
323
|
/** Listener function for agent session events */
|
|
324
324
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
325
|
-
export type AsyncJobSnapshotItem = Pick<
|
|
325
|
+
export type AsyncJobSnapshotItem = Pick<
|
|
326
|
+
AsyncJob,
|
|
327
|
+
"id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
|
|
328
|
+
>;
|
|
326
329
|
|
|
327
330
|
export interface AsyncJobSnapshot {
|
|
328
331
|
running: AsyncJobSnapshotItem[];
|
|
@@ -839,6 +842,8 @@ export type BeforeAgentStartInternalMessage = Pick<
|
|
|
839
842
|
"customType" | "content" | "display" | "details" | "attribution"
|
|
840
843
|
>;
|
|
841
844
|
|
|
845
|
+
type ProviderReplaySourceCacheEntry = { source: string; hash: bigint };
|
|
846
|
+
|
|
842
847
|
/**
|
|
843
848
|
* Internal (first-party, non-user-hook) contributor invoked at the active
|
|
844
849
|
* before-agent-start point alongside the extension runner. Returns an optional
|
|
@@ -863,6 +868,7 @@ export class AgentSession {
|
|
|
863
868
|
|
|
864
869
|
#scopedModels: ScopedModelSelection[];
|
|
865
870
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
871
|
+
#activeModelProfile: string | undefined;
|
|
866
872
|
#promptTemplates: PromptTemplate[];
|
|
867
873
|
#slashCommands: FileSlashCommand[];
|
|
868
874
|
|
|
@@ -900,6 +906,7 @@ export class AgentSession {
|
|
|
900
906
|
// Compaction state
|
|
901
907
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
902
908
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
909
|
+
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
903
910
|
|
|
904
911
|
// Branch summarization state
|
|
905
912
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
@@ -1056,6 +1063,7 @@ export class AgentSession {
|
|
|
1056
1063
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1057
1064
|
#obfuscator: SecretObfuscator | undefined;
|
|
1058
1065
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1066
|
+
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
1059
1067
|
#pendingRewindReport: string | undefined = undefined;
|
|
1060
1068
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1061
1069
|
#promptGeneration = 0;
|
|
@@ -1419,7 +1427,7 @@ export class AgentSession {
|
|
|
1419
1427
|
recordSkip("unsupported-role");
|
|
1420
1428
|
return undefined;
|
|
1421
1429
|
}
|
|
1422
|
-
const cloned =
|
|
1430
|
+
const cloned = cloneJsonValueForForkSeed(message) as Message;
|
|
1423
1431
|
if ("providerPayload" in cloned) {
|
|
1424
1432
|
delete (cloned as { providerPayload?: unknown }).providerPayload;
|
|
1425
1433
|
}
|
|
@@ -1466,7 +1474,7 @@ export class AgentSession {
|
|
|
1466
1474
|
}
|
|
1467
1475
|
return {
|
|
1468
1476
|
messages,
|
|
1469
|
-
agentMessages: messages.map(message =>
|
|
1477
|
+
agentMessages: messages.map(message => cloneJsonValueForForkSeed(message) as AgentMessage),
|
|
1470
1478
|
metadata: {
|
|
1471
1479
|
sourceSessionId: this.sessionId,
|
|
1472
1480
|
parentMessageCount: providerMessages.length,
|
|
@@ -1559,6 +1567,7 @@ export class AgentSession {
|
|
|
1559
1567
|
status: job.status,
|
|
1560
1568
|
label: job.label,
|
|
1561
1569
|
startTime: job.startTime,
|
|
1570
|
+
endTime: job.endTime,
|
|
1562
1571
|
metadata: job.metadata,
|
|
1563
1572
|
}));
|
|
1564
1573
|
const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
|
|
@@ -1567,6 +1576,7 @@ export class AgentSession {
|
|
|
1567
1576
|
status: job.status,
|
|
1568
1577
|
label: job.label,
|
|
1569
1578
|
startTime: job.startTime,
|
|
1579
|
+
endTime: job.endTime,
|
|
1570
1580
|
metadata: job.metadata,
|
|
1571
1581
|
}));
|
|
1572
1582
|
const delivery = manager.getDeliveryState(ownerFilter);
|
|
@@ -4588,7 +4598,7 @@ export class AgentSession {
|
|
|
4588
4598
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
4589
4599
|
await this.refreshGjcSubskillTools();
|
|
4590
4600
|
|
|
4591
|
-
if (eagerTodoPrelude) {
|
|
4601
|
+
if (eagerTodoPrelude?.toolChoice) {
|
|
4592
4602
|
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
4593
4603
|
label: "eager-todo",
|
|
4594
4604
|
});
|
|
@@ -4749,6 +4759,13 @@ export class AgentSession {
|
|
|
4749
4759
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
4750
4760
|
await this.#checkCompaction(lastAssistant, false);
|
|
4751
4761
|
}
|
|
4762
|
+
if (!options?.skipCompactionCheck) {
|
|
4763
|
+
await this.#checkEstimatedContextBeforePrompt([
|
|
4764
|
+
...(options?.prependMessages ?? []),
|
|
4765
|
+
message,
|
|
4766
|
+
...this.#pendingNextTurnMessages,
|
|
4767
|
+
]);
|
|
4768
|
+
}
|
|
4752
4769
|
|
|
4753
4770
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
4754
4771
|
const messages: AgentMessage[] = [];
|
|
@@ -5212,7 +5229,9 @@ export class AgentSession {
|
|
|
5212
5229
|
}
|
|
5213
5230
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5214
5231
|
try {
|
|
5215
|
-
await this
|
|
5232
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5233
|
+
skipPostPromptRecoveryWait: true,
|
|
5234
|
+
});
|
|
5216
5235
|
} finally {
|
|
5217
5236
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5218
5237
|
}
|
|
@@ -5236,7 +5255,9 @@ export class AgentSession {
|
|
|
5236
5255
|
}
|
|
5237
5256
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5238
5257
|
try {
|
|
5239
|
-
await this
|
|
5258
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5259
|
+
skipPostPromptRecoveryWait: true,
|
|
5260
|
+
});
|
|
5240
5261
|
} finally {
|
|
5241
5262
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5242
5263
|
}
|
|
@@ -5728,6 +5749,14 @@ export class AgentSession {
|
|
|
5728
5749
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
5729
5750
|
}
|
|
5730
5751
|
|
|
5752
|
+
setActiveModelProfile(name: string | undefined): void {
|
|
5753
|
+
this.#activeModelProfile = name;
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
getActiveModelProfile(): string | undefined {
|
|
5757
|
+
return this.#activeModelProfile;
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5731
5760
|
/**
|
|
5732
5761
|
* Set model temporarily (for this session only).
|
|
5733
5762
|
* Validates API key, saves to session log but NOT to settings.
|
|
@@ -6530,6 +6559,47 @@ export class AgentSession {
|
|
|
6530
6559
|
}
|
|
6531
6560
|
}
|
|
6532
6561
|
}
|
|
6562
|
+
|
|
6563
|
+
async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
6564
|
+
if (this.#prePromptContextCheckPromise) {
|
|
6565
|
+
await this.#prePromptContextCheckPromise;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
const checkPromise = this.#checkEstimatedContextBeforePromptOnce(pendingMessages);
|
|
6569
|
+
this.#prePromptContextCheckPromise = checkPromise;
|
|
6570
|
+
try {
|
|
6571
|
+
await checkPromise;
|
|
6572
|
+
} finally {
|
|
6573
|
+
if (this.#prePromptContextCheckPromise === checkPromise) {
|
|
6574
|
+
this.#prePromptContextCheckPromise = undefined;
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
6580
|
+
const model = this.model;
|
|
6581
|
+
if (!model) return;
|
|
6582
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
6583
|
+
if (contextWindow <= 0) return;
|
|
6584
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
6585
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6586
|
+
|
|
6587
|
+
let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
|
|
6588
|
+
const maxOutputTokens = model.maxTokens ?? 0;
|
|
6589
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
|
|
6590
|
+
|
|
6591
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
6592
|
+
if (pruneResult) {
|
|
6593
|
+
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6594
|
+
}
|
|
6595
|
+
if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
|
|
6596
|
+
await this.#runAutoCompaction("threshold", false, false, {
|
|
6597
|
+
continueAfterMaintenance: false,
|
|
6598
|
+
deferHandoffMaintenance: false,
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6533
6603
|
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
6534
6604
|
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
6535
6605
|
if (!toolCallId) return false;
|
|
@@ -6627,7 +6697,7 @@ export class AgentSession {
|
|
|
6627
6697
|
});
|
|
6628
6698
|
}
|
|
6629
6699
|
|
|
6630
|
-
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice
|
|
6700
|
+
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
6631
6701
|
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
6632
6702
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
6633
6703
|
if (!eagerTodosEnabled || !todosEnabled) {
|
|
@@ -6661,13 +6731,15 @@ export class AgentSession {
|
|
|
6661
6731
|
return undefined;
|
|
6662
6732
|
}
|
|
6663
6733
|
|
|
6664
|
-
const
|
|
6665
|
-
|
|
6666
|
-
|
|
6734
|
+
const todoWriteToolChoiceResult = buildNamedToolChoiceResult("todo_write", this.model);
|
|
6735
|
+
const todoWriteToolChoice = todoWriteToolChoiceResult.exactNamed ? todoWriteToolChoiceResult.choice : undefined;
|
|
6736
|
+
if (!todoWriteToolChoiceResult.exactNamed) {
|
|
6737
|
+
logger.debug("Eager todo enforcement degraded; sending reminder without forced tool choice", {
|
|
6667
6738
|
modelApi: this.model?.api,
|
|
6668
6739
|
modelId: this.model?.id,
|
|
6740
|
+
resolvedLevel: todoWriteToolChoiceResult.resolved?.resolvedLevel,
|
|
6741
|
+
reason: todoWriteToolChoiceResult.resolved?.reason,
|
|
6669
6742
|
});
|
|
6670
|
-
return undefined;
|
|
6671
6743
|
}
|
|
6672
6744
|
|
|
6673
6745
|
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
@@ -7049,11 +7121,37 @@ export class AgentSession {
|
|
|
7049
7121
|
}
|
|
7050
7122
|
}
|
|
7051
7123
|
|
|
7124
|
+
#getProviderReplaySource(message: AgentMessage): ProviderReplaySourceCacheEntry {
|
|
7125
|
+
const cached = this.#providerReplaySourceCache.get(message);
|
|
7126
|
+
if (cached) return cached;
|
|
7127
|
+
const source = JSON.stringify(this.#normalizeSessionMessageForProviderReplay(message));
|
|
7128
|
+
const hash = this.#hashProviderReplaySource(source);
|
|
7129
|
+
const entry = { source, hash };
|
|
7130
|
+
this.#providerReplaySourceCache.set(message, entry);
|
|
7131
|
+
return entry;
|
|
7132
|
+
}
|
|
7133
|
+
|
|
7134
|
+
#hashProviderReplaySource(source: string): bigint {
|
|
7135
|
+
return Bun.hash.xxHash64(source);
|
|
7136
|
+
}
|
|
7137
|
+
|
|
7052
7138
|
#didSessionMessagesChange(previousMessages: AgentMessage[], nextMessages: AgentMessage[]): boolean {
|
|
7053
|
-
return
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7139
|
+
if (previousMessages.length !== nextMessages.length) return true;
|
|
7140
|
+
|
|
7141
|
+
const previousSources: ProviderReplaySourceCacheEntry[] = [];
|
|
7142
|
+
const nextSources: ProviderReplaySourceCacheEntry[] = [];
|
|
7143
|
+
for (let i = 0; i < previousMessages.length; i++) {
|
|
7144
|
+
const previous = this.#getProviderReplaySource(previousMessages[i]!);
|
|
7145
|
+
const next = this.#getProviderReplaySource(nextMessages[i]!);
|
|
7146
|
+
if (previous.hash !== next.hash) return true;
|
|
7147
|
+
previousSources.push(previous);
|
|
7148
|
+
nextSources.push(next);
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
for (let i = 0; i < previousSources.length; i++) {
|
|
7152
|
+
if (previousSources[i]!.source !== nextSources[i]!.source) return true;
|
|
7153
|
+
}
|
|
7154
|
+
return false;
|
|
7057
7155
|
}
|
|
7058
7156
|
|
|
7059
7157
|
#getModelKey(model: Model): string {
|
|
@@ -7258,17 +7356,24 @@ export class AgentSession {
|
|
|
7258
7356
|
reason: "overflow" | "threshold" | "idle",
|
|
7259
7357
|
willRetry: boolean,
|
|
7260
7358
|
deferred = false,
|
|
7359
|
+
options?: { continueAfterMaintenance?: boolean; deferHandoffMaintenance?: boolean },
|
|
7261
7360
|
): Promise<void> {
|
|
7262
7361
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7263
7362
|
if (compactionSettings.strategy === "off") return;
|
|
7264
7363
|
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
7265
7364
|
const generation = this.#promptGeneration;
|
|
7266
|
-
if (
|
|
7365
|
+
if (
|
|
7366
|
+
options?.deferHandoffMaintenance !== false &&
|
|
7367
|
+
!deferred &&
|
|
7368
|
+
reason !== "overflow" &&
|
|
7369
|
+
reason !== "idle" &&
|
|
7370
|
+
compactionSettings.strategy === "handoff"
|
|
7371
|
+
) {
|
|
7267
7372
|
this.#schedulePostPromptTask(
|
|
7268
7373
|
async signal => {
|
|
7269
7374
|
await Promise.resolve();
|
|
7270
7375
|
if (signal.aborted) return;
|
|
7271
|
-
await this.#runAutoCompaction(reason, willRetry, true);
|
|
7376
|
+
await this.#runAutoCompaction(reason, willRetry, true, options);
|
|
7272
7377
|
},
|
|
7273
7378
|
{ generation },
|
|
7274
7379
|
);
|
|
@@ -7277,6 +7382,7 @@ export class AgentSession {
|
|
|
7277
7382
|
|
|
7278
7383
|
let action: "context-full" | "handoff" =
|
|
7279
7384
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7385
|
+
const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
|
|
7280
7386
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7281
7387
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7282
7388
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7316,7 +7422,12 @@ export class AgentSession {
|
|
|
7316
7422
|
aborted: false,
|
|
7317
7423
|
willRetry: false,
|
|
7318
7424
|
});
|
|
7319
|
-
if (
|
|
7425
|
+
if (
|
|
7426
|
+
continueAfterMaintenance &&
|
|
7427
|
+
!autoCompactionSignal.aborted &&
|
|
7428
|
+
reason !== "idle" &&
|
|
7429
|
+
compactionSettings.autoContinue !== false
|
|
7430
|
+
) {
|
|
7320
7431
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7321
7432
|
}
|
|
7322
7433
|
return;
|
|
@@ -7378,7 +7489,7 @@ export class AgentSession {
|
|
|
7378
7489
|
stopReason: tail?.stopReason,
|
|
7379
7490
|
});
|
|
7380
7491
|
}
|
|
7381
|
-
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7492
|
+
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7382
7493
|
this.#scheduleAgentContinue({
|
|
7383
7494
|
delayMs: 100,
|
|
7384
7495
|
generation,
|
|
@@ -7386,7 +7497,7 @@ export class AgentSession {
|
|
|
7386
7497
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
7387
7498
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7388
7499
|
});
|
|
7389
|
-
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7500
|
+
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7390
7501
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7391
7502
|
}
|
|
7392
7503
|
return;
|
|
@@ -7607,7 +7718,7 @@ export class AgentSession {
|
|
|
7607
7718
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7608
7719
|
});
|
|
7609
7720
|
}
|
|
7610
|
-
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7721
|
+
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7611
7722
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
7612
7723
|
// Kick the loop so queued messages are actually delivered.
|
|
7613
7724
|
this.#scheduleAgentContinue({
|
|
@@ -7617,7 +7728,7 @@ export class AgentSession {
|
|
|
7617
7728
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
7618
7729
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7619
7730
|
});
|
|
7620
|
-
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7731
|
+
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7621
7732
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7622
7733
|
}
|
|
7623
7734
|
} catch (error) {
|
|
@@ -9516,6 +9627,21 @@ export class AgentSession {
|
|
|
9516
9627
|
*/
|
|
9517
9628
|
#estimateContextTokens(): {
|
|
9518
9629
|
tokens: number;
|
|
9630
|
+
} {
|
|
9631
|
+
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
9632
|
+
}
|
|
9633
|
+
|
|
9634
|
+
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
9635
|
+
tokens: number;
|
|
9636
|
+
} {
|
|
9637
|
+
const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageNativeContextTokens(message));
|
|
9638
|
+
return {
|
|
9639
|
+
tokens: estimate.tokens + this.#estimateMessagesNativeContextTokens(pendingMessages),
|
|
9640
|
+
};
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
#estimateContextTokensWith(estimateMessage: (message: AgentMessage) => number): {
|
|
9644
|
+
tokens: number;
|
|
9519
9645
|
} {
|
|
9520
9646
|
const messages = this.messages;
|
|
9521
9647
|
|
|
@@ -9538,7 +9664,7 @@ export class AgentSession {
|
|
|
9538
9664
|
// No usage data - estimate all messages
|
|
9539
9665
|
let estimated = 0;
|
|
9540
9666
|
for (const message of messages) {
|
|
9541
|
-
estimated +=
|
|
9667
|
+
estimated += estimateMessage(message);
|
|
9542
9668
|
}
|
|
9543
9669
|
return {
|
|
9544
9670
|
tokens: estimated,
|
|
@@ -9548,7 +9674,7 @@ export class AgentSession {
|
|
|
9548
9674
|
const usageTokens = calculatePromptTokens(lastUsage);
|
|
9549
9675
|
let trailingTokens = 0;
|
|
9550
9676
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
9551
|
-
trailingTokens +=
|
|
9677
|
+
trailingTokens += estimateMessage(messages[i]);
|
|
9552
9678
|
}
|
|
9553
9679
|
|
|
9554
9680
|
return {
|
|
@@ -9556,6 +9682,30 @@ export class AgentSession {
|
|
|
9556
9682
|
};
|
|
9557
9683
|
}
|
|
9558
9684
|
|
|
9685
|
+
#estimateMessagesNativeContextTokens(messages: readonly AgentMessage[]): number {
|
|
9686
|
+
let tokens = 0;
|
|
9687
|
+
for (const message of messages) {
|
|
9688
|
+
tokens += this.#estimateMessageNativeContextTokens(message);
|
|
9689
|
+
}
|
|
9690
|
+
return tokens;
|
|
9691
|
+
}
|
|
9692
|
+
|
|
9693
|
+
#estimateMessageDisplayTokens(message: AgentMessage): number {
|
|
9694
|
+
let tokens = 0;
|
|
9695
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9696
|
+
tokens += estimateMessageTokensHeuristic(llmMessage);
|
|
9697
|
+
}
|
|
9698
|
+
return tokens;
|
|
9699
|
+
}
|
|
9700
|
+
|
|
9701
|
+
#estimateMessageNativeContextTokens(message: AgentMessage): number {
|
|
9702
|
+
let tokens = 0;
|
|
9703
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9704
|
+
tokens += estimateTokens(llmMessage);
|
|
9705
|
+
}
|
|
9706
|
+
return tokens;
|
|
9707
|
+
}
|
|
9708
|
+
|
|
9559
9709
|
/**
|
|
9560
9710
|
* Export session to HTML.
|
|
9561
9711
|
* @param outputPath Optional output path (defaults to session directory)
|
|
@@ -9768,3 +9918,7 @@ export class AgentSession {
|
|
|
9768
9918
|
return this.#extensionRunner;
|
|
9769
9919
|
}
|
|
9770
9920
|
}
|
|
9921
|
+
|
|
9922
|
+
function cloneJsonValueForForkSeed<T>(value: T): T {
|
|
9923
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
9924
|
+
}
|
|
@@ -95,6 +95,77 @@ export class BlobStore {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export class EphemeralBlobStore extends BlobStore {
|
|
99
|
+
/**
|
|
100
|
+
* Bounded LRU byte budget for the in-memory buffer cache. Keeps recent
|
|
101
|
+
* resident blobs hot for rematerialization after the weak materialized
|
|
102
|
+
* view is collected, without re-pinning the whole session in RAM.
|
|
103
|
+
*/
|
|
104
|
+
static readonly #BUFFER_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
105
|
+
|
|
106
|
+
#bufferCache = new Map<string, Buffer>();
|
|
107
|
+
#bufferCacheBytes = 0;
|
|
108
|
+
|
|
109
|
+
constructor(dir: string) {
|
|
110
|
+
super(dir);
|
|
111
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#cachePut(hash: string, data: Buffer): void {
|
|
116
|
+
const existing = this.#bufferCache.get(hash);
|
|
117
|
+
if (existing) {
|
|
118
|
+
this.#bufferCache.delete(hash);
|
|
119
|
+
this.#bufferCacheBytes -= existing.byteLength;
|
|
120
|
+
}
|
|
121
|
+
if (data.byteLength > EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) return;
|
|
122
|
+
this.#bufferCache.set(hash, data);
|
|
123
|
+
this.#bufferCacheBytes += data.byteLength;
|
|
124
|
+
for (const [oldHash, oldData] of this.#bufferCache) {
|
|
125
|
+
if (this.#bufferCacheBytes <= EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) break;
|
|
126
|
+
this.#bufferCache.delete(oldHash);
|
|
127
|
+
this.#bufferCacheBytes -= oldData.byteLength;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
putSync(data: Buffer): BlobPutResult {
|
|
132
|
+
const result = super.putSync(data);
|
|
133
|
+
this.#cachePut(result.hash, Buffer.from(data));
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getSync(hash: string): Buffer | null {
|
|
138
|
+
const cached = this.#bufferCache.get(hash);
|
|
139
|
+
if (cached) {
|
|
140
|
+
const blobPath = path.join(this.dir, hash);
|
|
141
|
+
if (fs.existsSync(blobPath)) {
|
|
142
|
+
// Refresh LRU recency on hit.
|
|
143
|
+
this.#bufferCache.delete(hash);
|
|
144
|
+
this.#bufferCache.set(hash, cached);
|
|
145
|
+
return Buffer.from(cached);
|
|
146
|
+
}
|
|
147
|
+
this.#bufferCache.delete(hash);
|
|
148
|
+
this.#bufferCacheBytes -= cached.byteLength;
|
|
149
|
+
}
|
|
150
|
+
const data = super.getSync(hash);
|
|
151
|
+
if (data) this.#cachePut(hash, Buffer.from(data));
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clear(): void {
|
|
156
|
+
this.#bufferCache.clear();
|
|
157
|
+
this.#bufferCacheBytes = 0;
|
|
158
|
+
fs.rmSync(this.dir, { recursive: true, force: true });
|
|
159
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
dispose(): void {
|
|
163
|
+
this.#bufferCache.clear();
|
|
164
|
+
this.#bufferCacheBytes = 0;
|
|
165
|
+
fs.rmSync(this.dir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
98
169
|
export class MemoryBlobStore extends BlobStore {
|
|
99
170
|
#blobs = new Map<string, Buffer>();
|
|
100
171
|
|
|
@@ -132,6 +203,18 @@ export class MemoryBlobStore extends BlobStore {
|
|
|
132
203
|
}
|
|
133
204
|
}
|
|
134
205
|
|
|
206
|
+
export class ResidentBlobMissingError extends Error {
|
|
207
|
+
constructor(
|
|
208
|
+
readonly hash: string,
|
|
209
|
+
readonly kind: "text" | "imageUrl" | "imageData",
|
|
210
|
+
readonly sessionId?: string,
|
|
211
|
+
readonly sessionFile?: string,
|
|
212
|
+
) {
|
|
213
|
+
super(`Missing resident ${kind} blob: ${hash}`);
|
|
214
|
+
this.name = "ResidentBlobMissingError";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
135
218
|
/** Check if a data string is a blob reference. */
|
|
136
219
|
export function isBlobRef(data: string): boolean {
|
|
137
220
|
return data.startsWith(BLOB_PREFIX);
|
|
@@ -184,7 +267,13 @@ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: strin
|
|
|
184
267
|
/**
|
|
185
268
|
* Resolve an externalized provider image data URL back to its original string.
|
|
186
269
|
* If the data is not a blob reference, returns it unchanged.
|
|
187
|
-
*
|
|
270
|
+
*
|
|
271
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the persisted blob is missing
|
|
272
|
+
* (e.g. resuming an old session whose image blob was pruned), this warns and returns
|
|
273
|
+
* the reference as-is rather than throwing, so legacy resume degrades gracefully.
|
|
274
|
+
* New resident byte-sensitive TEXT uses the fail-closed path instead
|
|
275
|
+
* (`resolveTextBlobSync` -> `ResidentBlobMissingError`). Do NOT route new byte-sensitive
|
|
276
|
+
* resident data through this warn-and-return path.
|
|
188
277
|
*/
|
|
189
278
|
export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
|
|
190
279
|
const hash = parseBlobRef(data);
|
|
@@ -201,7 +290,11 @@ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): P
|
|
|
201
290
|
/**
|
|
202
291
|
* Resolve a blob reference back to base64 data.
|
|
203
292
|
* If the data is not a blob reference, returns it unchanged.
|
|
204
|
-
*
|
|
293
|
+
*
|
|
294
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the blob is missing this warns
|
|
295
|
+
* and returns the reference as-is (downstream sees an invalid base64 ref but does not
|
|
296
|
+
* crash), preserving legacy-session resume. Byte-sensitive resident TEXT is fail-closed
|
|
297
|
+
* via `resolveTextBlobSync`; do NOT route new byte-sensitive resident data here.
|
|
205
298
|
*/
|
|
206
299
|
export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
|
|
207
300
|
const hash = parseBlobRef(data);
|
|
@@ -239,14 +332,63 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
|
|
|
239
332
|
return buffer.toString("base64");
|
|
240
333
|
}
|
|
241
334
|
|
|
242
|
-
/**
|
|
243
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Synchronously resolve a blob reference back to utf8 text.
|
|
337
|
+
*
|
|
338
|
+
* FAIL-CLOSED byte-sensitive path: a missing resident blob throws
|
|
339
|
+
* `ResidentBlobMissingError` rather than degrading, so a missing resident text blob can
|
|
340
|
+
* never silently leak a `blob:sha256:` ref into provider payloads, UI, or exports.
|
|
341
|
+
* (Contrast the legacy persisted-image warn-and-return resolvers above.)
|
|
342
|
+
*/
|
|
343
|
+
export function resolveTextBlobSync(
|
|
344
|
+
blobStore: BlobStore,
|
|
345
|
+
data: string,
|
|
346
|
+
context?: { kind?: "text"; sessionId?: string; sessionFile?: string },
|
|
347
|
+
): string {
|
|
244
348
|
const hash = parseBlobRef(data);
|
|
245
349
|
if (!hash) return data;
|
|
246
350
|
const buffer = blobStore.getSync(hash);
|
|
247
351
|
if (!buffer) {
|
|
248
|
-
|
|
249
|
-
return data;
|
|
352
|
+
throw new ResidentBlobMissingError(hash, context?.kind ?? "text", context?.sessionId, context?.sessionFile);
|
|
250
353
|
}
|
|
251
354
|
return buffer.toString("utf8");
|
|
252
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataUrlSync}: a missing resident
|
|
359
|
+
* image-data-url blob throws `ResidentBlobMissingError` ("imageUrl") instead of warn-returning,
|
|
360
|
+
* so resident byte-sensitive provider image data can never leak a `blob:sha256:` ref into
|
|
361
|
+
* materialized entries, context, or provider payloads. The warn-and-return `resolveImageDataUrl*`
|
|
362
|
+
* resolvers remain ONLY for legacy persisted-image resume.
|
|
363
|
+
*/
|
|
364
|
+
export function resolveResidentImageDataUrlSync(
|
|
365
|
+
blobStore: BlobStore,
|
|
366
|
+
data: string,
|
|
367
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
368
|
+
): string {
|
|
369
|
+
const hash = parseBlobRef(data);
|
|
370
|
+
if (!hash) return data;
|
|
371
|
+
const buffer = blobStore.getSync(hash);
|
|
372
|
+
if (!buffer) {
|
|
373
|
+
throw new ResidentBlobMissingError(hash, "imageUrl", context?.sessionId, context?.sessionFile);
|
|
374
|
+
}
|
|
375
|
+
return buffer.toString("utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataSync}: a missing resident image blob
|
|
380
|
+
* throws `ResidentBlobMissingError` ("imageData") instead of warn-returning a placeholder.
|
|
381
|
+
*/
|
|
382
|
+
export function resolveResidentImageDataSync(
|
|
383
|
+
blobStore: BlobStore,
|
|
384
|
+
data: string,
|
|
385
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
386
|
+
): string {
|
|
387
|
+
const hash = parseBlobRef(data);
|
|
388
|
+
if (!hash) return data;
|
|
389
|
+
const buffer = blobStore.getSync(hash);
|
|
390
|
+
if (!buffer) {
|
|
391
|
+
throw new ResidentBlobMissingError(hash, "imageData", context?.sessionId, context?.sessionFile);
|
|
392
|
+
}
|
|
393
|
+
return buffer.toString("base64");
|
|
394
|
+
}
|