@deepstrike/wasm 0.2.50 → 0.2.51

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.
Files changed (37) hide show
  1. package/README.md +2 -1
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +3 -2
  4. package/dist/runtime/canonical-kernel-step.d.ts +146 -0
  5. package/dist/runtime/canonical-kernel-step.js +1484 -0
  6. package/dist/runtime/execution-plane.d.ts +0 -3
  7. package/dist/runtime/execution-plane.js +0 -25
  8. package/dist/runtime/facade.js +3 -0
  9. package/dist/runtime/index.d.ts +7 -7
  10. package/dist/runtime/index.js +4 -3
  11. package/dist/runtime/kernel-event-log.d.ts +0 -1
  12. package/dist/runtime/kernel-event-log.js +7 -13
  13. package/dist/runtime/kernel-journal.d.ts +332 -0
  14. package/dist/runtime/kernel-journal.js +681 -0
  15. package/dist/runtime/kernel-step.d.ts +34 -89
  16. package/dist/runtime/kernel-step.js +67 -167
  17. package/dist/runtime/os-snapshot.d.ts +2 -2
  18. package/dist/runtime/os-snapshot.js +2 -6
  19. package/dist/runtime/payload-store.d.ts +30 -0
  20. package/dist/runtime/payload-store.js +66 -0
  21. package/dist/runtime/runner.d.ts +27 -75
  22. package/dist/runtime/runner.js +408 -632
  23. package/dist/runtime/session-log.d.ts +24 -27
  24. package/dist/runtime/session-log.js +11 -52
  25. package/dist/runtime/session-repair.d.ts +2 -36
  26. package/dist/runtime/session-repair.js +2 -47
  27. package/dist/runtime/sub-agent-orchestrator.d.ts +2 -2
  28. package/dist/runtime/sub-agent-orchestrator.js +36 -40
  29. package/dist/runtime/types/agent.d.ts +13 -16
  30. package/dist/runtime/types/agent.js +16 -41
  31. package/package.json +4 -3
  32. package/dist/runtime/kernel-rebuild.d.ts +0 -8
  33. package/dist/runtime/kernel-rebuild.js +0 -65
  34. package/dist/runtime/kernel-transaction-log.d.ts +0 -61
  35. package/dist/runtime/kernel-transaction-log.js +0 -140
  36. package/dist/runtime/large-result-spool.d.ts +0 -57
  37. package/dist/runtime/large-result-spool.js +0 -169
@@ -18,68 +18,13 @@ interface SkillMetadata {
18
18
  * `stable-core ∪ allowedTools`. Absent ⇒ no narrowing (back-compat). */
19
19
  allowedTools?: string[];
20
20
  }
