@gajae-code/coding-agent 0.4.5 → 0.5.0
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 +43 -0
- package/dist/types/commands/harness.d.ts +3 -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/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -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/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- 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/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/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/cli.ts +8 -4
- package/src/commands/harness.ts +36 -2
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -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-mcp/server.ts +54 -23
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -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/owner.ts +78 -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 +23 -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 +2 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/sdk.ts +29 -2
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +105 -20
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +309 -58
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/task/executor.ts +69 -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/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/resolve.ts +93 -18
- 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 {
|
|
@@ -839,6 +839,8 @@ export type BeforeAgentStartInternalMessage = Pick<
|
|
|
839
839
|
"customType" | "content" | "display" | "details" | "attribution"
|
|
840
840
|
>;
|
|
841
841
|
|
|
842
|
+
type ProviderReplaySourceCacheEntry = { source: string; hash: bigint };
|
|
843
|
+
|
|
842
844
|
/**
|
|
843
845
|
* Internal (first-party, non-user-hook) contributor invoked at the active
|
|
844
846
|
* before-agent-start point alongside the extension runner. Returns an optional
|
|
@@ -863,6 +865,7 @@ export class AgentSession {
|
|
|
863
865
|
|
|
864
866
|
#scopedModels: ScopedModelSelection[];
|
|
865
867
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
868
|
+
#activeModelProfile: string | undefined;
|
|
866
869
|
#promptTemplates: PromptTemplate[];
|
|
867
870
|
#slashCommands: FileSlashCommand[];
|
|
868
871
|
|
|
@@ -1056,6 +1059,7 @@ export class AgentSession {
|
|
|
1056
1059
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1057
1060
|
#obfuscator: SecretObfuscator | undefined;
|
|
1058
1061
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1062
|
+
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
1059
1063
|
#pendingRewindReport: string | undefined = undefined;
|
|
1060
1064
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1061
1065
|
#promptGeneration = 0;
|
|
@@ -1419,7 +1423,7 @@ export class AgentSession {
|
|
|
1419
1423
|
recordSkip("unsupported-role");
|
|
1420
1424
|
return undefined;
|
|
1421
1425
|
}
|
|
1422
|
-
const cloned =
|
|
1426
|
+
const cloned = cloneJsonValueForForkSeed(message) as Message;
|
|
1423
1427
|
if ("providerPayload" in cloned) {
|
|
1424
1428
|
delete (cloned as { providerPayload?: unknown }).providerPayload;
|
|
1425
1429
|
}
|
|
@@ -1466,7 +1470,7 @@ export class AgentSession {
|
|
|
1466
1470
|
}
|
|
1467
1471
|
return {
|
|
1468
1472
|
messages,
|
|
1469
|
-
agentMessages: messages.map(message =>
|
|
1473
|
+
agentMessages: messages.map(message => cloneJsonValueForForkSeed(message) as AgentMessage),
|
|
1470
1474
|
metadata: {
|
|
1471
1475
|
sourceSessionId: this.sessionId,
|
|
1472
1476
|
parentMessageCount: providerMessages.length,
|
|
@@ -4588,7 +4592,7 @@ export class AgentSession {
|
|
|
4588
4592
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
4589
4593
|
await this.refreshGjcSubskillTools();
|
|
4590
4594
|
|
|
4591
|
-
if (eagerTodoPrelude) {
|
|
4595
|
+
if (eagerTodoPrelude?.toolChoice) {
|
|
4592
4596
|
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
4593
4597
|
label: "eager-todo",
|
|
4594
4598
|
});
|
|
@@ -4749,6 +4753,9 @@ export class AgentSession {
|
|
|
4749
4753
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
4750
4754
|
await this.#checkCompaction(lastAssistant, false);
|
|
4751
4755
|
}
|
|
4756
|
+
if (!options?.skipCompactionCheck) {
|
|
4757
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
4758
|
+
}
|
|
4752
4759
|
|
|
4753
4760
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
4754
4761
|
const messages: AgentMessage[] = [];
|
|
@@ -5728,6 +5735,14 @@ export class AgentSession {
|
|
|
5728
5735
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
5729
5736
|
}
|
|
5730
5737
|
|
|
5738
|
+
setActiveModelProfile(name: string | undefined): void {
|
|
5739
|
+
this.#activeModelProfile = name;
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5742
|
+
getActiveModelProfile(): string | undefined {
|
|
5743
|
+
return this.#activeModelProfile;
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5731
5746
|
/**
|
|
5732
5747
|
* Set model temporarily (for this session only).
|
|
5733
5748
|
* Validates API key, saves to session log but NOT to settings.
|
|
@@ -6530,6 +6545,31 @@ export class AgentSession {
|
|
|
6530
6545
|
}
|
|
6531
6546
|
}
|
|
6532
6547
|
}
|
|
6548
|
+
|
|
6549
|
+
async #checkEstimatedContextBeforePrompt(): Promise<void> {
|
|
6550
|
+
const model = this.model;
|
|
6551
|
+
if (!model) return;
|
|
6552
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
6553
|
+
if (contextWindow <= 0) return;
|
|
6554
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
6555
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6556
|
+
|
|
6557
|
+
let contextTokens = this.#estimateContextTokens().tokens;
|
|
6558
|
+
const maxOutputTokens = model.maxTokens ?? 0;
|
|
6559
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
|
|
6560
|
+
|
|
6561
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
6562
|
+
if (pruneResult) {
|
|
6563
|
+
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6564
|
+
}
|
|
6565
|
+
if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
|
|
6566
|
+
await this.#runAutoCompaction("threshold", false, false, {
|
|
6567
|
+
continueAfterMaintenance: false,
|
|
6568
|
+
deferHandoffMaintenance: false,
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
|
|
6533
6573
|
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
6534
6574
|
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
6535
6575
|
if (!toolCallId) return false;
|
|
@@ -6627,7 +6667,7 @@ export class AgentSession {
|
|
|
6627
6667
|
});
|
|
6628
6668
|
}
|
|
6629
6669
|
|
|
6630
|
-
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice
|
|
6670
|
+
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
6631
6671
|
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
6632
6672
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
6633
6673
|
if (!eagerTodosEnabled || !todosEnabled) {
|
|
@@ -6661,13 +6701,15 @@ export class AgentSession {
|
|
|
6661
6701
|
return undefined;
|
|
6662
6702
|
}
|
|
6663
6703
|
|
|
6664
|
-
const
|
|
6665
|
-
|
|
6666
|
-
|
|
6704
|
+
const todoWriteToolChoiceResult = buildNamedToolChoiceResult("todo_write", this.model);
|
|
6705
|
+
const todoWriteToolChoice = todoWriteToolChoiceResult.exactNamed ? todoWriteToolChoiceResult.choice : undefined;
|
|
6706
|
+
if (!todoWriteToolChoiceResult.exactNamed) {
|
|
6707
|
+
logger.debug("Eager todo enforcement degraded; sending reminder without forced tool choice", {
|
|
6667
6708
|
modelApi: this.model?.api,
|
|
6668
6709
|
modelId: this.model?.id,
|
|
6710
|
+
resolvedLevel: todoWriteToolChoiceResult.resolved?.resolvedLevel,
|
|
6711
|
+
reason: todoWriteToolChoiceResult.resolved?.reason,
|
|
6669
6712
|
});
|
|
6670
|
-
return undefined;
|
|
6671
6713
|
}
|
|
6672
6714
|
|
|
6673
6715
|
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
@@ -7049,11 +7091,37 @@ export class AgentSession {
|
|
|
7049
7091
|
}
|
|
7050
7092
|
}
|
|
7051
7093
|
|
|
7094
|
+
#getProviderReplaySource(message: AgentMessage): ProviderReplaySourceCacheEntry {
|
|
7095
|
+
const cached = this.#providerReplaySourceCache.get(message);
|
|
7096
|
+
if (cached) return cached;
|
|
7097
|
+
const source = JSON.stringify(this.#normalizeSessionMessageForProviderReplay(message));
|
|
7098
|
+
const hash = this.#hashProviderReplaySource(source);
|
|
7099
|
+
const entry = { source, hash };
|
|
7100
|
+
this.#providerReplaySourceCache.set(message, entry);
|
|
7101
|
+
return entry;
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
#hashProviderReplaySource(source: string): bigint {
|
|
7105
|
+
return Bun.hash.xxHash64(source);
|
|
7106
|
+
}
|
|
7107
|
+
|
|
7052
7108
|
#didSessionMessagesChange(previousMessages: AgentMessage[], nextMessages: AgentMessage[]): boolean {
|
|
7053
|
-
return
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7109
|
+
if (previousMessages.length !== nextMessages.length) return true;
|
|
7110
|
+
|
|
7111
|
+
const previousSources: ProviderReplaySourceCacheEntry[] = [];
|
|
7112
|
+
const nextSources: ProviderReplaySourceCacheEntry[] = [];
|
|
7113
|
+
for (let i = 0; i < previousMessages.length; i++) {
|
|
7114
|
+
const previous = this.#getProviderReplaySource(previousMessages[i]!);
|
|
7115
|
+
const next = this.#getProviderReplaySource(nextMessages[i]!);
|
|
7116
|
+
if (previous.hash !== next.hash) return true;
|
|
7117
|
+
previousSources.push(previous);
|
|
7118
|
+
nextSources.push(next);
|
|
7119
|
+
}
|
|
7120
|
+
|
|
7121
|
+
for (let i = 0; i < previousSources.length; i++) {
|
|
7122
|
+
if (previousSources[i]!.source !== nextSources[i]!.source) return true;
|
|
7123
|
+
}
|
|
7124
|
+
return false;
|
|
7057
7125
|
}
|
|
7058
7126
|
|
|
7059
7127
|
#getModelKey(model: Model): string {
|
|
@@ -7258,17 +7326,24 @@ export class AgentSession {
|
|
|
7258
7326
|
reason: "overflow" | "threshold" | "idle",
|
|
7259
7327
|
willRetry: boolean,
|
|
7260
7328
|
deferred = false,
|
|
7329
|
+
options?: { continueAfterMaintenance?: boolean; deferHandoffMaintenance?: boolean },
|
|
7261
7330
|
): Promise<void> {
|
|
7262
7331
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7263
7332
|
if (compactionSettings.strategy === "off") return;
|
|
7264
7333
|
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
7265
7334
|
const generation = this.#promptGeneration;
|
|
7266
|
-
if (
|
|
7335
|
+
if (
|
|
7336
|
+
options?.deferHandoffMaintenance !== false &&
|
|
7337
|
+
!deferred &&
|
|
7338
|
+
reason !== "overflow" &&
|
|
7339
|
+
reason !== "idle" &&
|
|
7340
|
+
compactionSettings.strategy === "handoff"
|
|
7341
|
+
) {
|
|
7267
7342
|
this.#schedulePostPromptTask(
|
|
7268
7343
|
async signal => {
|
|
7269
7344
|
await Promise.resolve();
|
|
7270
7345
|
if (signal.aborted) return;
|
|
7271
|
-
await this.#runAutoCompaction(reason, willRetry, true);
|
|
7346
|
+
await this.#runAutoCompaction(reason, willRetry, true, options);
|
|
7272
7347
|
},
|
|
7273
7348
|
{ generation },
|
|
7274
7349
|
);
|
|
@@ -7277,6 +7352,7 @@ export class AgentSession {
|
|
|
7277
7352
|
|
|
7278
7353
|
let action: "context-full" | "handoff" =
|
|
7279
7354
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7355
|
+
const continueAfterMaintenance = options?.continueAfterMaintenance !== false;
|
|
7280
7356
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7281
7357
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7282
7358
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7316,7 +7392,12 @@ export class AgentSession {
|
|
|
7316
7392
|
aborted: false,
|
|
7317
7393
|
willRetry: false,
|
|
7318
7394
|
});
|
|
7319
|
-
if (
|
|
7395
|
+
if (
|
|
7396
|
+
continueAfterMaintenance &&
|
|
7397
|
+
!autoCompactionSignal.aborted &&
|
|
7398
|
+
reason !== "idle" &&
|
|
7399
|
+
compactionSettings.autoContinue !== false
|
|
7400
|
+
) {
|
|
7320
7401
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7321
7402
|
}
|
|
7322
7403
|
return;
|
|
@@ -7378,7 +7459,7 @@ export class AgentSession {
|
|
|
7378
7459
|
stopReason: tail?.stopReason,
|
|
7379
7460
|
});
|
|
7380
7461
|
}
|
|
7381
|
-
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7462
|
+
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7382
7463
|
this.#scheduleAgentContinue({
|
|
7383
7464
|
delayMs: 100,
|
|
7384
7465
|
generation,
|
|
@@ -7386,7 +7467,7 @@ export class AgentSession {
|
|
|
7386
7467
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
7387
7468
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7388
7469
|
});
|
|
7389
|
-
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7470
|
+
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7390
7471
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7391
7472
|
}
|
|
7392
7473
|
return;
|
|
@@ -7607,7 +7688,7 @@ export class AgentSession {
|
|
|
7607
7688
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7608
7689
|
});
|
|
7609
7690
|
}
|
|
7610
|
-
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7691
|
+
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7611
7692
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
7612
7693
|
// Kick the loop so queued messages are actually delivered.
|
|
7613
7694
|
this.#scheduleAgentContinue({
|
|
@@ -7617,7 +7698,7 @@ export class AgentSession {
|
|
|
7617
7698
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
7618
7699
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7619
7700
|
});
|
|
7620
|
-
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7701
|
+
} else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7621
7702
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7622
7703
|
}
|
|
7623
7704
|
} catch (error) {
|
|
@@ -9768,3 +9849,7 @@ export class AgentSession {
|
|
|
9768
9849
|
return this.#extensionRunner;
|
|
9769
9850
|
}
|
|
9770
9851
|
}
|
|
9852
|
+
|
|
9853
|
+
function cloneJsonValueForForkSeed<T>(value: T): T {
|
|
9854
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
9855
|
+
}
|
|
@@ -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);
|
|
@@ -240,13 +323,16 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
|
|
|
240
323
|
}
|
|
241
324
|
|
|
242
325
|
/** Synchronously resolve a blob reference back to utf8 text. */
|
|
243
|
-
export function resolveTextBlobSync(
|
|
326
|
+
export function resolveTextBlobSync(
|
|
327
|
+
blobStore: BlobStore,
|
|
328
|
+
data: string,
|
|
329
|
+
context?: { kind?: "text"; sessionId?: string; sessionFile?: string },
|
|
330
|
+
): string {
|
|
244
331
|
const hash = parseBlobRef(data);
|
|
245
332
|
if (!hash) return data;
|
|
246
333
|
const buffer = blobStore.getSync(hash);
|
|
247
334
|
if (!buffer) {
|
|
248
|
-
|
|
249
|
-
return data;
|
|
335
|
+
throw new ResidentBlobMissingError(hash, context?.kind ?? "text", context?.sessionId, context?.sessionFile);
|
|
250
336
|
}
|
|
251
337
|
return buffer.toString("utf8");
|
|
252
338
|
}
|