@deepstrike/wasm 0.2.42 → 0.2.44

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.
@@ -7,6 +7,12 @@ export interface AttemptRequest {
7
7
  sessionId?: string;
8
8
  goal: string;
9
9
  criteria?: Criterion[];
10
+ /**
11
+ * Multimodal inputs (images / audio) attached to the task. Forwarded to every attempt
12
+ * unconditionally; the runner seeds them per session idempotently, so fresh-session carries
13
+ * re-seed while same-session carries do not double.
14
+ */
15
+ attachments?: import("../types.js").ContentPart[];
10
16
  extensions?: Record<string, unknown>;
11
17
  inheritEvents?: Array<{
12
18
  seq: number;
@@ -14,6 +14,7 @@ export class RuntimeAttemptBody {
14
14
  sessionId: context.sessionId,
15
15
  goal: context.goal,
16
16
  criteria: (context.criteria ?? []).map(criterion => criterion.text),
17
+ ...(context.attachments?.length ? { attachments: context.attachments } : {}),
17
18
  extensions: context.extensions,
18
19
  ...(context.attempt === 1 && context.inheritEvents
19
20
  ? { inheritEvents: context.inheritEvents }
@@ -18,7 +18,6 @@ export function restoreKernelRuntime(runtime, snapshot) {
18
18
  }, 1);
19
19
  kernelWireStates.set(runtime, { operationId, nextEventSequence });
20
20
  }
21
- let nextOperationSequence = 1;
22
21
  const kernelWireStates = new WeakMap();
23
22
  function tryParseJson(s) {
24
23
  try {
@@ -267,7 +266,10 @@ function mapKernelAction(raw) {
267
266
  function stepInput(runtime, event) {
268
267
  let state = kernelWireStates.get(runtime);
269
268
  if (!state) {
270
- state = { operationId: `wasm-operation-${nextOperationSequence++}`, nextEventSequence: 1 };
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 };
271
273
  kernelWireStates.set(runtime, state);
272
274
  }
273
275
  const correlatedEvent = event.kind === "cancel_operation"
@@ -43,14 +43,15 @@ export declare class LargeResultSpool {
43
43
  private writeToDriver;
44
44
  private generatePreview;
45
45
  processToolResult(result: ToolResult): Promise<SpooledToolResult>;
46
- persistOutput(callId: string, content: string): Promise<string>;
46
+ private callKey;
47
+ persistOutput(sessionId: string, callId: string, content: string): Promise<string>;
47
48
  readSpooledResult(spoolRef: string): Promise<string>;
48
49
  /**
49
50
  * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
50
51
  * `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
51
- * the `.spool/${callId}-*.txt` naming convention; returns `undefined` if nothing was ever
52
+ * the hashed session-scoped call-key prefix; returns `undefined` if nothing was ever
52
53
  * spooled for that call.
53
54
  */
54
- findByCallId(callId: string): Promise<string | undefined>;
55
+ findByCallId(sessionId: string, callId: string): Promise<string | undefined>;
55
56
  cleanup(maxAgeMs?: number): Promise<number>;
56
57
  }
@@ -92,9 +92,16 @@ omitted: ${omitted} chars
92
92
  wasSpooled: true,
93
93
  };
94
94
  }
95
- async persistOutput(callId, content) {
95
+ callKey(sessionId, callId) {
96
+ // Session-scoped and hashed: the driver's key space is shared across sessions and outlives
97
+ // runs, while vendor call ids can be index-style ("call_0") and repeat — an unscoped key
98
+ // lets read_result in one session fetch another session's spooled output. Hashing also keeps
99
+ // untrusted call-id characters out of driver keys (parity with Node/Python).
100
+ return simpleHash(`${sessionId}\u0000${callId}`);
101
+ }
102
+ async persistOutput(sessionId, callId, content) {
96
103
  const hash = simpleHash(content);
97
- const key = `.spool/${callId}-${hash.slice(0, 16)}.txt`;
104
+ const key = `.spool/${this.callKey(sessionId, callId)}-${hash.slice(0, 16)}.txt`;
98
105
  let promise = this.activeWrites.get(key);
99
106
  if (!promise) {
100
107
  promise = (async () => {
@@ -116,10 +123,10 @@ omitted: ${omitted} chars
116
123
  /**
117
124
  * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
118
125
  * `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
119
- * the `.spool/${callId}-*.txt` naming convention; returns `undefined` if nothing was ever
126
+ * the hashed session-scoped call-key prefix; returns `undefined` if nothing was ever
120
127
  * spooled for that call.
121
128
  */
122
- async findByCallId(callId) {
129
+ async findByCallId(sessionId, callId) {
123
130
  let keys;
124
131
  try {
125
132
  keys = await this.driver.list();
@@ -127,7 +134,7 @@ omitted: ${omitted} chars
127
134
  catch {
128
135
  return undefined;
129
136
  }
130
- const prefix = `.spool/${callId}-`;
137
+ const prefix = `.spool/${this.callKey(sessionId, callId)}-`;
131
138
  const match = keys.find(k => k.startsWith(prefix) && k.endsWith('.txt'));
132
139
  if (!match)
133
140
  return undefined;
@@ -158,6 +158,12 @@ export class RuntimeRunner {
158
158
  async *run(req) {
159
159
  const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
160
160
  const midRun = isMidRun(prior);
161
+ // Idempotent per session: an earlier run's `run_started` already carries these attachments
162
+ // (same-session retry attempt), so replay reconstructs them — recording and seeding again
163
+ // would double them in history. Deduping at the append keeps live and replay in agreement.
164
+ const attachments = req.attachments?.length && !attachmentsAlreadySeeded(prior, req.attachments)
165
+ ? req.attachments
166
+ : undefined;
161
167
  if (!midRun) {
162
168
  await this.opts.sessionLog.append(req.sessionId, {
163
169
  kind: "run_started",
@@ -166,10 +172,10 @@ export class RuntimeRunner {
166
172
  criteria: req.criteria ?? [],
167
173
  agent_id: this.opts.agentId,
168
174
  system_prompt: this.opts.systemPrompt,
169
- ...(req.attachments?.length ? { attachments: req.attachments } : {}),
175
+ ...(attachments ? { attachments } : {}),
170
176
  });
171
177
  }
172
- yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments);
178
+ yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments);
173
179
  }
174
180
  async *wake(sessionId, extensions) {
175
181
  const events = await this.opts.sessionLog.read(sessionId);
@@ -352,7 +358,7 @@ export class RuntimeRunner {
352
358
  let full;
353
359
  if (full === undefined && this.opts.resultSpool) {
354
360
  try {
355
- full = await this.opts.resultSpool.findByCallId(callId);
361
+ full = await this.opts.resultSpool.findByCallId(sessionId, callId);
356
362
  }
357
363
  catch {
358
364
  full = undefined;
@@ -801,7 +807,7 @@ export class RuntimeRunner {
801
807
  let spoolRef;
802
808
  let error;
803
809
  try {
804
- spoolRef = await spool.persistOutput(action.callId, action.output);
810
+ spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
805
811
  }
806
812
  catch (cause) {
807
813
  error = formatToolError(cause);
@@ -2143,6 +2149,15 @@ function signalToKernelEvent(delivery) {
2143
2149
  },
2144
2150
  };
2145
2151
  }
2152
+ /**
2153
+ * True when an earlier run in this session already seeded the same attachments. Replay
2154
+ * reconstructs the attachment message from that run's `run_started`, so recording and
2155
+ * live-seeding them again (a same-session retry attempt) would double them in history.
2156
+ */
2157
+ function attachmentsAlreadySeeded(prior, attachments) {
2158
+ const wanted = JSON.stringify(attachments);
2159
+ return prior.some(({ event }) => event.kind === "run_started" && JSON.stringify(event.attachments ?? []) === wanted);
2160
+ }
2146
2161
  /** Convert SDK ContentParts (camelCase mediaType) to kernel serde shape (media_type). */
2147
2162
  function attachmentsToKernelMessage(parts) {
2148
2163
  const content = parts.map(p => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,12 +10,12 @@
10
10
  "README.md"
11
11
  ],
12
12
  "scripts": {
13
- "build:wasm": "wasm-pack build ../../crates/deepstrike-wasm --target bundler --out-dir ../../wasm/pkg",
13
+ "build:wasm": "wasm-pack build ../crates/deepstrike-wasm --target bundler --out-dir ../../wasm/pkg",
14
14
  "build": "tsc",
15
15
  "test": "node --experimental-vm-modules node_modules/.bin/jest"
16
16
  },
17
17
  "dependencies": {
18
- "@deepstrike/wasm-kernel": "0.2.42"
18
+ "@deepstrike/wasm-kernel": "0.2.44"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/jest": "^30.0.0",