@tanstack/ai-sandbox 0.3.4 → 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/README.md +26 -0
- package/dist/esm/approvals.js +5 -4
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/bridge-events.js +7 -6
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/checkpoint-store.d.ts +147 -0
- package/dist/esm/checkpoint-store.js +267 -0
- package/dist/esm/checkpoint-store.js.map +1 -0
- package/dist/esm/chunk-identity.d.ts +0 -17
- package/dist/esm/chunk-identity.js +28 -5
- package/dist/esm/chunk-identity.js.map +1 -1
- package/dist/esm/contracts.d.ts +19 -0
- package/dist/esm/index.d.ts +11 -1
- package/dist/esm/index.js +12 -7
- package/dist/esm/memory-snapshot-types.d.ts +129 -0
- package/dist/esm/memory-snapshots.d.ts +6 -0
- package/dist/esm/memory-snapshots.js +490 -0
- package/dist/esm/memory-snapshots.js.map +1 -0
- package/dist/esm/middleware.d.ts +33 -1
- package/dist/esm/middleware.js +339 -94
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.d.ts +1 -1
- package/dist/esm/sandbox.d.ts +16 -0
- package/dist/esm/sandbox.js +62 -9
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/snapshot-operations.d.ts +65 -0
- package/dist/esm/snapshot-operations.js +317 -0
- package/dist/esm/snapshot-operations.js.map +1 -0
- package/dist/esm/snapshot-tools.d.ts +185 -0
- package/dist/esm/snapshot-tools.js +160 -0
- package/dist/esm/snapshot-tools.js.map +1 -0
- package/dist/esm/snapshots.d.ts +51 -0
- package/dist/esm/snapshots.js +350 -0
- package/dist/esm/snapshots.js.map +1 -0
- package/dist/esm/testkit/checkpoint-conformance.d.ts +2 -0
- package/dist/esm/testkit/checkpoint-conformance.js +453 -0
- package/dist/esm/testkit/checkpoint-conformance.js.map +1 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.d.ts +18 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js +191 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js.map +1 -0
- package/dist/esm/testkit/conformance.d.ts +4 -0
- package/dist/esm/testkit/conformance.js +3 -1
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/tool-history.js +4 -5
- package/dist/esm/tool-history.js.map +1 -1
- package/package.json +8 -3
- package/skills/ai-sandbox/SKILL.md +96 -8
- package/src/approvals.ts +13 -12
- package/src/bridge-events.ts +16 -10
- package/src/checkpoint-store.ts +652 -0
- package/src/chunk-identity.ts +36 -5
- package/src/contracts.ts +12 -0
- package/src/index.ts +56 -0
- package/src/memory-snapshot-types.ts +167 -0
- package/src/memory-snapshots.ts +936 -0
- package/src/middleware.ts +610 -160
- package/src/sandbox.ts +107 -6
- package/src/snapshot-operations.ts +540 -0
- package/src/snapshot-tools.ts +208 -0
- package/src/snapshots.ts +711 -0
- package/src/testkit/checkpoint-conformance.ts +472 -0
- package/src/testkit/checkpoint-fork-conformance.ts +299 -0
- package/src/testkit/conformance.ts +7 -0
- package/src/tool-history.ts +9 -13
package/README.md
CHANGED
|
@@ -137,6 +137,30 @@ lifecycle: {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
### Portable snapshots
|
|
141
|
+
|
|
142
|
+
Use portable snapshots when a later run must rebuild completed files after the
|
|
143
|
+
provider sandbox is gone. Create one snapshots object, pass it to
|
|
144
|
+
`withPersistence` and `withSandbox`, and put `withPersistence` first.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { withPersistence } from '@tanstack/ai-persistence'
|
|
148
|
+
import { memorySandboxSnapshots, withSandbox } from '@tanstack/ai-sandbox'
|
|
149
|
+
|
|
150
|
+
const snapshots = await memorySandboxSnapshots({ sandbox, instances })
|
|
151
|
+
|
|
152
|
+
const middleware = [
|
|
153
|
+
withPersistence(snapshots.persistence),
|
|
154
|
+
withSandbox(sandbox, { instances, snapshots }),
|
|
155
|
+
]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A successful terminal run saves regular files, empty directories, saved
|
|
159
|
+
conversation data, and thread artifacts. Restore runs only in a new private
|
|
160
|
+
sandbox. A live resumed sandbox keeps its current files. Read
|
|
161
|
+
[Keep Files After Reload](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots-configure)
|
|
162
|
+
for the full server setup.
|
|
163
|
+
|
|
140
164
|
### Secrets
|
|
141
165
|
|
|
142
166
|
Use `createSecrets()` so values stay behind opaque `SecretRef` tokens. They are never written to snapshots, the sandbox store, or event logs. The sandbox layer resolves them onto the live handle at create, resume, and snapshot restore:
|
|
@@ -178,6 +202,8 @@ Full guides on [tanstack.com/ai](https://tanstack.com/ai/latest/docs/sandbox/ove
|
|
|
178
202
|
- [Policy](https://tanstack.com/ai/latest/docs/sandbox/policy)
|
|
179
203
|
- [Tools](https://tanstack.com/ai/latest/docs/sandbox/tools) (host tool bridge)
|
|
180
204
|
- [Lifecycle & snapshots](https://tanstack.com/ai/latest/docs/sandbox/lifecycle)
|
|
205
|
+
- [Portable sandbox snapshots](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots)
|
|
206
|
+
- [Pick which files to keep](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots-files)
|
|
181
207
|
|
|
182
208
|
## Examples
|
|
183
209
|
|
package/dist/esm/approvals.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { evaluateCommand } from "./policy.js";
|
|
2
|
-
import { EventType } from "@tanstack/ai";
|
|
2
|
+
import { EventType, withTanstackMetadata } from "@tanstack/ai";
|
|
3
3
|
//#region src/approvals.ts
|
|
4
4
|
/**
|
|
5
5
|
* Shared interactive-approval logic for harness adapters.
|
|
@@ -51,7 +51,7 @@ function resolveApproval(input) {
|
|
|
51
51
|
}
|
|
52
52
|
/** Build the AG-UI `approval-requested` CUSTOM event for a harness action. */
|
|
53
53
|
function buildApprovalRequestedEvent(input) {
|
|
54
|
-
return {
|
|
54
|
+
return withTanstackMetadata({
|
|
55
55
|
type: EventType.CUSTOM,
|
|
56
56
|
name: APPROVAL_REQUESTED_EVENT,
|
|
57
57
|
value: {
|
|
@@ -59,10 +59,11 @@ function buildApprovalRequestedEvent(input) {
|
|
|
59
59
|
title: input.title,
|
|
60
60
|
...input.detail ?? {}
|
|
61
61
|
},
|
|
62
|
-
timestamp: Date.now()
|
|
62
|
+
timestamp: Date.now()
|
|
63
|
+
}, {
|
|
63
64
|
threadId: input.threadId,
|
|
64
65
|
runId: input.runId
|
|
65
|
-
};
|
|
66
|
+
});
|
|
66
67
|
}
|
|
67
68
|
//#endregion
|
|
68
69
|
export { APPROVAL_REQUESTED_EVENT, approvalId, buildApprovalRequestedEvent, resolveApproval };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvals.js","names":[],"sources":["../../src/approvals.ts"],"sourcesContent":["/**\n * Shared interactive-approval logic for harness adapters.\n *\n * Flow (rides chat()'s existing resume-based approval mechanism):\n * 1. The agent (inside the sandbox) asks to run a risky action; the harness's\n * host-side permission callback fires.\n * 2. `resolveApproval` evaluates the sandbox policy: `allow`/`deny` are final;\n * `ask` consults the client's approval decisions (threaded via\n * `TextOptions.approvals`, keyed by a stable `approvalId`).\n * 3. On `ask` with no decision yet, the adapter emits an `approval-requested`\n * CUSTOM event (carrying the `approvalId`) and denies the action this turn.\n * The client shows UI, then re-runs chat() with the decision in the message;\n * the engine surfaces it as `approvals`, and the next run allows it.\n *\n * `approvalId` is stable for a given (provider, kind, target) so a client grant\n * matches the same action on the resumed run.\n */\nimport { EventType } from '@tanstack/ai'\nimport { evaluateCommand } from './policy'\nimport type { SandboxPolicy } from './policy'\nimport type { StreamChunk } from '@tanstack/ai'\n\n/** CUSTOM event name emitted when a harness action needs client approval. */\nexport const APPROVAL_REQUESTED_EVENT = 'approval-requested'\n\n/** A stable, opaque approval id for a harness action. */\nexport function approvalId(input: {\n provider: string\n kind: 'command' | 'fileWrite' | 'network' | 'tool'\n target: string\n}): string {\n return `${input.provider}:${input.kind}:${input.target}`\n}\n\nexport interface ResolveApprovalInput {\n policy: SandboxPolicy | undefined\n /** Client approval decisions, keyed by `approvalId`. */\n approvals: ReadonlyMap<string, boolean> | undefined\n /** Precomputed approval id for this action. */\n id: string\n /** A shell command to match against `policy.commands`. */\n command?: string\n /** Named workspace scripts for policy alias resolution. */\n scripts?: Record<string, string>\n /** A coarse capability to match against `policy.capabilities`. */\n capability?: 'fileWrite' | 'network'\n}\n\nexport interface ApprovalOutcome {\n decision: 'allow' | 'deny'\n /** True when policy said `ask` and the client hasn't decided yet. */\n needsApproval: boolean\n}\n\n/** Resolve a harness permission request against policy + client approvals. */\nexport function resolveApproval(input: ResolveApprovalInput): ApprovalOutcome {\n const base =\n input.command !== undefined\n ? evaluateCommand(input.command, input.policy, input.scripts)\n : input.capability !== undefined\n ? (input.policy?.capabilities?.[input.capability] ??\n input.policy?.default ??\n 'ask')\n : (input.policy?.default ?? 'ask')\n\n if (base === 'allow') return { decision: 'allow', needsApproval: false }\n if (base === 'deny') return { decision: 'deny', needsApproval: false }\n\n // base === 'ask' — consult the client's decision.\n const granted = input.approvals?.get(input.id)\n if (granted === true) return { decision: 'allow', needsApproval: false }\n if (granted === false) return { decision: 'deny', needsApproval: false }\n return { decision: 'deny', needsApproval: true }\n}\n\n/** Build the AG-UI `approval-requested` CUSTOM event for a harness action. */\nexport function buildApprovalRequestedEvent(input: {\n approvalId: string\n title: string\n threadId: string\n runId: string\n detail?: Record<string, unknown>\n}): StreamChunk {\n return {\n
|
|
1
|
+
{"version":3,"file":"approvals.js","names":[],"sources":["../../src/approvals.ts"],"sourcesContent":["/**\n * Shared interactive-approval logic for harness adapters.\n *\n * Flow (rides chat()'s existing resume-based approval mechanism):\n * 1. The agent (inside the sandbox) asks to run a risky action; the harness's\n * host-side permission callback fires.\n * 2. `resolveApproval` evaluates the sandbox policy: `allow`/`deny` are final;\n * `ask` consults the client's approval decisions (threaded via\n * `TextOptions.approvals`, keyed by a stable `approvalId`).\n * 3. On `ask` with no decision yet, the adapter emits an `approval-requested`\n * CUSTOM event (carrying the `approvalId`) and denies the action this turn.\n * The client shows UI, then re-runs chat() with the decision in the message;\n * the engine surfaces it as `approvals`, and the next run allows it.\n *\n * `approvalId` is stable for a given (provider, kind, target) so a client grant\n * matches the same action on the resumed run.\n */\nimport { EventType, withTanstackMetadata } from '@tanstack/ai'\nimport { evaluateCommand } from './policy'\nimport type { SandboxPolicy } from './policy'\nimport type { StreamChunk } from '@tanstack/ai'\n\n/** CUSTOM event name emitted when a harness action needs client approval. */\nexport const APPROVAL_REQUESTED_EVENT = 'approval-requested'\n\n/** A stable, opaque approval id for a harness action. */\nexport function approvalId(input: {\n provider: string\n kind: 'command' | 'fileWrite' | 'network' | 'tool'\n target: string\n}): string {\n return `${input.provider}:${input.kind}:${input.target}`\n}\n\nexport interface ResolveApprovalInput {\n policy: SandboxPolicy | undefined\n /** Client approval decisions, keyed by `approvalId`. */\n approvals: ReadonlyMap<string, boolean> | undefined\n /** Precomputed approval id for this action. */\n id: string\n /** A shell command to match against `policy.commands`. */\n command?: string\n /** Named workspace scripts for policy alias resolution. */\n scripts?: Record<string, string>\n /** A coarse capability to match against `policy.capabilities`. */\n capability?: 'fileWrite' | 'network'\n}\n\nexport interface ApprovalOutcome {\n decision: 'allow' | 'deny'\n /** True when policy said `ask` and the client hasn't decided yet. */\n needsApproval: boolean\n}\n\n/** Resolve a harness permission request against policy + client approvals. */\nexport function resolveApproval(input: ResolveApprovalInput): ApprovalOutcome {\n const base =\n input.command !== undefined\n ? evaluateCommand(input.command, input.policy, input.scripts)\n : input.capability !== undefined\n ? (input.policy?.capabilities?.[input.capability] ??\n input.policy?.default ??\n 'ask')\n : (input.policy?.default ?? 'ask')\n\n if (base === 'allow') return { decision: 'allow', needsApproval: false }\n if (base === 'deny') return { decision: 'deny', needsApproval: false }\n\n // base === 'ask' — consult the client's decision.\n const granted = input.approvals?.get(input.id)\n if (granted === true) return { decision: 'allow', needsApproval: false }\n if (granted === false) return { decision: 'deny', needsApproval: false }\n return { decision: 'deny', needsApproval: true }\n}\n\n/** Build the AG-UI `approval-requested` CUSTOM event for a harness action. */\nexport function buildApprovalRequestedEvent(input: {\n approvalId: string\n title: string\n threadId: string\n runId: string\n detail?: Record<string, unknown>\n}): StreamChunk {\n return withTanstackMetadata(\n {\n type: EventType.CUSTOM,\n name: APPROVAL_REQUESTED_EVENT,\n value: {\n approvalId: input.approvalId,\n title: input.title,\n ...(input.detail ?? {}),\n },\n timestamp: Date.now(),\n },\n { threadId: input.threadId, runId: input.runId },\n ) as StreamChunk\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,2BAA2B;;AAGxC,SAAgB,WAAW,OAIhB;CACT,OAAO,GAAG,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG,MAAM;AAClD;;AAuBA,SAAgB,gBAAgB,OAA8C;CAC5E,MAAM,OACJ,MAAM,YAAY,KAAA,IACd,gBAAgB,MAAM,SAAS,MAAM,QAAQ,MAAM,OAAO,IAC1D,MAAM,eAAe,KAAA,IAClB,MAAM,QAAQ,eAAe,MAAM,eACpC,MAAM,QAAQ,WACd,QACC,MAAM,QAAQ,WAAW;CAElC,IAAI,SAAS,SAAS,OAAO;EAAE,UAAU;EAAS,eAAe;CAAM;CACvE,IAAI,SAAS,QAAQ,OAAO;EAAE,UAAU;EAAQ,eAAe;CAAM;CAGrE,MAAM,UAAU,MAAM,WAAW,IAAI,MAAM,EAAE;CAC7C,IAAI,YAAY,MAAM,OAAO;EAAE,UAAU;EAAS,eAAe;CAAM;CACvE,IAAI,YAAY,OAAO,OAAO;EAAE,UAAU;EAAQ,eAAe;CAAM;CACvE,OAAO;EAAE,UAAU;EAAQ,eAAe;CAAK;AACjD;;AAGA,SAAgB,4BAA4B,OAM5B;CACd,OAAO,qBACL;EACE,MAAM,UAAU;EAChB,MAAM;EACN,OAAO;GACL,YAAY,MAAM;GAClB,OAAO,MAAM;GACb,GAAI,MAAM,UAAU,CAAC;EACvB;EACA,WAAW,KAAK,IAAI;CACtB,GACA;EAAE,UAAU,MAAM;EAAU,OAAO,MAAM;CAAM,CACjD;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventType } from "@tanstack/ai";
|
|
1
|
+
import { EventType, withTanstackMetadata } from "@tanstack/ai";
|
|
2
2
|
//#region src/bridge-events.ts
|
|
3
3
|
/**
|
|
4
4
|
* Helpers that let a harness adapter surface custom events emitted by BRIDGED
|
|
@@ -34,15 +34,16 @@ function createBridgeEventChannel(meta) {
|
|
|
34
34
|
return {
|
|
35
35
|
emitCustomEvent(eventName, value) {
|
|
36
36
|
if (closed) return;
|
|
37
|
-
buffer.push({
|
|
37
|
+
buffer.push(withTanstackMetadata({
|
|
38
38
|
type: EventType.CUSTOM,
|
|
39
39
|
name: eventName,
|
|
40
40
|
value,
|
|
41
|
-
timestamp: Date.now()
|
|
41
|
+
timestamp: Date.now()
|
|
42
|
+
}, {
|
|
42
43
|
model: meta.model,
|
|
43
|
-
...meta.threadId !== void 0
|
|
44
|
-
...meta.runId !== void 0
|
|
45
|
-
});
|
|
44
|
+
...meta.threadId !== void 0 ? { threadId: meta.threadId } : {},
|
|
45
|
+
...meta.runId !== void 0 ? { runId: meta.runId } : {}
|
|
46
|
+
}));
|
|
46
47
|
notify?.();
|
|
47
48
|
},
|
|
48
49
|
close() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-events.js","names":[],"sources":["../../src/bridge-events.ts"],"sourcesContent":["/**\n * Helpers that let a harness adapter surface custom events emitted by BRIDGED\n * tools (via {@link ToolBridgeCoreOptions.emitCustomEvent}) on its live output\n * stream.\n *\n * The bridge runs out-of-band from the harness's own event stream, so a bridged\n * tool's progress/console events have no path to the client on their own. An\n * adapter creates a {@link BridgeEventChannel}, hands its `emitCustomEvent` to\n * the bridge provisioner, and {@link mergeChunkStreams | merges} the channel's\n * stream into its translated output — so events interleave live while the agent\n * runs (e.g. code mode's `code_mode:console` logs during a long execution).\n */\nimport { EventType } from '@tanstack/ai'\nimport type { StreamChunk } from '@tanstack/ai'\n\nexport interface BridgeEventChannel {\n /** Pass as the bridge's `emitCustomEvent`; buffers a CUSTOM chunk for the stream. */\n emitCustomEvent: (eventName: string, value: Record<string, unknown>) => void\n /** Live CUSTOM-chunk stream; ends after {@link close} once drained. */\n stream: AsyncIterable<StreamChunk>\n /** Stop the stream (call when the run's main output is done). */\n close: () => void\n}\n\n/** Create a channel whose emitted events become CUSTOM {@link StreamChunk}s. */\nexport function createBridgeEventChannel(meta: {\n model: string\n threadId?: string\n runId?: string\n}): BridgeEventChannel {\n const buffer: Array<StreamChunk> = []\n let notify: (() => void) | null = null\n let closed = false\n\n async function* stream(): AsyncIterable<StreamChunk> {\n for (;;) {\n const next = buffer.shift()\n if (next !== undefined) {\n yield next\n continue\n }\n if (closed) return\n await new Promise<void>((resolve) => {\n notify = resolve\n })\n notify = null\n }\n }\n\n return {\n emitCustomEvent(eventName, value) {\n if (closed) return\n buffer.push({\n
|
|
1
|
+
{"version":3,"file":"bridge-events.js","names":[],"sources":["../../src/bridge-events.ts"],"sourcesContent":["/**\n * Helpers that let a harness adapter surface custom events emitted by BRIDGED\n * tools (via {@link ToolBridgeCoreOptions.emitCustomEvent}) on its live output\n * stream.\n *\n * The bridge runs out-of-band from the harness's own event stream, so a bridged\n * tool's progress/console events have no path to the client on their own. An\n * adapter creates a {@link BridgeEventChannel}, hands its `emitCustomEvent` to\n * the bridge provisioner, and {@link mergeChunkStreams | merges} the channel's\n * stream into its translated output — so events interleave live while the agent\n * runs (e.g. code mode's `code_mode:console` logs during a long execution).\n */\nimport { EventType, withTanstackMetadata } from '@tanstack/ai'\nimport type { StreamChunk } from '@tanstack/ai'\n\nexport interface BridgeEventChannel {\n /** Pass as the bridge's `emitCustomEvent`; buffers a CUSTOM chunk for the stream. */\n emitCustomEvent: (eventName: string, value: Record<string, unknown>) => void\n /** Live CUSTOM-chunk stream; ends after {@link close} once drained. */\n stream: AsyncIterable<StreamChunk>\n /** Stop the stream (call when the run's main output is done). */\n close: () => void\n}\n\n/** Create a channel whose emitted events become CUSTOM {@link StreamChunk}s. */\nexport function createBridgeEventChannel(meta: {\n model: string\n threadId?: string\n runId?: string\n}): BridgeEventChannel {\n const buffer: Array<StreamChunk> = []\n let notify: (() => void) | null = null\n let closed = false\n\n async function* stream(): AsyncIterable<StreamChunk> {\n for (;;) {\n const next = buffer.shift()\n if (next !== undefined) {\n yield next\n continue\n }\n if (closed) return\n await new Promise<void>((resolve) => {\n notify = resolve\n })\n notify = null\n }\n }\n\n return {\n emitCustomEvent(eventName, value) {\n if (closed) return\n buffer.push(\n withTanstackMetadata(\n {\n type: EventType.CUSTOM,\n name: eventName,\n value,\n timestamp: Date.now(),\n },\n {\n model: meta.model,\n ...(meta.threadId !== undefined ? { threadId: meta.threadId } : {}),\n ...(meta.runId !== undefined ? { runId: meta.runId } : {}),\n },\n ) as StreamChunk,\n )\n notify?.()\n },\n close() {\n closed = true\n notify?.()\n },\n stream: stream(),\n }\n}\n\n/**\n * Merge a `side` chunk stream into a `base` chunk stream, yielding from whichever\n * settles first. Terminates when `base` ends (the run is over), then releases the\n * side iterator — so a never-ending channel (until closed) doesn't hang the merge.\n */\nexport async function* mergeChunkStreams(\n base: AsyncIterable<StreamChunk>,\n side: AsyncIterable<StreamChunk>,\n): AsyncIterable<StreamChunk> {\n const baseIt = base[Symbol.asyncIterator]()\n const sideIt = side[Symbol.asyncIterator]()\n let baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n let sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n let sideLive = true\n try {\n for (;;) {\n const winner = await Promise.race(\n sideLive ? [baseNext, sideNext] : [baseNext],\n )\n if (winner.from === 'base') {\n if (winner.r.done) return\n yield winner.r.value\n baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n } else if (winner.r.done) {\n sideLive = false\n } else {\n yield winner.r.value\n sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n }\n }\n } finally {\n // Fire-and-forget: do NOT await the side return. The channel generator is\n // suspended on a promise that only `close()` resolves, and `close()` runs in\n // the adapter's `finally` AFTER this merge completes — awaiting here would\n // deadlock. The adapter's `close()` lets the generator unwind afterwards.\n const baseReturn = baseIt.return?.(undefined)\n if (baseReturn) void baseReturn.catch(() => {})\n const sideReturn = sideIt.return?.(undefined)\n if (sideReturn) void sideReturn.catch(() => {})\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,SAAgB,yBAAyB,MAIlB;CACrB,MAAM,SAA6B,CAAC;CACpC,IAAI,SAA8B;CAClC,IAAI,SAAS;CAEb,gBAAgB,SAAqC;EACnD,SAAS;GACP,MAAM,OAAO,OAAO,MAAM;GAC1B,IAAI,SAAS,KAAA,GAAW;IACtB,MAAM;IACN;GACF;GACA,IAAI,QAAQ;GACZ,MAAM,IAAI,SAAe,YAAY;IACnC,SAAS;GACX,CAAC;GACD,SAAS;EACX;CACF;CAEA,OAAO;EACL,gBAAgB,WAAW,OAAO;GAChC,IAAI,QAAQ;GACZ,OAAO,KACL,qBACE;IACE,MAAM,UAAU;IAChB,MAAM;IACN;IACA,WAAW,KAAK,IAAI;GACtB,GACA;IACE,OAAO,KAAK;IACZ,GAAI,KAAK,aAAa,KAAA,IAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;IACjE,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;GAC1D,CACF,CACF;GACA,SAAS;EACX;EACA,QAAQ;GACN,SAAS;GACT,SAAS;EACX;EACA,QAAQ,OAAO;CACjB;AACF;;;;;;AAOA,gBAAuB,kBACrB,MACA,MAC4B;CAC5B,MAAM,SAAS,KAAK,OAAO,cAAc,CAAC;CAC1C,MAAM,SAAS,KAAK,OAAO,cAAc,CAAC;CAC1C,IAAI,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;EAAE,MAAM;EAAiB;CAAE,EAAE;CACvE,IAAI,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;EAAE,MAAM;EAAiB;CAAE,EAAE;CACvE,IAAI,WAAW;CACf,IAAI;EACF,SAAS;GACP,MAAM,SAAS,MAAM,QAAQ,KAC3B,WAAW,CAAC,UAAU,QAAQ,IAAI,CAAC,QAAQ,CAC7C;GACA,IAAI,OAAO,SAAS,QAAQ;IAC1B,IAAI,OAAO,EAAE,MAAM;IACnB,MAAM,OAAO,EAAE;IACf,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;KAAE,MAAM;KAAiB;IAAE,EAAE;GACrE,OAAO,IAAI,OAAO,EAAE,MAClB,WAAW;QACN;IACL,MAAM,OAAO,EAAE;IACf,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;KAAE,MAAM;KAAiB;IAAE,EAAE;GACrE;EACF;CACF,UAAU;EAKR,MAAM,aAAa,OAAO,SAAS,KAAA,CAAS;EAC5C,IAAI,YAAY,WAAgB,YAAY,CAAC,CAAC;EAC9C,MAAM,aAAa,OAAO,SAAS,KAAA,CAAS;EAC5C,IAAI,YAAY,WAAgB,YAAY,CAAC,CAAC;CAChD;AACF"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ModelMessage } from '@tanstack/ai';
|
|
2
|
+
export interface SandboxSnapshotFileEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
kind: 'file';
|
|
5
|
+
blobKey: string;
|
|
6
|
+
size: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SandboxSnapshotDirectoryEntry {
|
|
9
|
+
path: string;
|
|
10
|
+
kind: 'dir';
|
|
11
|
+
}
|
|
12
|
+
export type SandboxSnapshotEntry = SandboxSnapshotFileEntry | SandboxSnapshotDirectoryEntry;
|
|
13
|
+
export interface SandboxSnapshotArtifact {
|
|
14
|
+
artifactId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
blobKey: string;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SandboxCheckpoint {
|
|
22
|
+
id: string;
|
|
23
|
+
threadId: string;
|
|
24
|
+
parentCheckpointId: string | null;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
reason: 'automatic' | 'named' | 'fork-root';
|
|
27
|
+
label?: string;
|
|
28
|
+
sourceRunId?: string;
|
|
29
|
+
files: ReadonlyArray<SandboxSnapshotEntry>;
|
|
30
|
+
conversation: ReadonlyArray<ModelMessage>;
|
|
31
|
+
artifacts: ReadonlyArray<SandboxSnapshotArtifact>;
|
|
32
|
+
}
|
|
33
|
+
export interface SandboxCheckpointStore {
|
|
34
|
+
get: (id: string) => Promise<SandboxCheckpoint | null>;
|
|
35
|
+
list: (threadId: string) => Promise<Array<SandboxCheckpoint>>;
|
|
36
|
+
getHead: (threadId: string) => Promise<string | null>;
|
|
37
|
+
append: (input: {
|
|
38
|
+
checkpoint: SandboxCheckpoint;
|
|
39
|
+
expectedHeadId: string | null;
|
|
40
|
+
writer: SandboxCheckpointWriter;
|
|
41
|
+
}) => Promise<{
|
|
42
|
+
headId: string;
|
|
43
|
+
}>;
|
|
44
|
+
deleteHead: (input: {
|
|
45
|
+
threadId: string;
|
|
46
|
+
checkpointId: string;
|
|
47
|
+
writer: SandboxCheckpointWriter;
|
|
48
|
+
}) => Promise<void>;
|
|
49
|
+
acquireWriter: (threadId: string) => Promise<SandboxCheckpointWriterLease>;
|
|
50
|
+
listBlobReferences: () => Promise<Array<{
|
|
51
|
+
key: string;
|
|
52
|
+
references: number;
|
|
53
|
+
}>>;
|
|
54
|
+
/** Optional atomic fork capability. Stores without this method cannot fork. */
|
|
55
|
+
forkFromCheckpoint?: SandboxCheckpointForkCapability['forkFromCheckpoint'];
|
|
56
|
+
}
|
|
57
|
+
export interface SandboxCheckpointForkInput {
|
|
58
|
+
sourceThreadId: string;
|
|
59
|
+
sourceCheckpointId: string;
|
|
60
|
+
destinationThreadId: string;
|
|
61
|
+
destinationCheckpointId: string;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
writer: SandboxCheckpointWriter;
|
|
64
|
+
}
|
|
65
|
+
export interface SandboxCheckpointForkCapability {
|
|
66
|
+
forkFromCheckpoint: (input: SandboxCheckpointForkInput) => Promise<{
|
|
67
|
+
checkpoint: SandboxCheckpoint;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
export type ForkCapableSandboxCheckpointStore = SandboxCheckpointStore & SandboxCheckpointForkCapability;
|
|
71
|
+
export declare function isForkCapableSandboxCheckpointStore(store: SandboxCheckpointStore): store is ForkCapableSandboxCheckpointStore;
|
|
72
|
+
export interface SandboxCheckpointWriter {
|
|
73
|
+
threadId: string;
|
|
74
|
+
ownerToken: string;
|
|
75
|
+
fence: number;
|
|
76
|
+
}
|
|
77
|
+
export interface SandboxCheckpointWriterLease extends SandboxCheckpointWriter {
|
|
78
|
+
expiresAt: number;
|
|
79
|
+
renewAfterMs: number;
|
|
80
|
+
renew: () => Promise<{
|
|
81
|
+
expiresAt: number;
|
|
82
|
+
}>;
|
|
83
|
+
release: () => Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export interface SandboxCheckpointStoreOptions {
|
|
86
|
+
now?: () => number;
|
|
87
|
+
leaseDurationMs?: number;
|
|
88
|
+
renewAfterMs?: number;
|
|
89
|
+
}
|
|
90
|
+
export type SandboxCheckpointErrorCode = 'SANDBOX_SNAPSHOT_STALE_HEAD' | 'SANDBOX_SNAPSHOT_PARENT_MISMATCH' | 'SANDBOX_SNAPSHOT_DUPLICATE_ID' | 'SANDBOX_SNAPSHOT_NOT_HEAD' | 'SANDBOX_SNAPSHOT_WRITER_CONFLICT' | 'SANDBOX_SNAPSHOT_WRITER_LOST' | 'SANDBOX_SNAPSHOT_INVALID_ID' | 'SANDBOX_SNAPSHOT_INVALID_ENTRY' | 'SANDBOX_SNAPSHOT_CHECKPOINT_NOT_FOUND' | 'SANDBOX_SNAPSHOT_ATOMIC_FORK_REQUIRED' | 'SANDBOX_SNAPSHOT_FORK_SOURCE_NOT_FOUND' | 'SANDBOX_SNAPSHOT_FORK_SOURCE_THREAD_MISMATCH' | 'SANDBOX_SNAPSHOT_FORK_DESTINATION_NOT_EMPTY';
|
|
91
|
+
export declare class SandboxCheckpointError extends Error {
|
|
92
|
+
readonly code: SandboxCheckpointErrorCode;
|
|
93
|
+
constructor(code: SandboxCheckpointErrorCode, message: string);
|
|
94
|
+
}
|
|
95
|
+
export declare class SandboxCheckpointConflictError extends SandboxCheckpointError {
|
|
96
|
+
constructor(message: string);
|
|
97
|
+
}
|
|
98
|
+
export declare class SandboxCheckpointDuplicateIdError extends SandboxCheckpointError {
|
|
99
|
+
constructor(message: string);
|
|
100
|
+
}
|
|
101
|
+
export declare class SandboxCheckpointInvalidIdError extends SandboxCheckpointError {
|
|
102
|
+
constructor(message: string);
|
|
103
|
+
}
|
|
104
|
+
export declare class SandboxCheckpointInvalidEntryError extends SandboxCheckpointError {
|
|
105
|
+
constructor(message: string);
|
|
106
|
+
}
|
|
107
|
+
export declare class SandboxCheckpointParentMismatchError extends SandboxCheckpointError {
|
|
108
|
+
constructor(message: string);
|
|
109
|
+
}
|
|
110
|
+
export declare class SandboxCheckpointNotHeadError extends SandboxCheckpointError {
|
|
111
|
+
constructor(message: string);
|
|
112
|
+
}
|
|
113
|
+
export declare class SandboxCheckpointWriterConflictError extends SandboxCheckpointError {
|
|
114
|
+
constructor(message: string);
|
|
115
|
+
}
|
|
116
|
+
export declare class SandboxCheckpointWriterLostError extends SandboxCheckpointError {
|
|
117
|
+
constructor(message: string);
|
|
118
|
+
}
|
|
119
|
+
export declare function defineSandboxCheckpointStore(store: SandboxCheckpointStore): SandboxCheckpointStore;
|
|
120
|
+
export declare class InMemorySandboxCheckpointStore implements SandboxCheckpointStore {
|
|
121
|
+
private readonly state;
|
|
122
|
+
private readonly now;
|
|
123
|
+
private readonly leaseDurationMs;
|
|
124
|
+
private readonly renewAfterMs;
|
|
125
|
+
constructor(options?: SandboxCheckpointStoreOptions);
|
|
126
|
+
get(id: string): Promise<SandboxCheckpoint | null>;
|
|
127
|
+
list(threadId: string): Promise<Array<SandboxCheckpoint>>;
|
|
128
|
+
getHead(threadId: string): Promise<string | null>;
|
|
129
|
+
append(input: {
|
|
130
|
+
checkpoint: SandboxCheckpoint;
|
|
131
|
+
expectedHeadId: string | null;
|
|
132
|
+
writer: SandboxCheckpointWriter;
|
|
133
|
+
}): Promise<{
|
|
134
|
+
headId: string;
|
|
135
|
+
}>;
|
|
136
|
+
deleteHead(input: {
|
|
137
|
+
threadId: string;
|
|
138
|
+
checkpointId: string;
|
|
139
|
+
writer: SandboxCheckpointWriter;
|
|
140
|
+
}): Promise<void>;
|
|
141
|
+
acquireWriter(threadId: string): Promise<SandboxCheckpointWriterLease>;
|
|
142
|
+
private assertWriter;
|
|
143
|
+
listBlobReferences(): Promise<Array<{
|
|
144
|
+
key: string;
|
|
145
|
+
references: number;
|
|
146
|
+
}>>;
|
|
147
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
//#region src/checkpoint-store.ts
|
|
2
|
+
var utf8Encoder = new TextEncoder();
|
|
3
|
+
var compareUtf8Bytes = (left, right) => {
|
|
4
|
+
const leftBytes = utf8Encoder.encode(left);
|
|
5
|
+
const rightBytes = utf8Encoder.encode(right);
|
|
6
|
+
const length = Math.min(leftBytes.length, rightBytes.length);
|
|
7
|
+
for (let index = 0; index < length; index++) {
|
|
8
|
+
const leftByte = leftBytes[index];
|
|
9
|
+
const rightByte = rightBytes[index];
|
|
10
|
+
if (leftByte !== rightByte) return (leftByte ?? 0) - (rightByte ?? 0);
|
|
11
|
+
}
|
|
12
|
+
return leftBytes.length - rightBytes.length;
|
|
13
|
+
};
|
|
14
|
+
function isForkCapableSandboxCheckpointStore(store) {
|
|
15
|
+
return typeof store.forkFromCheckpoint === "function";
|
|
16
|
+
}
|
|
17
|
+
var SandboxCheckpointError = class extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SandboxCheckpointError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var SandboxCheckpointConflictError = class extends SandboxCheckpointError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super("SANDBOX_SNAPSHOT_STALE_HEAD", message);
|
|
28
|
+
this.name = "SandboxCheckpointConflictError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var SandboxCheckpointDuplicateIdError = class extends SandboxCheckpointError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super("SANDBOX_SNAPSHOT_DUPLICATE_ID", message);
|
|
34
|
+
this.name = "SandboxCheckpointDuplicateIdError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var SandboxCheckpointInvalidIdError = class extends SandboxCheckpointError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super("SANDBOX_SNAPSHOT_INVALID_ID", message);
|
|
40
|
+
this.name = "SandboxCheckpointInvalidIdError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var SandboxCheckpointInvalidEntryError = class extends SandboxCheckpointError {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super("SANDBOX_SNAPSHOT_INVALID_ENTRY", message);
|
|
46
|
+
this.name = "SandboxCheckpointInvalidEntryError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var SandboxCheckpointParentMismatchError = class extends SandboxCheckpointError {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super("SANDBOX_SNAPSHOT_PARENT_MISMATCH", message);
|
|
52
|
+
this.name = "SandboxCheckpointParentMismatchError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var SandboxCheckpointNotHeadError = class extends SandboxCheckpointError {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super("SANDBOX_SNAPSHOT_NOT_HEAD", message);
|
|
58
|
+
this.name = "SandboxCheckpointNotHeadError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var SandboxCheckpointWriterConflictError = class extends SandboxCheckpointError {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super("SANDBOX_SNAPSHOT_WRITER_CONFLICT", message);
|
|
64
|
+
this.name = "SandboxCheckpointWriterConflictError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var SandboxCheckpointWriterLostError = class extends SandboxCheckpointError {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super("SANDBOX_SNAPSHOT_WRITER_LOST", message);
|
|
70
|
+
this.name = "SandboxCheckpointWriterLostError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function defineSandboxCheckpointStore(store) {
|
|
74
|
+
return store;
|
|
75
|
+
}
|
|
76
|
+
function copy(value) {
|
|
77
|
+
return structuredClone(value);
|
|
78
|
+
}
|
|
79
|
+
function blobKeys(checkpoint) {
|
|
80
|
+
const keys = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const entry of checkpoint.files) if (entry.kind === "file") keys.add(entry.blobKey);
|
|
82
|
+
for (const artifact of checkpoint.artifacts) keys.add(artifact.blobKey);
|
|
83
|
+
return keys;
|
|
84
|
+
}
|
|
85
|
+
function hasUnpairedSurrogate(value) {
|
|
86
|
+
for (let index = 0; index < value.length; index++) {
|
|
87
|
+
const code = value.charCodeAt(index);
|
|
88
|
+
if (code >= 55296 && code <= 56319) {
|
|
89
|
+
const next = value.charCodeAt(index + 1);
|
|
90
|
+
if (Number.isNaN(next) || next < 56320 || next > 57343) return true;
|
|
91
|
+
index++;
|
|
92
|
+
} else if (code >= 56320 && code <= 57343) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function assertValidIdentifier(value, label) {
|
|
97
|
+
if (typeof value !== "string" || value.length === 0 || hasUnpairedSurrogate(value)) throw new SandboxCheckpointInvalidIdError(`${label} must be a non-empty well-formed Unicode string`);
|
|
98
|
+
}
|
|
99
|
+
function hasOwn(value, key) {
|
|
100
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
101
|
+
}
|
|
102
|
+
function validateEntries(checkpoint) {
|
|
103
|
+
if (!Array.isArray(checkpoint.files)) throw new SandboxCheckpointInvalidEntryError("Checkpoint files must be an array");
|
|
104
|
+
const paths = /* @__PURE__ */ new Set();
|
|
105
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const entry of checkpoint.files) {
|
|
107
|
+
if (entry === null || typeof entry !== "object") throw new SandboxCheckpointInvalidEntryError("Checkpoint entry must be an object");
|
|
108
|
+
const candidate = entry;
|
|
109
|
+
if (typeof candidate.path !== "string" || candidate.path.length === 0 || candidate.path.includes("\0") || candidate.path.startsWith("/") || candidate.path.startsWith("\\") || /^[A-Za-z]:([\\/]|$)/.test(candidate.path) || candidate.path.includes("\\") || candidate.path.split("/").some((part) => part.length === 0 || part === "." || part === "..")) throw new SandboxCheckpointInvalidEntryError("Checkpoint entry path must be a normalized workspace-relative path");
|
|
110
|
+
const path = candidate.path;
|
|
111
|
+
if (paths.has(path)) throw new SandboxCheckpointInvalidEntryError(`Checkpoint contains duplicate entry path '${path}'`);
|
|
112
|
+
for (let separator = path.indexOf("/"); separator !== -1; separator = path.indexOf("/", separator + 1)) {
|
|
113
|
+
const ancestor = path.slice(0, separator);
|
|
114
|
+
if (kinds.get(ancestor) === "file") throw new SandboxCheckpointInvalidEntryError(`Checkpoint entry '${path}' is beneath file '${ancestor}'`);
|
|
115
|
+
}
|
|
116
|
+
if (candidate.kind === "file" && Array.from(kinds.keys()).some((other) => other.startsWith(`${path}/`))) throw new SandboxCheckpointInvalidEntryError(`Checkpoint file '${path}' is an ancestor of another entry`);
|
|
117
|
+
paths.add(path);
|
|
118
|
+
if (candidate.kind === "file") {
|
|
119
|
+
if (typeof candidate.blobKey !== "string" || candidate.blobKey.length === 0 || hasUnpairedSurrogate(candidate.blobKey) || !/^sandbox-files\/sha256\/[0-9a-f]{64}$/.test(candidate.blobKey)) throw new SandboxCheckpointInvalidEntryError("File entries require a non-empty blobKey");
|
|
120
|
+
if (!hasOwn(candidate, "size") || typeof candidate.size !== "number" || !Number.isSafeInteger(candidate.size) || candidate.size < 0) throw new SandboxCheckpointInvalidEntryError("File entry size must be a non-negative safe integer");
|
|
121
|
+
} else if (candidate.kind === "dir") {
|
|
122
|
+
if (hasOwn(candidate, "blobKey") || hasOwn(candidate, "size")) throw new SandboxCheckpointInvalidEntryError("Directory entries cannot contain file fields");
|
|
123
|
+
} else throw new SandboxCheckpointInvalidEntryError("Checkpoint entry kind must be file or dir");
|
|
124
|
+
kinds.set(path, candidate.kind);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function validateArtifacts(checkpoint) {
|
|
128
|
+
if (!Array.isArray(checkpoint.artifacts)) throw new SandboxCheckpointInvalidEntryError("Checkpoint artifacts must be an array");
|
|
129
|
+
for (const artifact of checkpoint.artifacts) {
|
|
130
|
+
if (artifact === null || typeof artifact !== "object") throw new SandboxCheckpointInvalidEntryError("Checkpoint artifact must be an object");
|
|
131
|
+
const candidate = artifact;
|
|
132
|
+
if (typeof candidate.artifactId !== "string" || candidate.artifactId.length === 0 || hasUnpairedSurrogate(candidate.artifactId) || typeof candidate.name !== "string" || candidate.name.length === 0 || typeof candidate.mimeType !== "string" || candidate.mimeType.length === 0 || typeof candidate.blobKey !== "string" || candidate.blobKey.length === 0 || hasUnpairedSurrogate(candidate.blobKey) || !/^sandbox-artifacts\/sha256\/[0-9a-f]{64}$/.test(candidate.blobKey) || typeof candidate.size !== "number" || !Number.isSafeInteger(candidate.size) || candidate.size < 0 || typeof candidate.createdAt !== "number" || !Number.isFinite(candidate.createdAt)) throw new SandboxCheckpointInvalidEntryError("Checkpoint artifact has invalid fields");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
var InMemorySandboxCheckpointStore = class {
|
|
136
|
+
state;
|
|
137
|
+
now;
|
|
138
|
+
leaseDurationMs;
|
|
139
|
+
renewAfterMs;
|
|
140
|
+
constructor(options = {}) {
|
|
141
|
+
const state = {
|
|
142
|
+
checkpoints: /* @__PURE__ */ new Map(),
|
|
143
|
+
heads: /* @__PURE__ */ new Map(),
|
|
144
|
+
writers: /* @__PURE__ */ new Map(),
|
|
145
|
+
fences: /* @__PURE__ */ new Map(),
|
|
146
|
+
references: /* @__PURE__ */ new Map()
|
|
147
|
+
};
|
|
148
|
+
this.state = state;
|
|
149
|
+
this.now = options.now ?? (() => Date.now());
|
|
150
|
+
this.leaseDurationMs = options.leaseDurationMs ?? 12e4;
|
|
151
|
+
this.renewAfterMs = options.renewAfterMs ?? 45e3;
|
|
152
|
+
if (!Number.isFinite(this.leaseDurationMs) || this.leaseDurationMs <= 0) throw new Error("leaseDurationMs must be finite and positive");
|
|
153
|
+
if (!Number.isFinite(this.renewAfterMs) || this.renewAfterMs <= 0 || this.renewAfterMs >= this.leaseDurationMs) throw new Error("renewAfterMs must be finite, positive, and less than leaseDurationMs");
|
|
154
|
+
}
|
|
155
|
+
async get(id) {
|
|
156
|
+
assertValidIdentifier(id, "Checkpoint id");
|
|
157
|
+
const checkpoint = this.state.checkpoints.get(id);
|
|
158
|
+
return checkpoint ? copy(checkpoint) : null;
|
|
159
|
+
}
|
|
160
|
+
async list(threadId) {
|
|
161
|
+
assertValidIdentifier(threadId, "Thread id");
|
|
162
|
+
return Array.from(this.state.checkpoints.values()).filter((checkpoint) => checkpoint.threadId === threadId).sort((a, b) => a.createdAt - b.createdAt || compareUtf8Bytes(a.id, b.id)).map(copy);
|
|
163
|
+
}
|
|
164
|
+
async getHead(threadId) {
|
|
165
|
+
assertValidIdentifier(threadId, "Thread id");
|
|
166
|
+
return this.state.heads.get(threadId) ?? null;
|
|
167
|
+
}
|
|
168
|
+
async append(input) {
|
|
169
|
+
const checkpoint = copy(input.checkpoint);
|
|
170
|
+
const { expectedHeadId, writer } = input;
|
|
171
|
+
assertValidIdentifier(checkpoint.threadId, "Checkpoint thread id");
|
|
172
|
+
assertValidIdentifier(writer.threadId, "Writer thread id");
|
|
173
|
+
assertValidIdentifier(checkpoint.id, "Checkpoint id");
|
|
174
|
+
if (expectedHeadId !== null) assertValidIdentifier(expectedHeadId, "Expected head id");
|
|
175
|
+
if (checkpoint.parentCheckpointId != null) assertValidIdentifier(checkpoint.parentCheckpointId, "Parent checkpoint id");
|
|
176
|
+
if (writer.threadId !== checkpoint.threadId) throw new SandboxCheckpointWriterLostError("Checkpoint writer thread does not match checkpoint thread");
|
|
177
|
+
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0) throw new SandboxCheckpointInvalidIdError("Checkpoint id must be non-empty");
|
|
178
|
+
if (hasUnpairedSurrogate(checkpoint.id)) throw new SandboxCheckpointInvalidIdError("Checkpoint id must contain valid Unicode");
|
|
179
|
+
if (hasUnpairedSurrogate(checkpoint.threadId)) throw new SandboxCheckpointInvalidIdError("Checkpoint thread id must contain valid Unicode");
|
|
180
|
+
if (typeof checkpoint.createdAt !== "number" || !Number.isFinite(checkpoint.createdAt)) throw new SandboxCheckpointInvalidEntryError("Checkpoint createdAt must be a finite number");
|
|
181
|
+
if (expectedHeadId === "") throw new SandboxCheckpointInvalidIdError("Expected head id must be null or non-empty");
|
|
182
|
+
const parentCheckpointId = checkpoint.parentCheckpointId ?? null;
|
|
183
|
+
if (parentCheckpointId === "") throw new SandboxCheckpointInvalidIdError("Parent checkpoint id must be null or non-empty");
|
|
184
|
+
if (expectedHeadId !== null && hasUnpairedSurrogate(expectedHeadId)) throw new SandboxCheckpointInvalidIdError("Expected head id must contain valid Unicode");
|
|
185
|
+
if (parentCheckpointId !== null && hasUnpairedSurrogate(parentCheckpointId)) throw new SandboxCheckpointInvalidIdError("Parent checkpoint id must contain valid Unicode");
|
|
186
|
+
validateEntries(checkpoint);
|
|
187
|
+
validateArtifacts(checkpoint);
|
|
188
|
+
this.assertWriter(writer, checkpoint.threadId);
|
|
189
|
+
if (this.state.checkpoints.has(checkpoint.id)) throw new SandboxCheckpointDuplicateIdError(`Checkpoint '${checkpoint.id}' already exists`);
|
|
190
|
+
const actualHeadId = this.state.heads.get(checkpoint.threadId) ?? null;
|
|
191
|
+
if (actualHeadId !== expectedHeadId) throw new SandboxCheckpointConflictError(`Expected head '${expectedHeadId}', but thread '${checkpoint.threadId}' is at '${actualHeadId}'`);
|
|
192
|
+
if (parentCheckpointId !== expectedHeadId) throw new SandboxCheckpointParentMismatchError(`Checkpoint '${checkpoint.id}' parent does not match expected head`);
|
|
193
|
+
const stored = {
|
|
194
|
+
...checkpoint,
|
|
195
|
+
parentCheckpointId
|
|
196
|
+
};
|
|
197
|
+
const keys = blobKeys(stored);
|
|
198
|
+
this.state.checkpoints.set(stored.id, stored);
|
|
199
|
+
this.state.heads.set(stored.threadId, stored.id);
|
|
200
|
+
for (const key of keys) this.state.references.set(key, (this.state.references.get(key) ?? 0) + 1);
|
|
201
|
+
return { headId: stored.id };
|
|
202
|
+
}
|
|
203
|
+
async deleteHead(input) {
|
|
204
|
+
const { threadId, checkpointId, writer } = input;
|
|
205
|
+
assertValidIdentifier(threadId, "Thread id");
|
|
206
|
+
assertValidIdentifier(checkpointId, "Checkpoint id");
|
|
207
|
+
assertValidIdentifier(writer.threadId, "Writer thread id");
|
|
208
|
+
if (writer.threadId !== threadId) throw new SandboxCheckpointWriterLostError("Checkpoint writer thread does not match operation thread");
|
|
209
|
+
this.assertWriter(writer, threadId);
|
|
210
|
+
if ((this.state.heads.get(threadId) ?? null) !== checkpointId) throw new SandboxCheckpointNotHeadError(`Checkpoint '${checkpointId}' is not the current head of thread '${threadId}'`);
|
|
211
|
+
const checkpoint = this.state.checkpoints.get(checkpointId);
|
|
212
|
+
if (!checkpoint) throw new SandboxCheckpointNotHeadError(`Checkpoint '${checkpointId}' does not exist`);
|
|
213
|
+
this.state.checkpoints.delete(checkpointId);
|
|
214
|
+
if (checkpoint.parentCheckpointId) this.state.heads.set(threadId, checkpoint.parentCheckpointId);
|
|
215
|
+
else this.state.heads.delete(threadId);
|
|
216
|
+
for (const key of blobKeys(checkpoint)) {
|
|
217
|
+
const references = (this.state.references.get(key) ?? 0) - 1;
|
|
218
|
+
if (references > 0) this.state.references.set(key, references);
|
|
219
|
+
else this.state.references.delete(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async acquireWriter(threadId) {
|
|
223
|
+
assertValidIdentifier(threadId, "Thread id");
|
|
224
|
+
const current = this.state.writers.get(threadId);
|
|
225
|
+
if (current && current.expiresAt > this.now()) throw new SandboxCheckpointWriterConflictError(`Thread '${threadId}' already has an active checkpoint writer`);
|
|
226
|
+
const fence = (this.state.fences.get(threadId) ?? 0) + 1;
|
|
227
|
+
this.state.fences.set(threadId, fence);
|
|
228
|
+
const ownerToken = globalThis.crypto.randomUUID();
|
|
229
|
+
const lease = {
|
|
230
|
+
threadId,
|
|
231
|
+
ownerToken,
|
|
232
|
+
fence,
|
|
233
|
+
expiresAt: this.now() + this.leaseDurationMs
|
|
234
|
+
};
|
|
235
|
+
this.state.writers.set(threadId, lease);
|
|
236
|
+
return {
|
|
237
|
+
...lease,
|
|
238
|
+
get expiresAt() {
|
|
239
|
+
return lease.expiresAt;
|
|
240
|
+
},
|
|
241
|
+
renewAfterMs: this.renewAfterMs,
|
|
242
|
+
renew: async () => {
|
|
243
|
+
this.assertWriter(lease, threadId);
|
|
244
|
+
lease.expiresAt = this.now() + this.leaseDurationMs;
|
|
245
|
+
return { expiresAt: lease.expiresAt };
|
|
246
|
+
},
|
|
247
|
+
release: async () => {
|
|
248
|
+
const currentLease = this.state.writers.get(threadId);
|
|
249
|
+
if (currentLease?.ownerToken === ownerToken && currentLease.fence === fence) this.state.writers.delete(threadId);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
assertWriter(writer, threadId) {
|
|
254
|
+
const current = this.state.writers.get(threadId);
|
|
255
|
+
if (!current || current.ownerToken !== writer.ownerToken || current.fence !== writer.fence || current.expiresAt <= this.now()) throw new SandboxCheckpointWriterLostError(`Checkpoint writer lease for thread '${threadId}' is no longer current`);
|
|
256
|
+
}
|
|
257
|
+
async listBlobReferences() {
|
|
258
|
+
return Array.from(this.state.references.entries()).sort(([a], [b]) => compareUtf8Bytes(a, b)).map(([key, references]) => ({
|
|
259
|
+
key,
|
|
260
|
+
references
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
265
|
+
export { InMemorySandboxCheckpointStore, SandboxCheckpointConflictError, SandboxCheckpointDuplicateIdError, SandboxCheckpointError, SandboxCheckpointInvalidEntryError, SandboxCheckpointInvalidIdError, SandboxCheckpointNotHeadError, SandboxCheckpointParentMismatchError, SandboxCheckpointWriterConflictError, SandboxCheckpointWriterLostError, defineSandboxCheckpointStore, isForkCapableSandboxCheckpointStore };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=checkpoint-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-store.js","names":[],"sources":["../../src/checkpoint-store.ts"],"sourcesContent":["import type { ModelMessage } from '@tanstack/ai'\n\n// Compare strings by their UTF-8 bytes so ordering does not depend on locale.\nconst utf8Encoder = new TextEncoder()\n\nconst compareUtf8Bytes = (left: string, right: string): number => {\n const leftBytes = utf8Encoder.encode(left)\n const rightBytes = utf8Encoder.encode(right)\n const length = Math.min(leftBytes.length, rightBytes.length)\n for (let index = 0; index < length; index++) {\n const leftByte = leftBytes[index]\n const rightByte = rightBytes[index]\n if (leftByte !== rightByte) {\n return (leftByte ?? 0) - (rightByte ?? 0)\n }\n }\n return leftBytes.length - rightBytes.length\n}\n\nexport interface SandboxSnapshotFileEntry {\n path: string\n kind: 'file'\n blobKey: string\n size: number\n}\n\nexport interface SandboxSnapshotDirectoryEntry {\n path: string\n kind: 'dir'\n}\n\nexport type SandboxSnapshotEntry =\n | SandboxSnapshotFileEntry\n | SandboxSnapshotDirectoryEntry\n\nexport interface SandboxSnapshotArtifact {\n artifactId: string\n name: string\n mimeType: string\n size: number\n blobKey: string\n createdAt: number\n}\n\nexport interface SandboxCheckpoint {\n id: string\n threadId: string\n parentCheckpointId: string | null\n createdAt: number\n reason: 'automatic' | 'named' | 'fork-root'\n label?: string\n sourceRunId?: string\n files: ReadonlyArray<SandboxSnapshotEntry>\n conversation: ReadonlyArray<ModelMessage>\n artifacts: ReadonlyArray<SandboxSnapshotArtifact>\n}\n\nexport interface SandboxCheckpointStore {\n get: (id: string) => Promise<SandboxCheckpoint | null>\n list: (threadId: string) => Promise<Array<SandboxCheckpoint>>\n getHead: (threadId: string) => Promise<string | null>\n append: (input: {\n checkpoint: SandboxCheckpoint\n expectedHeadId: string | null\n writer: SandboxCheckpointWriter\n }) => Promise<{ headId: string }>\n deleteHead: (input: {\n threadId: string\n checkpointId: string\n writer: SandboxCheckpointWriter\n }) => Promise<void>\n acquireWriter: (threadId: string) => Promise<SandboxCheckpointWriterLease>\n listBlobReferences: () => Promise<Array<{ key: string; references: number }>>\n /** Optional atomic fork capability. Stores without this method cannot fork. */\n forkFromCheckpoint?: SandboxCheckpointForkCapability['forkFromCheckpoint']\n}\n\nexport interface SandboxCheckpointForkInput {\n sourceThreadId: string\n sourceCheckpointId: string\n destinationThreadId: string\n destinationCheckpointId: string\n createdAt: number\n writer: SandboxCheckpointWriter\n}\n\nexport interface SandboxCheckpointForkCapability {\n forkFromCheckpoint: (input: SandboxCheckpointForkInput) => Promise<{\n checkpoint: SandboxCheckpoint\n }>\n}\n\nexport type ForkCapableSandboxCheckpointStore = SandboxCheckpointStore &\n SandboxCheckpointForkCapability\n\nexport function isForkCapableSandboxCheckpointStore(\n store: SandboxCheckpointStore,\n): store is ForkCapableSandboxCheckpointStore {\n return typeof store.forkFromCheckpoint === 'function'\n}\n\nexport interface SandboxCheckpointWriter {\n threadId: string\n ownerToken: string\n fence: number\n}\n\nexport interface SandboxCheckpointWriterLease extends SandboxCheckpointWriter {\n expiresAt: number\n renewAfterMs: number\n renew: () => Promise<{ expiresAt: number }>\n release: () => Promise<void>\n}\n\nexport interface SandboxCheckpointStoreOptions {\n now?: () => number\n leaseDurationMs?: number\n renewAfterMs?: number\n}\n\ninterface InMemoryCheckpointState {\n checkpoints: Map<string, SandboxCheckpoint>\n heads: Map<string, string>\n writers: Map<string, { ownerToken: string; fence: number; expiresAt: number }>\n fences: Map<string, number>\n references: Map<string, number>\n}\n\nexport type SandboxCheckpointErrorCode =\n | 'SANDBOX_SNAPSHOT_STALE_HEAD'\n | 'SANDBOX_SNAPSHOT_PARENT_MISMATCH'\n | 'SANDBOX_SNAPSHOT_DUPLICATE_ID'\n | 'SANDBOX_SNAPSHOT_NOT_HEAD'\n | 'SANDBOX_SNAPSHOT_WRITER_CONFLICT'\n | 'SANDBOX_SNAPSHOT_WRITER_LOST'\n | 'SANDBOX_SNAPSHOT_INVALID_ID'\n | 'SANDBOX_SNAPSHOT_INVALID_ENTRY'\n | 'SANDBOX_SNAPSHOT_CHECKPOINT_NOT_FOUND'\n | 'SANDBOX_SNAPSHOT_ATOMIC_FORK_REQUIRED'\n | 'SANDBOX_SNAPSHOT_FORK_SOURCE_NOT_FOUND'\n | 'SANDBOX_SNAPSHOT_FORK_SOURCE_THREAD_MISMATCH'\n | 'SANDBOX_SNAPSHOT_FORK_DESTINATION_NOT_EMPTY'\n\nexport class SandboxCheckpointError extends Error {\n readonly code: SandboxCheckpointErrorCode\n\n constructor(code: SandboxCheckpointErrorCode, message: string) {\n super(message)\n this.name = 'SandboxCheckpointError'\n this.code = code\n }\n}\n\nexport class SandboxCheckpointConflictError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_STALE_HEAD', message)\n this.name = 'SandboxCheckpointConflictError'\n }\n}\n\nexport class SandboxCheckpointDuplicateIdError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_DUPLICATE_ID', message)\n this.name = 'SandboxCheckpointDuplicateIdError'\n }\n}\n\nexport class SandboxCheckpointInvalidIdError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_INVALID_ID', message)\n this.name = 'SandboxCheckpointInvalidIdError'\n }\n}\n\nexport class SandboxCheckpointInvalidEntryError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_INVALID_ENTRY', message)\n this.name = 'SandboxCheckpointInvalidEntryError'\n }\n}\n\nexport class SandboxCheckpointParentMismatchError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_PARENT_MISMATCH', message)\n this.name = 'SandboxCheckpointParentMismatchError'\n }\n}\n\nexport class SandboxCheckpointNotHeadError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_NOT_HEAD', message)\n this.name = 'SandboxCheckpointNotHeadError'\n }\n}\n\nexport class SandboxCheckpointWriterConflictError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_WRITER_CONFLICT', message)\n this.name = 'SandboxCheckpointWriterConflictError'\n }\n}\n\nexport class SandboxCheckpointWriterLostError extends SandboxCheckpointError {\n constructor(message: string) {\n super('SANDBOX_SNAPSHOT_WRITER_LOST', message)\n this.name = 'SandboxCheckpointWriterLostError'\n }\n}\n\nexport function defineSandboxCheckpointStore(\n store: SandboxCheckpointStore,\n): SandboxCheckpointStore {\n return store\n}\n\nfunction copy<T>(value: T): T {\n return structuredClone(value)\n}\n\nfunction blobKeys(checkpoint: SandboxCheckpoint): Set<string> {\n const keys = new Set<string>()\n for (const entry of checkpoint.files) {\n if (entry.kind === 'file') keys.add(entry.blobKey)\n }\n for (const artifact of checkpoint.artifacts) keys.add(artifact.blobKey)\n return keys\n}\n\nfunction hasUnpairedSurrogate(value: string): boolean {\n for (let index = 0; index < value.length; index++) {\n const code = value.charCodeAt(index)\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = value.charCodeAt(index + 1)\n if (Number.isNaN(next) || next < 0xdc00 || next > 0xdfff) return true\n index++\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n return true\n }\n }\n return false\n}\n\nfunction assertValidIdentifier(\n value: unknown,\n label: string,\n): asserts value is string {\n if (\n typeof value !== 'string' ||\n value.length === 0 ||\n hasUnpairedSurrogate(value)\n ) {\n throw new SandboxCheckpointInvalidIdError(\n `${label} must be a non-empty well-formed Unicode string`,\n )\n }\n}\n\nfunction hasOwn(value: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(value, key)\n}\n\nfunction validateEntries(checkpoint: SandboxCheckpoint): void {\n if (!Array.isArray(checkpoint.files)) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint files must be an array',\n )\n }\n const paths = new Set<string>()\n const kinds = new Map<string, 'file' | 'dir'>()\n for (const entry of checkpoint.files as ReadonlyArray<unknown>) {\n if (entry === null || typeof entry !== 'object') {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint entry must be an object',\n )\n }\n const candidate = entry as Record<string, unknown>\n if (\n typeof candidate.path !== 'string' ||\n candidate.path.length === 0 ||\n candidate.path.includes('\\0') ||\n candidate.path.startsWith('/') ||\n candidate.path.startsWith('\\\\') ||\n /^[A-Za-z]:([\\\\/]|$)/.test(candidate.path) ||\n candidate.path.includes('\\\\') ||\n candidate.path\n .split('/')\n .some((part) => part.length === 0 || part === '.' || part === '..')\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint entry path must be a normalized workspace-relative path',\n )\n }\n const path = candidate.path\n if (paths.has(path)) {\n throw new SandboxCheckpointInvalidEntryError(\n `Checkpoint contains duplicate entry path '${path}'`,\n )\n }\n for (\n let separator = path.indexOf('/');\n separator !== -1;\n separator = path.indexOf('/', separator + 1)\n ) {\n const ancestor = path.slice(0, separator)\n if (kinds.get(ancestor) === 'file') {\n throw new SandboxCheckpointInvalidEntryError(\n `Checkpoint entry '${path}' is beneath file '${ancestor}'`,\n )\n }\n }\n if (\n candidate.kind === 'file' &&\n Array.from(kinds.keys()).some((other) => other.startsWith(`${path}/`))\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n `Checkpoint file '${path}' is an ancestor of another entry`,\n )\n }\n paths.add(path)\n if (candidate.kind === 'file') {\n if (\n typeof candidate.blobKey !== 'string' ||\n candidate.blobKey.length === 0 ||\n hasUnpairedSurrogate(candidate.blobKey) ||\n !/^sandbox-files\\/sha256\\/[0-9a-f]{64}$/.test(candidate.blobKey)\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n 'File entries require a non-empty blobKey',\n )\n }\n if (\n !hasOwn(candidate, 'size') ||\n typeof candidate.size !== 'number' ||\n !Number.isSafeInteger(candidate.size) ||\n candidate.size < 0\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n 'File entry size must be a non-negative safe integer',\n )\n }\n } else if (candidate.kind === 'dir') {\n if (hasOwn(candidate, 'blobKey') || hasOwn(candidate, 'size')) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Directory entries cannot contain file fields',\n )\n }\n } else {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint entry kind must be file or dir',\n )\n }\n kinds.set(path, candidate.kind)\n }\n}\n\nfunction validateArtifacts(checkpoint: SandboxCheckpoint): void {\n if (!Array.isArray(checkpoint.artifacts)) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint artifacts must be an array',\n )\n }\n for (const artifact of checkpoint.artifacts as ReadonlyArray<unknown>) {\n if (artifact === null || typeof artifact !== 'object') {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint artifact must be an object',\n )\n }\n const candidate = artifact as Record<string, unknown>\n if (\n typeof candidate.artifactId !== 'string' ||\n candidate.artifactId.length === 0 ||\n hasUnpairedSurrogate(candidate.artifactId) ||\n typeof candidate.name !== 'string' ||\n candidate.name.length === 0 ||\n typeof candidate.mimeType !== 'string' ||\n candidate.mimeType.length === 0 ||\n typeof candidate.blobKey !== 'string' ||\n candidate.blobKey.length === 0 ||\n hasUnpairedSurrogate(candidate.blobKey) ||\n !/^sandbox-artifacts\\/sha256\\/[0-9a-f]{64}$/.test(candidate.blobKey) ||\n typeof candidate.size !== 'number' ||\n !Number.isSafeInteger(candidate.size) ||\n candidate.size < 0 ||\n typeof candidate.createdAt !== 'number' ||\n !Number.isFinite(candidate.createdAt)\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint artifact has invalid fields',\n )\n }\n }\n}\n\nexport class InMemorySandboxCheckpointStore implements SandboxCheckpointStore {\n private readonly state: InMemoryCheckpointState\n private readonly now: () => number\n private readonly leaseDurationMs: number\n private readonly renewAfterMs: number\n\n constructor(options: SandboxCheckpointStoreOptions = {}) {\n const state: InMemoryCheckpointState = {\n checkpoints: new Map(),\n heads: new Map(),\n writers: new Map(),\n fences: new Map(),\n references: new Map(),\n }\n this.state = state\n this.now = options.now ?? (() => Date.now())\n this.leaseDurationMs = options.leaseDurationMs ?? 120_000\n this.renewAfterMs = options.renewAfterMs ?? 45_000\n if (!Number.isFinite(this.leaseDurationMs) || this.leaseDurationMs <= 0) {\n throw new Error('leaseDurationMs must be finite and positive')\n }\n if (\n !Number.isFinite(this.renewAfterMs) ||\n this.renewAfterMs <= 0 ||\n this.renewAfterMs >= this.leaseDurationMs\n ) {\n throw new Error(\n 'renewAfterMs must be finite, positive, and less than leaseDurationMs',\n )\n }\n }\n\n async get(id: string): Promise<SandboxCheckpoint | null> {\n assertValidIdentifier(id, 'Checkpoint id')\n const checkpoint = this.state.checkpoints.get(id)\n return checkpoint ? copy(checkpoint) : null\n }\n\n async list(threadId: string): Promise<Array<SandboxCheckpoint>> {\n assertValidIdentifier(threadId, 'Thread id')\n return Array.from(this.state.checkpoints.values())\n .filter((checkpoint) => checkpoint.threadId === threadId)\n .sort((a, b) => a.createdAt - b.createdAt || compareUtf8Bytes(a.id, b.id))\n .map(copy)\n }\n\n async getHead(threadId: string): Promise<string | null> {\n assertValidIdentifier(threadId, 'Thread id')\n return this.state.heads.get(threadId) ?? null\n }\n\n async append(input: {\n checkpoint: SandboxCheckpoint\n expectedHeadId: string | null\n writer: SandboxCheckpointWriter\n }): Promise<{ headId: string }> {\n const checkpoint = copy(input.checkpoint)\n const { expectedHeadId, writer } = input\n assertValidIdentifier(checkpoint.threadId, 'Checkpoint thread id')\n assertValidIdentifier(writer.threadId, 'Writer thread id')\n assertValidIdentifier(checkpoint.id, 'Checkpoint id')\n if (expectedHeadId !== null) {\n assertValidIdentifier(expectedHeadId, 'Expected head id')\n }\n if (checkpoint.parentCheckpointId != null) {\n assertValidIdentifier(\n checkpoint.parentCheckpointId,\n 'Parent checkpoint id',\n )\n }\n if (writer.threadId !== checkpoint.threadId) {\n throw new SandboxCheckpointWriterLostError(\n 'Checkpoint writer thread does not match checkpoint thread',\n )\n }\n if (typeof checkpoint.id !== 'string' || checkpoint.id.length === 0) {\n throw new SandboxCheckpointInvalidIdError(\n 'Checkpoint id must be non-empty',\n )\n }\n if (hasUnpairedSurrogate(checkpoint.id)) {\n throw new SandboxCheckpointInvalidIdError(\n 'Checkpoint id must contain valid Unicode',\n )\n }\n if (hasUnpairedSurrogate(checkpoint.threadId)) {\n throw new SandboxCheckpointInvalidIdError(\n 'Checkpoint thread id must contain valid Unicode',\n )\n }\n if (\n typeof checkpoint.createdAt !== 'number' ||\n !Number.isFinite(checkpoint.createdAt)\n ) {\n throw new SandboxCheckpointInvalidEntryError(\n 'Checkpoint createdAt must be a finite number',\n )\n }\n if (expectedHeadId === '') {\n throw new SandboxCheckpointInvalidIdError(\n 'Expected head id must be null or non-empty',\n )\n }\n const parentCheckpointId = checkpoint.parentCheckpointId ?? null\n if (parentCheckpointId === '') {\n throw new SandboxCheckpointInvalidIdError(\n 'Parent checkpoint id must be null or non-empty',\n )\n }\n if (expectedHeadId !== null && hasUnpairedSurrogate(expectedHeadId)) {\n throw new SandboxCheckpointInvalidIdError(\n 'Expected head id must contain valid Unicode',\n )\n }\n if (\n parentCheckpointId !== null &&\n hasUnpairedSurrogate(parentCheckpointId)\n ) {\n throw new SandboxCheckpointInvalidIdError(\n 'Parent checkpoint id must contain valid Unicode',\n )\n }\n validateEntries(checkpoint)\n validateArtifacts(checkpoint)\n\n // The caller can re-enter append while its checkpoint is staged. Recheck\n // live writer and CAS state immediately before publishing.\n this.assertWriter(writer, checkpoint.threadId)\n if (this.state.checkpoints.has(checkpoint.id)) {\n throw new SandboxCheckpointDuplicateIdError(\n `Checkpoint '${checkpoint.id}' already exists`,\n )\n }\n const actualHeadId = this.state.heads.get(checkpoint.threadId) ?? null\n if (actualHeadId !== expectedHeadId) {\n throw new SandboxCheckpointConflictError(\n `Expected head '${expectedHeadId}', but thread '${checkpoint.threadId}' is at '${actualHeadId}'`,\n )\n }\n if (parentCheckpointId !== expectedHeadId) {\n throw new SandboxCheckpointParentMismatchError(\n `Checkpoint '${checkpoint.id}' parent does not match expected head`,\n )\n }\n const stored = { ...checkpoint, parentCheckpointId }\n const keys = blobKeys(stored)\n this.state.checkpoints.set(stored.id, stored)\n this.state.heads.set(stored.threadId, stored.id)\n for (const key of keys) {\n this.state.references.set(key, (this.state.references.get(key) ?? 0) + 1)\n }\n return { headId: stored.id }\n }\n\n async deleteHead(input: {\n threadId: string\n checkpointId: string\n writer: SandboxCheckpointWriter\n }): Promise<void> {\n const { threadId, checkpointId, writer } = input\n assertValidIdentifier(threadId, 'Thread id')\n assertValidIdentifier(checkpointId, 'Checkpoint id')\n assertValidIdentifier(writer.threadId, 'Writer thread id')\n if (writer.threadId !== threadId) {\n throw new SandboxCheckpointWriterLostError(\n 'Checkpoint writer thread does not match operation thread',\n )\n }\n this.assertWriter(writer, threadId)\n const headId = this.state.heads.get(threadId) ?? null\n if (headId !== checkpointId) {\n throw new SandboxCheckpointNotHeadError(\n `Checkpoint '${checkpointId}' is not the current head of thread '${threadId}'`,\n )\n }\n const checkpoint = this.state.checkpoints.get(checkpointId)\n if (!checkpoint) {\n throw new SandboxCheckpointNotHeadError(\n `Checkpoint '${checkpointId}' does not exist`,\n )\n }\n this.state.checkpoints.delete(checkpointId)\n if (checkpoint.parentCheckpointId) {\n this.state.heads.set(threadId, checkpoint.parentCheckpointId)\n } else {\n this.state.heads.delete(threadId)\n }\n for (const key of blobKeys(checkpoint)) {\n const references = (this.state.references.get(key) ?? 0) - 1\n if (references > 0) this.state.references.set(key, references)\n else this.state.references.delete(key)\n }\n }\n\n async acquireWriter(threadId: string): Promise<SandboxCheckpointWriterLease> {\n assertValidIdentifier(threadId, 'Thread id')\n const current = this.state.writers.get(threadId)\n if (current && current.expiresAt > this.now()) {\n throw new SandboxCheckpointWriterConflictError(\n `Thread '${threadId}' already has an active checkpoint writer`,\n )\n }\n const fence = (this.state.fences.get(threadId) ?? 0) + 1\n this.state.fences.set(threadId, fence)\n const ownerToken = globalThis.crypto.randomUUID()\n const lease = {\n threadId,\n ownerToken,\n fence,\n expiresAt: this.now() + this.leaseDurationMs,\n }\n this.state.writers.set(threadId, lease)\n return {\n ...lease,\n get expiresAt() {\n return lease.expiresAt\n },\n renewAfterMs: this.renewAfterMs,\n renew: async () => {\n this.assertWriter(lease, threadId)\n lease.expiresAt = this.now() + this.leaseDurationMs\n return { expiresAt: lease.expiresAt }\n },\n release: async () => {\n const currentLease = this.state.writers.get(threadId)\n if (\n currentLease?.ownerToken === ownerToken &&\n currentLease.fence === fence\n )\n this.state.writers.delete(threadId)\n },\n }\n }\n\n private assertWriter(\n writer: SandboxCheckpointWriter,\n threadId: string,\n ): void {\n const current = this.state.writers.get(threadId)\n if (\n !current ||\n current.ownerToken !== writer.ownerToken ||\n current.fence !== writer.fence ||\n current.expiresAt <= this.now()\n ) {\n throw new SandboxCheckpointWriterLostError(\n `Checkpoint writer lease for thread '${threadId}' is no longer current`,\n )\n }\n }\n\n async listBlobReferences(): Promise<\n Array<{ key: string; references: number }>\n > {\n return Array.from(this.state.references.entries())\n .sort(([a], [b]) => compareUtf8Bytes(a, b))\n .map(([key, references]) => ({ key, references }))\n }\n}\n"],"mappings":"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"}
|