21
- export declare const KERNEL_ABI_VERSION = 2;
22
- export interface KernelRuntimeHandle {
23
- step(inputJson: string): string;
24
- prepareStep(inputJson: string): string;
25
- commitPrepared(prepareToken: string): string;
26
- abortPrepared(prepareToken: string): void;
27
- snapshot(): string;
28
- restore(snapshotJson: string): void;
29
- diagnostics(): string;
30
- isTerminal(): boolean;
31
- turn(): number;
32
- recoveryContentBytes(): number;
33
- render(): RenderedContext;
34
- drainNewMessages(): Message[];
35
- preservedRefs(): string[];
36
- }
37
- export type KernelPreparationStatus = "prepared" | "replayed" | "rejected";
38
- export interface KernelPreparedStep {
39
- status: KernelPreparationStatus;
40
- base_generation: number;
41
- prepare_token?: string;
42
- input: Record<string, unknown>;
43
- step: KernelStepJson;
44
- }
45
- export interface KernelDiagnostics {
46
- lifecycle: string;
47
- next_step_seq: number;
48
- accepted_input_count: number;
49
- accepted_input_bytes: number;
50
- snapshot_input_limit: number;
51
- snapshot_journal_bytes_limit: number;
52
- max_input_bytes: number;
53
- snapshot_overflowed: boolean;
54
- recorded_event_count: number;
55
- completed_effect_count: number;
56
- pending_effect_count: number;
57
- }
58
- export declare function readKernelDiagnostics(runtime: KernelRuntimeHandle): KernelDiagnostics;
59
- export interface KernelSnapshot {
60
- snapshot_version: 2;
61
- abi_version: 2;
62
- initial_policy: {
63
- max_tokens: number;
64
- max_turns: number;
65
- max_total_tokens: string;
66
- max_wall_ms?: string;
67
- };
68
- lifecycle: string;
69
- operation_id?: string;
70
- next_step_seq: number;
71
- snapshot_input_limit: number;
72
- max_input_bytes: number;
73
- snapshot_journal_bytes_limit: number;
74
- accepted_input_bytes: number;
75
- accepted_inputs: Array<{
76
- event_id: string;
77
- [key: string]: unknown;
78
- }>;
79
- last_step?: Record<string, unknown>;
80
- }
81
- export declare function snapshotKernelRuntime(runtime: KernelRuntimeHandle): KernelSnapshot;
82
- export declare function restoreKernelRuntime(runtime: KernelRuntimeHandle, snapshot: KernelSnapshot): void;
21
+ export declare const CANONICAL_CONTENT_PARTS_PREFIX = "[[deepstrike-content-parts:v1]]";
22
+ /**
23
+ * Browser/worker-safe transport for typed content. `btoa` and `atob` operate on
24
+ * binary strings, so explicitly bridge UTF-8 bytes rather than assuming Latin-1.
25
+ */
26
+ export declare function encodeCanonicalContentParts(parts: unknown[]): string;
27
+ export declare function decodeCanonicalContentParts(content: string): Array<Record<string, unknown>> | undefined;
83
28
  export interface PaceDecision {
84
29
  action: "continue" | "sleep" | "stop";
85
30
  delayMs?: number;
@@ -91,6 +36,7 @@ export interface KernelLoopResult {
91
36
  termination: string;
92
37
  turnsUsed: number;
93
38
  totalTokensUsed: number;
39
+ finalMessage?: Message;
94
40
  /** ③ loop-agent: the kernel-adjudicated after-round decision (absent on non-loop runs). */
95
41
  paceDecision?: PaceDecision;
96
42
  }
@@ -121,6 +67,10 @@ export type KernelRunnerAction = {
121
67
  kind: "preempt_sub_agents";
122
68
  effectId: string;
123
69
  agentIds: string[];
70
+ attempts?: Array<{
71
+ task_id: string;
72
+ attempt_id: string;
73
+ }>;
124
74
  reason: string;
125
75
  } | {
126
76
  kind: "persist_memory";
@@ -131,28 +81,36 @@ export type KernelRunnerAction = {
131
81
  effectId: string;
132
82
  query: Record<string, unknown>;
133
83
  requestedK: number;
134
- } | {
135
- kind: "spool_large_result";
136
- effectId: string;
137
- callId: string;
138
- tool: string;
139
- output: string;
140
- originalSize: number;
141
- previewSize: number;
142
84
  } | {
143
85
  kind: "archive_page_out";
144
86
  effectId: string;
145
- turn: number;
146
- action: string;
87
+ turn?: number;
88
+ action?: string;
147
89
  summary?: string;
148
90
  archived: Message[];
149
- tier: string;
91
+ tier?: string;
92
+ handleId?: string;
93
+ payload?: {
94
+ content: string;
95
+ digest: string;
96
+ original_size: string;
97
+ preview?: string;
98
+ };
99
+ } | {
100
+ kind: "load_payload";
101
+ effectId: string;
102
+ handleId: string;
103
+ payloadRef: string;
150
104
  } | {
151
105
  kind: "evaluate_milestone";
152
106
  effectId: string;
153
107
  phaseId: string;
154
108
  criteria: string[];
155
109
  requiredEvidence?: string[];
110
+ } | {
111
+ kind: "unsupported_effect";
112
+ effectId: string;
113
+ effectKind: string;
156
114
  } | {
157
115
  kind: "done";
158
116
  effectId: string;
@@ -181,7 +139,7 @@ export interface KernelObservation {
181
139
  evidence?: string[];
182
140
  reason?: RollbackReason | string;
183
141
  agent_id?: string;
184
- parent_session_id?: string;
142
+ parent_task_id?: string;
185
143
  role?: string;
186
144
  isolation?: string;
187
145
  context_inheritance?: string;
@@ -210,7 +168,6 @@ export interface KernelObservation {
210
168
  tier?: string;
211
169
  message_count?: number;
212
170
  archive_ref?: string;
213
- spool_ref?: string;
214
171
  record_id?: string;
215
172
  scope?: {
216
173
  tenant_id: string;
@@ -249,16 +206,6 @@ export interface KernelObservation {
249
206
  window_turns?: number;
250
207
  threshold?: number;
251
208
  }
252
- interface KernelStepJson {
253
- version: number;
254
- actions: Array<Record<string, unknown>>;
255
- observations: KernelObservation[];
256
- faults?: Array<{
257
- code?: string;
258
- message?: string;
259
- effect_id?: string;
260
- }>;
261
- }
262
209
  export declare function toolSchemaToKernel(schema: ToolSchema): Record<string, unknown>;
263
210
  export declare function skillMetadataToKernel(skill: SkillMetadata): Record<string, unknown>;
264
211
  export declare function messageToKernelMessage(message: Message): Record<string, unknown>;
@@ -267,8 +214,6 @@ export declare function taskUpdateToKernel(update: TaskUpdate): Record<string, u
267
214
  export declare function capabilityTool(schema: ToolSchema): Record<string, unknown>;
268
215
  export declare function capabilitySkill(skill: SkillMetadata): Record<string, unknown>;
269
216
  export declare function capabilityMarker(kind: string, id: string, description: string): Record<string, unknown>;
270
- export declare function kernelApply(runtime: KernelRuntimeHandle, pending: KernelObservation[], event: Record<string, unknown>): KernelObservation[];
271
- export declare function kernelAction(runtime: KernelRuntimeHandle, pending: KernelObservation[], event: Record<string, unknown>): KernelRunnerAction;
272
- /** Like kernelAction but tolerates zero-action steps (e.g. queued signals). */
273
- export declare function kernelMaybeAction(runtime: KernelRuntimeHandle, pending: KernelObservation[], event: Record<string, unknown>): KernelRunnerAction | null;
217
+ export declare function kernelMessageToSdk(raw: Record<string, unknown>): Message;
218
+ export declare function renderedContextToSdk(raw: Record<string, unknown>): RenderedContext;
274
219
  export {};
@@ -1,24 +1,30 @@
1
- export const KERNEL_ABI_VERSION = 2;
2
- export function readKernelDiagnostics(runtime) {
3
- return JSON.parse(runtime.diagnostics());
4
- }
5
- export function snapshotKernelRuntime(runtime) {
6
- return JSON.parse(runtime.snapshot());
7
- }
8
- export function restoreKernelRuntime(runtime, snapshot) {
9
- runtime.restore(JSON.stringify(snapshot));
10
- const operationId = snapshot.operation_id;
11
- if (!operationId) {
12
- kernelWireStates.delete(runtime);
13
- return;
1
+ export const CANONICAL_CONTENT_PARTS_PREFIX = "[[deepstrike-content-parts:v1]]";
2
+ /**
3
+ * Browser/worker-safe transport for typed content. `btoa` and `atob` operate on
4
+ * binary strings, so explicitly bridge UTF-8 bytes rather than assuming Latin-1.
5
+ */
6
+ export function encodeCanonicalContentParts(parts) {
7
+ const bytes = new TextEncoder().encode(JSON.stringify(parts));
8
+ let binary = "";
9
+ for (const byte of bytes)
10
+ binary += String.fromCharCode(byte);
11
+ return `${CANONICAL_CONTENT_PARTS_PREFIX}${btoa(binary)}`;
12
+ }
13
+ export function decodeCanonicalContentParts(content) {
14
+ if (!content.startsWith(CANONICAL_CONTENT_PARTS_PREFIX))
15
+ return undefined;
16
+ try {
17
+ const binary = atob(content.slice(CANONICAL_CONTENT_PARTS_PREFIX.length));
18
+ const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
19
+ const decoded = JSON.parse(new TextDecoder().decode(bytes));
20
+ return Array.isArray(decoded)
21
+ ? decoded.filter((part) => Boolean(part) && typeof part === "object")
22
+ : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
14
26
  }
15
- const nextEventSequence = snapshot.accepted_inputs.reduce((next, input) => {
16
- const match = input.event_id.match(/-event-(\d+)$/);
17
- return match ? Math.max(next, Number(match[1]) + 1) : next;
18
- }, 1);
19
- kernelWireStates.set(runtime, { operationId, nextEventSequence });
20
27
  }
21
- const kernelWireStates = new WeakMap();
22
28
  function tryParseJson(s) {
23
29
  try {
24
30
  return JSON.parse(s);
@@ -126,10 +132,7 @@ export function capabilitySkill(skill) {
126
132
  export function capabilityMarker(kind, id, description) {
127
133
  return { id, kind, description };
128
134
  }
129
- function parseStep(raw) {
130
- return JSON.parse(raw);
131
- }
132
- function kernelMessageToSdk(raw) {
135
+ export function kernelMessageToSdk(raw) {
133
136
  const content = raw.content;
134
137
  const message = {
135
138
  role: raw.role,
@@ -152,9 +155,49 @@ function kernelMessageToSdk(raw) {
152
155
  if (typeof raw.token_count === "number") {
153
156
  message.tokenCount = raw.token_count;
154
157
  }
158
+ if (typeof content === "string") {
159
+ const parts = decodeCanonicalContentParts(content);
160
+ if (parts) {
161
+ const contentParts = [];
162
+ for (const part of parts) {
163
+ switch (part.type) {
164
+ case "text":
165
+ contentParts.push({ type: "text", text: String(part.text ?? "") });
166
+ break;
167
+ case "tool_result":
168
+ contentParts.push({
169
+ type: "tool_result",
170
+ callId: String(part.call_id ?? ""),
171
+ output: String(part.output ?? ""),
172
+ isError: Boolean(part.is_error),
173
+ });
174
+ break;
175
+ case "image":
176
+ contentParts.push({
177
+ type: "image",
178
+ ...(part.url ? { url: String(part.url) } : {}),
179
+ ...(part.data ? { data: String(part.data) } : {}),
180
+ ...(part.media_type ? { mediaType: String(part.media_type) } : {}),
181
+ ...(part.detail === "auto" || part.detail === "low" || part.detail === "high"
182
+ ? { detail: part.detail }
183
+ : {}),
184
+ });
185
+ break;
186
+ case "audio":
187
+ contentParts.push({
188
+ type: "audio",
189
+ data: String(part.data ?? ""),
190
+ ...(part.media_type ? { mediaType: String(part.media_type) } : {}),
191
+ });
192
+ break;
193
+ }
194
+ }
195
+ message.contentParts = contentParts;
196
+ }
197
+ }
155
198
  return message;
156
199
  }
157
- function renderedContextToSdk(raw) {
200
+ export function renderedContextToSdk(raw) {
158
201
  const rawStateTurn = (raw.state_turn ?? raw.stateTurn);
159
202
  const frozenLen = (raw.frozen_prefix_len ?? raw.frozenPrefixLen);
160
203
  const ctx = {
@@ -169,146 +212,3 @@ function renderedContextToSdk(raw) {
169
212
  ctx.frozenPrefixLen = frozenLen;
170
213
  return ctx;
171
214
  }
172
- function mapKernelAction(raw) {
173
- const effectId = String(raw.effect_id ?? "");
174
- if (!effectId)
175
- throw new Error(`kernel action ${String(raw.kind)} is missing effect_id`);
176
- switch (raw.kind) {
177
- case "call_provider":
178
- return {
179
- kind: "call_provider",
180
- effectId,
181
- context: renderedContextToSdk(raw.context ?? {}),
182
- tools: (raw.tools ?? []).map(t => ({
183
- name: String(t.name ?? ""),
184
- description: String(t.description ?? ""),
185
- parameters: JSON.stringify(t.parameters ?? {}),
186
- })),
187
- };
188
- case "execute_tool":
189
- return {
190
- kind: "execute_tool",
191
- effectId,
192
- calls: (raw.calls ?? []).map(c => ({
193
- id: String(c.id ?? ""),
194
- name: String(c.name ?? ""),
195
- arguments: JSON.stringify(c.arguments ?? {}),
196
- })),
197
- };
198
- case "request_approval":
199
- return {
200
- kind: "request_approval", effectId,
201
- requests: (raw.requests ?? []).map(request => ({
202
- callId: String(request.call_id ?? ""), tool: String(request.tool ?? ""),
203
- arguments: JSON.stringify(request.arguments ?? {}), reason: String(request.reason ?? ""),
204
- })),
205
- };
206
- case "spawn_workflow":
207
- return {
208
- kind: "spawn_workflow", effectId,
209
- nodes: raw.nodes ?? [],
210
- ...(raw.budget && typeof raw.budget === "object" ? { budget: raw.budget } : {}),
211
- };
212
- case "preempt_sub_agents":
213
- return { kind: "preempt_sub_agents", effectId, agentIds: raw.agent_ids ?? [], reason: String(raw.reason ?? "") };
214
- case "persist_memory":
215
- return { kind: "persist_memory", effectId, memory: raw.memory ?? {} };
216
- case "query_memory":
217
- return { kind: "query_memory", effectId, query: raw.query ?? {}, requestedK: Number(raw.requested_k ?? 0) };
218
- case "spool_large_result":
219
- return {
220
- kind: "spool_large_result", effectId, callId: String(raw.call_id ?? ""), tool: String(raw.tool ?? ""),
221
- output: String(raw.output ?? ""), originalSize: Number(raw.original_size ?? 0), previewSize: Number(raw.preview_size ?? 0),
222
- };
223
- case "archive_page_out":
224
- return {
225
- kind: "archive_page_out", effectId, turn: Number(raw.turn ?? 0), action: String(raw.action ?? "auto_compact"),
226
- ...(typeof raw.summary === "string" ? { summary: raw.summary } : {}),
227
- archived: (raw.archived ?? []).map(kernelMessageToSdk),
228
- tier: String(raw.tier ?? "durable"),
229
- };
230
- case "evaluate_milestone":
231
- return {
232
- kind: "evaluate_milestone",
233
- effectId,
234
- phaseId: String(raw.phase_id ?? ""),
235
- criteria: raw.criteria ?? [],
236
- requiredEvidence: raw.required_evidence ?? [],
237
- };
238
- case "done": {
239
- const result = raw.result ?? {};
240
- const pace = result.pace_decision;
241
- return {
242
- kind: "done",
243
- effectId,
244
- result: {
245
- termination: String(result.termination ?? "error"),
246
- turnsUsed: Number(result.turns_used ?? 0),
247
- totalTokensUsed: Number(result.total_tokens_used ?? 0),
248
- // ③ loop-agent: the kernel-adjudicated after-round decision (absent on non-loop runs).
249
- ...(pace
250
- ? {
251
- paceDecision: {
252
- action: (pace.action ?? "stop"),
253
- delayMs: pace.delay_ms,
254
- reason: pace.reason ?? "",
255
- coercedFrom: pace.coerced_from,
256
- },
257
- }
258
- : {}),
259
- },
260
- };
261
- }
262
- default:
263
- throw new Error(`unknown KernelAction kind: ${String(raw.kind)}`);
264
- }
265
- }
266
- function stepInput(runtime, event) {
267
- let state = kernelWireStates.get(runtime);
268
- if (!state) {
269
- // Globally unique, never a process-local counter: durable session logs key the kernel
270
- // genesis/transaction chains by (sessionId, operationId) and outlive this isolate, so a
271
- // counter that restarts at 1 collides with a prior chain on the same session.
272
- state = { operationId: `wasm-operation-${crypto.randomUUID()}`, nextEventSequence: 1 };
273
- kernelWireStates.set(runtime, state);
274
- }
275
- const correlatedEvent = event.kind === "cancel_operation"
276
- ? { ...event, operation_id: state.operationId }
277
- : event;
278
- return JSON.stringify({
279
- version: KERNEL_ABI_VERSION,
280
- operation_id: state.operationId,
281
- event_id: `${state.operationId}-event-${state.nextEventSequence++}`,
282
- observed_at_ms: Date.now(),
283
- event: correlatedEvent,
284
- });
285
- }
286
- export function kernelApply(runtime, pending, event) {
287
- const step = parseStep(runtime.step(stepInput(runtime, event)));
288
- const fault = step.faults?.[0];
289
- if (fault)
290
- throw new Error(`${fault.code ?? "kernel_fault"}: ${fault.message ?? "kernel transition failed"}`);
291
- pending.push(...step.observations);
292
- return step.observations;
293
- }
294
- export function kernelAction(runtime, pending, event) {
295
- const step = parseStep(runtime.step(stepInput(runtime, event)));
296
- const fault = step.faults?.[0];
297
- if (fault)
298
- throw new Error(`${fault.code ?? "kernel_fault"}: ${fault.message ?? "kernel transition failed"}`);
299
- pending.push(...step.observations);
300
- const raw = step.actions[0];
301
- if (!raw)
302
- throw new Error("kernel transition must return one action");
303
- return mapKernelAction(raw);
304
- }
305
- /** Like kernelAction but tolerates zero-action steps (e.g. queued signals). */
306
- export function kernelMaybeAction(runtime, pending, event) {
307
- const step = parseStep(runtime.step(stepInput(runtime, event)));
308
- const fault = step.faults?.[0];
309
- if (fault)
310
- throw new Error(`${fault.code ?? "kernel_fault"}: ${fault.message ?? "kernel transition failed"}`);
311
- pending.push(...step.observations);
312
- const raw = step.actions[0];
313
- return raw ? mapKernelAction(raw) : null;
314
- }
@@ -9,7 +9,8 @@ export interface OsSnapshot {
9
9
  processByAgent: Array<{
10
10
  turn: number;
11
11
  agent_id: string;
12
- parent_session_id: string;
12
+ parent_task_id?: string;
13
+ parent_session_id?: string;
13
14
  state: string;
14
15
  }>;
15
16
  budgetExceeded: Array<{
@@ -43,7 +44,6 @@ export interface OsSnapshot {
43
44
  }>;
44
45
  pageOutCount: number;
45
46
  pageInCount: number;
46
- spoolCount: number;
47
47
  toolGatedCount: number;
48
48
  }
49
49
  export declare function rebuildOsSnapshotFromSessionEvents(events: SessionEvent[]): OsSnapshot;
@@ -2,7 +2,6 @@ const KERNEL_KINDS = new Set([
2
2
  "compressed",
3
3
  "page_out",
4
4
  "page_in",
5
- "large_result_spooled",
6
5
  "capability_changed",
7
6
  "context_renewed",
8
7
  "suspended",
@@ -27,7 +26,6 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
27
26
  signals: [],
28
27
  pageOutCount: 0,
29
28
  pageInCount: 0,
30
- spoolCount: 0,
31
29
  toolGatedCount: 0,
32
30
  };
33
31
  const index = new Map();
@@ -53,7 +51,8 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
53
51
  const record = {
54
52
  turn: event.turn,
55
53
  agent_id: event.agent_id,
56
- parent_session_id: event.parent_session_id,
54
+ ...(event.parent_task_id ? { parent_task_id: event.parent_task_id } : {}),
55
+ ...(event.parent_session_id ? { parent_session_id: event.parent_session_id } : {}),
57
56
  state: event.state ?? "running",
58
57
  };
59
58
  const idx = index.get(event.agent_id);
@@ -91,9 +90,6 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
91
90
  case "page_in":
92
91
  snap.pageInCount += 1;
93
92
  break;
94
- case "large_result_spooled":
95
- snap.spoolCount += 1;
96
- break;
97
93
  default:
98
94
  break;
99
95
  }
@@ -0,0 +1,30 @@
1
+ export interface PayloadStorageDriver {
2
+ write(key: string, content: string): Promise<void> | void;
3
+ read(key: string): Promise<string | undefined> | string | undefined;
4
+ delete(key: string): Promise<void> | void;
5
+ list(): Promise<string[]> | string[];
6
+ mtime?(key: string): Promise<number> | number;
7
+ }
8
+ export declare class MemoryPayloadDriver implements PayloadStorageDriver {
9
+ private readonly values;
10
+ write(key: string, content: string): Promise<void>;
11
+ read(key: string): Promise<string | undefined>;
12
+ delete(key: string): Promise<void>;
13
+ list(): Promise<string[]>;
14
+ mtime(key: string): Promise<number>;
15
+ }
16
+ export interface PayloadStoreConfig {
17
+ driver?: PayloadStorageDriver;
18
+ maxAgeMs?: number;
19
+ }
20
+ /** Driver-backed storage for canonical opaque payload locators. */
21
+ export declare class PayloadStore {
22
+ private readonly driver;
23
+ private readonly maxAgeMs?;
24
+ private readonly activeWrites;
25
+ constructor(config?: PayloadStoreConfig);
26
+ private key;
27
+ persistPayload(sessionId: string, payloadRef: string, content: string): Promise<void>;
28
+ loadPayload(sessionId: string, payloadRef: string): Promise<string | undefined>;
29
+ cleanup(maxAgeMs?: number): Promise<number>;
30
+ }
@@ -0,0 +1,66 @@
1
+ export class MemoryPayloadDriver {
2
+ values = new Map();
3
+ async write(key, content) {
4
+ this.values.set(key, { content, mtime: Date.now() });
5
+ }
6
+ async read(key) {
7
+ return this.values.get(key)?.content;
8
+ }
9
+ async delete(key) {
10
+ this.values.delete(key);
11
+ }
12
+ async list() {
13
+ return [...this.values.keys()];
14
+ }
15
+ async mtime(key) {
16
+ return this.values.get(key)?.mtime ?? 0;
17
+ }
18
+ }
19
+ async function sha256(value) {
20
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
21
+ return [...new Uint8Array(digest)]
22
+ .map(byte => byte.toString(16).padStart(2, "0"))
23
+ .join("");
24
+ }
25
+ /** Driver-backed storage for canonical opaque payload locators. */
26
+ export class PayloadStore {
27
+ driver;
28
+ maxAgeMs;
29
+ activeWrites = new Map();
30
+ constructor(config = {}) {
31
+ this.driver = config.driver ?? new MemoryPayloadDriver();
32
+ this.maxAgeMs = config.maxAgeMs;
33
+ }
34
+ async key(sessionId, payloadRef) {
35
+ return `payload/${await sha256(`${sessionId}\u0000${payloadRef}`)}`;
36
+ }
37
+ async persistPayload(sessionId, payloadRef, content) {
38
+ const key = await this.key(sessionId, payloadRef);
39
+ let write = this.activeWrites.get(key);
40
+ if (!write) {
41
+ write = Promise.resolve(this.driver.write(key, content)).finally(() => {
42
+ this.activeWrites.delete(key);
43
+ });
44
+ this.activeWrites.set(key, write);
45
+ }
46
+ await write;
47
+ }
48
+ async loadPayload(sessionId, payloadRef) {
49
+ return await this.driver.read(await this.key(sessionId, payloadRef));
50
+ }
51
+ async cleanup(maxAgeMs = this.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000) {
52
+ if (!this.driver.mtime)
53
+ return 0;
54
+ const now = Date.now();
55
+ let removed = 0;
56
+ for (const key of await this.driver.list()) {
57
+ if (!key.startsWith("payload/"))
58
+ continue;
59
+ if (now - await this.driver.mtime(key) > maxAgeMs) {
60
+ await this.driver.delete(key);
61
+ removed += 1;
62
+ }
63
+ }
64
+ return removed;
65
+ }
66
+ }