@deepstrike/wasm 0.2.39 → 0.2.41

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 (48) hide show
  1. package/README.md +14 -13
  2. package/dist/harness/index.d.ts +127 -61
  3. package/dist/harness/index.js +212 -83
  4. package/dist/index.d.ts +13 -6
  5. package/dist/index.js +7 -2
  6. package/dist/memory/extraction.d.ts +4 -0
  7. package/dist/memory/extraction.js +48 -0
  8. package/dist/memory/in-memory-store.d.ts +19 -9
  9. package/dist/memory/in-memory-store.js +68 -23
  10. package/dist/memory/index.d.ts +52 -24
  11. package/dist/memory/ranking.d.ts +32 -0
  12. package/dist/memory/ranking.js +77 -0
  13. package/dist/memory/retention.d.ts +17 -0
  14. package/dist/memory/retention.js +54 -0
  15. package/dist/providers/base.d.ts +5 -0
  16. package/dist/providers/base.js +32 -0
  17. package/dist/runtime/context-policy.d.ts +35 -0
  18. package/dist/runtime/context-policy.js +66 -0
  19. package/dist/runtime/eval.d.ts +2 -0
  20. package/dist/runtime/execution-plane.d.ts +2 -1
  21. package/dist/runtime/execution-plane.js +3 -3
  22. package/dist/runtime/facade.js +2 -1
  23. package/dist/runtime/index.d.ts +13 -4
  24. package/dist/runtime/index.js +6 -1
  25. package/dist/runtime/kernel-event-log.js +46 -13
  26. package/dist/runtime/kernel-rebuild.d.ts +8 -0
  27. package/dist/runtime/kernel-rebuild.js +65 -0
  28. package/dist/runtime/kernel-step.d.ts +139 -7
  29. package/dist/runtime/kernel-step.js +110 -10
  30. package/dist/runtime/kernel-transaction-log.d.ts +61 -0
  31. package/dist/runtime/kernel-transaction-log.js +140 -0
  32. package/dist/runtime/os-profile.d.ts +9 -10
  33. package/dist/runtime/os-profile.js +14 -10
  34. package/dist/runtime/os-snapshot.d.ts +19 -0
  35. package/dist/runtime/os-snapshot.js +16 -3
  36. package/dist/runtime/runner.d.ts +73 -41
  37. package/dist/runtime/runner.js +562 -290
  38. package/dist/runtime/session-log.d.ts +55 -10
  39. package/dist/runtime/session-log.js +60 -3
  40. package/dist/runtime/session-repair.d.ts +11 -7
  41. package/dist/runtime/session-repair.js +11 -8
  42. package/dist/runtime/sub-agent-orchestrator.js +1 -1
  43. package/dist/runtime/types/agent.d.ts +31 -0
  44. package/dist/runtime/types/agent.js +35 -0
  45. package/dist/signals/index.d.ts +21 -1
  46. package/dist/signals/index.js +1 -0
  47. package/dist/types.d.ts +7 -2
  48. package/package.json +2 -2
@@ -1,12 +1,13 @@
1
+ import { extractSessionMemories } from "../memory/extraction.js";
1
2
  import { resolvePermissionRequest } from "./execution-plane.js";
2
3
  import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
3
4
  import { getKernel } from "./kernel.js";
4
5
  import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
5
6
  import { sanitizeReplayText } from "./replay-sanitize.js";
6
7
  import { formatToolError } from "../tools/errors.js";
7
- import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
8
+ import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverWorkflowNodeOutcomes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
8
9
  import { kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
9
- import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
10
+ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeOutcomeFromKernel, workflowNodeStatusFromTermination, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
10
11
  import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
11
12
  import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
12
13
  import { resolveReducer } from "./reducers.js";
@@ -14,9 +15,36 @@ import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote,
14
15
  import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
15
16
  import { assertNativeProfile } from "./os-profile.js";
16
17
  import { LargeResultSpool } from "./large-result-spool.js";
18
+ import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
19
+ export function schedulerPolicyToKernel(policy) {
20
+ const allowed = new Set([
21
+ "version", "criticalPathWeight", "fanoutWeight", "ageWeight", "tokenCostWeight",
22
+ ]);
23
+ const unknown = Object.keys(policy).filter(key => !allowed.has(key));
24
+ if (unknown.length > 0)
25
+ throw new TypeError(`unknown scheduler policy field(s): ${unknown.join(", ")}`);
26
+ return {
27
+ version: policy.version,
28
+ critical_path_weight: policy.criticalPathWeight,
29
+ fanout_weight: policy.fanoutWeight,
30
+ age_weight: policy.ageWeight,
31
+ token_cost_weight: policy.tokenCostWeight,
32
+ };
33
+ }
34
+ function pendingCallIds(action) {
35
+ switch (action.kind) {
36
+ case "call_provider": return [action.effectId];
37
+ case "execute_tool": return action.calls.map(call => call.id);
38
+ case "request_approval": return action.requests.map(request => request.callId);
39
+ case "spawn_workflow": return action.nodes.map(node => String(node.agent_id ?? "")).filter(Boolean);
40
+ case "preempt_sub_agents": return action.agentIds;
41
+ default: return "effectId" in action ? [action.effectId] : [];
42
+ }
43
+ }
17
44
  export class RuntimeRunner {
18
45
  opts;
19
46
  interrupted = false;
47
+ cancellationReason;
20
48
  /** #2-B-ii: aborts the in-flight provider stream on interrupt/preempt. Recreated per `execute`. */
21
49
  abortController = null;
22
50
  pendingObservations = [];
@@ -33,18 +61,28 @@ export class RuntimeRunner {
33
61
  /** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
34
62
  currentGoal = "";
35
63
  nextArchiveStart = 0;
64
+ pendingPageOutArchives = [];
65
+ activePageOutArchive;
36
66
  /** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
37
67
  * at the next safe point (after the tool turn resolves, kernel back in Reason). */
38
68
  pendingAuthoredWorkflows = [];
39
- pendingSpoolOutputs = new Map();
69
+ workflowContinuation = null;
40
70
  constructor(opts) {
41
71
  this.opts = opts;
72
+ const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
73
+ if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
74
+ throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
75
+ }
42
76
  }
43
77
  get hostOptions() { return this.opts; }
44
- interrupt() { this.interrupted = true; this.abortController?.abort(); }
78
+ interrupt(reason = "user") {
79
+ this.interrupted = true;
80
+ this.cancellationReason = reason;
81
+ this.abortController?.abort(reason);
82
+ }
45
83
  /** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
46
- * the next turn boundary, routes through the kernel attention policy, and once acted on — renders
47
- * as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. `urgency` maps to
84
+ * the next turn boundary, routes through the kernel attention policy, and renders once as a
85
+ * `[SIGNAL] <text>` line in the volatile state turn. `urgency` maps to
48
86
  * the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
49
87
  * preempts. */
50
88
  injectNote(text, urgency = "normal") {
@@ -60,10 +98,51 @@ export class RuntimeRunner {
60
98
  async nextInboundSignal() {
61
99
  const injected = this.injectedSignals.shift();
62
100
  if (injected)
63
- return injected;
101
+ return {
102
+ signalId: crypto.randomUUID(),
103
+ deliveryId: `injected-${crypto.randomUUID()}`,
104
+ deliveryAttempt: 1,
105
+ signal: injected,
106
+ ack: async () => true,
107
+ nack: async () => true,
108
+ };
64
109
  if (!this.opts.signalSource)
65
110
  return null;
66
- return this.opts.signalSource.nextSignal();
111
+ const source = this.opts.signalSource;
112
+ const claim = await source.claimSignal();
113
+ if (!claim)
114
+ return null;
115
+ const receipt = {
116
+ deliveryId: claim.deliveryId,
117
+ leaseToken: claim.leaseToken,
118
+ };
119
+ return {
120
+ signalId: claim.signalId,
121
+ deliveryId: claim.deliveryId,
122
+ deliveryAttempt: claim.deliveryAttempt,
123
+ signal: claim.signal,
124
+ ack: () => source.ackSignal(receipt),
125
+ nack: () => source.nackSignal(receipt),
126
+ };
127
+ }
128
+ async consumeInboundSignal(delivery, consume) {
129
+ try {
130
+ const observationStart = this.pendingObservations.length;
131
+ const result = consume(delivery);
132
+ const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
133
+ && observation.delivery_id === delivery.deliveryId
134
+ && observation.attempt === delivery.deliveryAttempt);
135
+ if (dispositions.length !== 1) {
136
+ throw new Error("kernel did not return the matching signal delivery disposition");
137
+ }
138
+ if (!await delivery.ack())
139
+ throw new Error("signal lease was lost before acknowledgement");
140
+ return result;
141
+ }
142
+ catch (cause) {
143
+ await delivery.nack();
144
+ throw cause;
145
+ }
67
146
  }
68
147
  async *run(req) {
69
148
  const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
@@ -76,9 +155,10 @@ export class RuntimeRunner {
76
155
  criteria: req.criteria ?? [],
77
156
  agent_id: this.opts.agentId,
78
157
  system_prompt: this.opts.systemPrompt,
158
+ ...(req.attachments?.length ? { attachments: req.attachments } : {}),
79
159
  });
80
160
  }
81
- yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun);
161
+ yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments);
82
162
  }
83
163
  async *wake(sessionId, extensions) {
84
164
  const events = await this.opts.sessionLog.read(sessionId);
@@ -88,7 +168,47 @@ export class RuntimeRunner {
88
168
  if (!startEntry)
89
169
  throw new Error(`No run_started event for session: ${sessionId}`);
90
170
  const start = startEntry.event;
91
- yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
171
+ yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments);
172
+ }
173
+ async writeMemory(memory, sessionId) {
174
+ if (!this.opts.dreamStore || !this.opts.agentId)
175
+ return;
176
+ const kernel = await getKernel();
177
+ const runtime = new kernel.KernelRuntime({ maxTokens: this.opts.maxTokens, maxTurns: this.opts.maxTurns ?? 25 });
178
+ const observations = [];
179
+ const action = kernelMaybeAction(runtime, observations, { kind: "write_memory", memory });
180
+ if (!action) {
181
+ await this.appendMemorySyscallObservations(sessionId, observations);
182
+ return;
183
+ }
184
+ if (action.kind !== "persist_memory")
185
+ throw new Error(`write_memory returned unexpected kernel effect: ${action.kind}`);
186
+ let error;
187
+ try {
188
+ await this.opts.dreamStore.upsert(this.opts.agentId, action.memory);
189
+ }
190
+ catch (cause) {
191
+ error = formatToolError(cause);
192
+ }
193
+ kernelApply(runtime, observations, {
194
+ kind: "memory_persist_result",
195
+ effect_id: action.effectId,
196
+ ...(error ? { error } : {}),
197
+ });
198
+ await this.appendMemorySyscallObservations(sessionId, observations);
199
+ if (error)
200
+ throw new Error(error);
201
+ }
202
+ async appendMemorySyscallObservations(sessionId, observations) {
203
+ if (!sessionId)
204
+ return;
205
+ for (const observation of observations) {
206
+ if (!["memory_written", "memory_queried", "memory_validation_failed"].includes(observation.kind))
207
+ continue;
208
+ const event = kernelObservationToSessionEvent(observation, 0);
209
+ if (event)
210
+ await this.opts.sessionLog.append(sessionId, event);
211
+ }
92
212
  }
93
213
  /** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
94
214
  * K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
@@ -120,26 +240,25 @@ export class RuntimeRunner {
120
240
  kernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
121
241
  this.knowledgePushedSkills.delete(name);
122
242
  }
123
- async resolveKernelSuspend(runtime, sessionId) {
124
- const gated = this.pendingObservations.filter((o) => o.kind === "tool_gated" && typeof o.call_id === "string" && typeof o.tool === "string");
243
+ async resolveKernelSuspend(requests, runtime, sessionId) {
125
244
  const approved = [];
126
245
  const denied = [];
127
246
  const events = [];
128
247
  const runCtx = { onPermissionRequest: this.opts.onPermissionRequest };
129
- for (const g of gated) {
248
+ for (const requestAction of requests) {
130
249
  const request = {
131
250
  type: "permission_request",
132
- callId: g.call_id,
133
- toolName: g.tool,
134
- arguments: "{}",
135
- reason: typeof g.reason === "string" ? g.reason : "",
251
+ callId: requestAction.callId,
252
+ toolName: requestAction.tool,
253
+ arguments: requestAction.arguments,
254
+ reason: requestAction.reason,
136
255
  };
137
256
  events.push(request);
138
257
  const decision = await resolvePermissionRequest(request, runCtx);
139
258
  events.push({
140
259
  type: "permission_resolved",
141
- callId: g.call_id,
142
- toolName: g.tool,
260
+ callId: requestAction.callId,
261
+ toolName: requestAction.tool,
143
262
  approved: decision.approved,
144
263
  responder: decision.responder ?? "host",
145
264
  ...(decision.reason ? { reason: decision.reason } : {}),
@@ -147,8 +266,8 @@ export class RuntimeRunner {
147
266
  await this.opts.sessionLog.append(sessionId, {
148
267
  kind: "permission_requested",
149
268
  turn: runtime.turn(),
150
- tool: g.tool,
151
- arguments: "{}",
269
+ tool: requestAction.tool,
270
+ arguments: requestAction.arguments,
152
271
  reason: request.reason,
153
272
  });
154
273
  await this.opts.sessionLog.append(sessionId, {
@@ -158,21 +277,21 @@ export class RuntimeRunner {
158
277
  responder: decision.responder ?? "host",
159
278
  });
160
279
  if (decision.approved) {
161
- approved.push(g.call_id);
280
+ approved.push(requestAction.callId);
162
281
  }
163
282
  else {
164
- denied.push(g.call_id);
283
+ denied.push(requestAction.callId);
165
284
  const denyReason = decision.reason ?? "permission denied";
166
285
  events.push({
167
286
  type: "tool_denied",
168
- callId: g.call_id,
169
- toolName: g.tool,
287
+ callId: requestAction.callId,
288
+ toolName: requestAction.tool,
170
289
  reason: denyReason,
171
290
  });
172
291
  events.push({
173
292
  type: "tool_result",
174
- callId: g.call_id,
175
- name: g.tool,
293
+ callId: requestAction.callId,
294
+ name: requestAction.tool,
176
295
  content: `permission denied: ${denyReason}`,
177
296
  isError: true,
178
297
  errorKind: "governance_denied",
@@ -180,15 +299,15 @@ export class RuntimeRunner {
180
299
  await this.opts.sessionLog.append(sessionId, {
181
300
  kind: "tool_denied",
182
301
  turn: runtime.turn(),
183
- call_id: g.call_id,
184
- tool_name: g.tool,
302
+ call_id: requestAction.callId,
303
+ tool_name: requestAction.tool,
185
304
  reason: denyReason,
186
305
  });
187
306
  await this.opts.sessionLog.append(sessionId, {
188
307
  kind: "tool_completed",
189
308
  turn: runtime.turn(),
190
309
  results: [{
191
- call_id: g.call_id,
310
+ call_id: requestAction.callId,
192
311
  output: `permission denied: ${denyReason}`,
193
312
  is_error: true,
194
313
  error_kind: "governance_denied",
@@ -200,10 +319,9 @@ export class RuntimeRunner {
200
319
  }
201
320
  /**
202
321
  * O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
203
- * output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map, (b) the result
204
- * spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
205
- * session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
206
- * resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
322
+ * output. Resolution order: (a) the result spool committed by `large_result_spool_result`,
323
+ * (b) a session-log scan for the original `tool_completed` event carrying that `call_id`.
324
+ * Slices the resolved text by `[offset, offset + maxBytes)` (plain string slice "bytes-ish").
207
325
  */
208
326
  async resolveReadResult(sessionId, argsJson) {
209
327
  let callId = "";
@@ -220,7 +338,7 @@ export class RuntimeRunner {
220
338
  catch {
221
339
  // malformed arguments — callId stays empty, falls through to "not found" below
222
340
  }
223
- let full = this.pendingSpoolOutputs.get(callId)?.output;
341
+ let full;
224
342
  if (full === undefined && this.opts.resultSpool) {
225
343
  try {
226
344
  full = await this.opts.resultSpool.findByCallId(callId);
@@ -255,71 +373,13 @@ export class RuntimeRunner {
255
373
  isError: false,
256
374
  };
257
375
  }
258
- async dream(agentId, nowMs = Date.now()) {
259
- if (!this.opts.dreamStore)
260
- throw new Error("dreamStore not configured");
261
- const kernel = await getKernel();
262
- const sessions = await this.opts.dreamStore.loadSessions(agentId);
263
- const existingMemories = await this.opts.dreamStore.loadMemories(agentId);
264
- if (!sessions.length)
265
- return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
266
- const pipeline = new kernel.IdlePipeline(agentId);
267
- const action1 = pipeline.feedTrigger(sessions.map(s => ({
268
- sessionId: s.sessionId, agentId: s.agentId,
269
- messages: s.messages.map(m => ({
270
- role: m.role, content: m.content, tokenCount: m.tokenCount,
271
- toolCalls: (m.toolCalls ?? []).map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments })),
272
- })),
273
- metadata: JSON.stringify(s.metadata ?? null),
274
- createdAtMs: s.createdAtMs, updatedAtMs: s.updatedAtMs,
275
- })), existingMemories.map(e => ({ text: e.text, score: e.score, metadata: JSON.stringify(e.metadata ?? null) })), nowMs);
276
- if (action1.kind === "noop" || action1.kind === "aborted") {
277
- return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
278
- }
279
- if (action1.kind !== "synthesize_insights")
280
- throw new Error(`unexpected: ${action1.kind}`);
281
- let synthesisText = "";
282
- const dreamProvider = this.opts.dreamProvider ?? this.opts.provider;
283
- const providerState = dreamProvider.createRunState?.();
284
- const synthMsgs = (action1.messages ?? []);
285
- const synthContext = {
286
- systemText: synthMsgs.filter(m => m.role === "system").map(m => m.content).join("\n\n"),
287
- turns: synthMsgs.filter(m => m.role !== "system"),
288
- };
289
- for await (const evt of dreamProvider.stream(synthContext, [], undefined, providerState)) {
290
- if (evt.type === "text_delta")
291
- synthesisText += evt.delta;
292
- }
293
- const action2 = pipeline.feedSynthesisResult(synthesisText);
294
- if (action2.kind !== "commit_memories")
295
- throw new Error(`unexpected: ${action2.kind}`);
296
- const cr = action2.curationResult;
297
- const rr = action2.runResult;
298
- const dsResult = {
299
- toAdd: (cr.toAdd ?? []).map((e) => ({
300
- text: e.text, score: e.score, metadata: tryParseJson(e.metadata),
301
- })),
302
- toRemoveIndices: (cr.toRemoveIndices ?? []).map(Number),
303
- stats: {
304
- insightsProcessed: cr.stats?.insightsProcessed ?? 0,
305
- duplicatesRemoved: cr.stats?.duplicatesRemoved ?? 0,
306
- conflictsResolved: cr.stats?.conflictsResolved ?? 0,
307
- entriesAdded: cr.stats?.entriesAdded ?? 0,
308
- },
309
- };
310
- await this.opts.dreamStore.commit(agentId, dsResult, existingMemories);
311
- return {
312
- sessionsProcessed: rr.sessionsProcessed,
313
- insightsExtracted: rr.insightsExtracted,
314
- entriesAdded: cr.stats?.entriesAdded ?? 0,
315
- entriesRemoved: (cr.toRemoveIndices ?? []).length,
316
- };
317
- }
318
- async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
376
+ async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
319
377
  this.interrupted = false;
378
+ this.cancellationReason = undefined;
320
379
  this.abortController = new AbortController();
321
380
  this.pendingObservations = [];
322
- this.pendingSpoolOutputs.clear();
381
+ this.pendingPageOutArchives = [];
382
+ this.activePageOutArchive = undefined;
323
383
  this.currentSessionId = sessionId;
324
384
  const kernel = await getKernel();
325
385
  const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
@@ -333,7 +393,7 @@ export class RuntimeRunner {
333
393
  maxTurns: effectiveMaxTurns,
334
394
  // M4/G5: per-node token cap → child run's cumulative token budget (wasm LoopPolicy.maxTotalTokens is f64).
335
395
  ...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
336
- timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
396
+ timeoutMs: effectiveTimeoutMs,
337
397
  });
338
398
  this.activeKernel = runtime;
339
399
  if (this.opts.tokenizer) {
@@ -410,7 +470,7 @@ export class RuntimeRunner {
410
470
  if (priorEvents && priorEvents.length > 0) {
411
471
  const repaired = repairEventsForRecovery(priorEvents, maxBytes);
412
472
  seedProviderReplayFromEvents(this.opts.provider, repaired);
413
- const replayed = replayMessages(repaired, maxBytes);
473
+ const replayed = await replayMessages(repaired, maxBytes, this.opts.compressionStore);
414
474
  kernelApply(runtime, this.pendingObservations, {
415
475
  kind: "preload_history",
416
476
  messages: replayed.map(messageToKernelMessage),
@@ -450,6 +510,13 @@ export class RuntimeRunner {
450
510
  }
451
511
  }
452
512
  }
513
+ // Multimodal upload: seed attachments before start_run (parity with Node/Python).
514
+ if (!resumeMidRun && attachments?.length) {
515
+ kernelApply(runtime, this.pendingObservations, {
516
+ kind: "add_history_message",
517
+ message: attachmentsToKernelMessage(attachments),
518
+ });
519
+ }
453
520
  const sessionStart = Date.now();
454
521
  const startPayload = {
455
522
  kind: "start_run",
@@ -490,18 +557,20 @@ export class RuntimeRunner {
490
557
  while (!runtime.isTerminal()) {
491
558
  nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
492
559
  if (this.interrupted) {
493
- action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
560
+ action = kernelAction(runtime, this.pendingObservations, {
561
+ kind: "cancel_operation",
562
+ reason: this.cancellationReason ?? "user",
563
+ pending_call_ids: pendingCallIds(action),
564
+ });
494
565
  break;
495
566
  }
496
567
  if (this.opts.signalSource || this.injectedSignals.length > 0) {
497
- const sig = await this.nextInboundSignal();
498
- if (sig) {
499
- const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
568
+ const delivery = await this.nextInboundSignal();
569
+ if (delivery) {
570
+ const sigAction = await this.consumeInboundSignal(delivery, claimed => kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
500
571
  if (sigAction)
501
572
  action = sigAction;
502
- // I0a: Critical signal carries user_abort intent; see Node runner for full rationale.
503
- if (sig.urgency === "critical")
504
- this.interrupted = true;
573
+ // Critical attention/preemption is distinct from operation cancellation.
505
574
  }
506
575
  }
507
576
  if (runtime.isTerminal())
@@ -513,6 +582,7 @@ export class RuntimeRunner {
513
582
  if (this.pendingAuthoredWorkflows.length > 0) {
514
583
  action = await this.driveAuthoredWorkflows(runtime, action);
515
584
  }
585
+ const providerEffectId = action.effectId;
516
586
  const finalToolCalls = [];
517
587
  let finalText = "";
518
588
  // I5: governance schema-level pre-filter — see Node runner for full rationale.
@@ -574,6 +644,7 @@ export class RuntimeRunner {
574
644
  // interrupt (the post-stream `aborted` check below converts it to a clean
575
645
  // timeout/UserAbort), not a crash or a provider error.
576
646
  this.interrupted = true;
647
+ this.cancellationReason ??= "user";
577
648
  }
578
649
  else {
579
650
  // Reactive recovery is now a kernel decision. Forward the raw provider error and
@@ -583,6 +654,7 @@ export class RuntimeRunner {
583
654
  // duplicated across the four SDK runners.
584
655
  action = kernelAction(runtime, this.pendingObservations, {
585
656
  kind: "provider_error",
657
+ effect_id: providerEffectId,
586
658
  message: formatToolError(err),
587
659
  });
588
660
  // Withholding (query.ts parity): surface the raw provider error only when the kernel
@@ -598,7 +670,11 @@ export class RuntimeRunner {
598
670
  }
599
671
  // #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
600
672
  if (abortSignal?.aborted) {
601
- action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
673
+ action = kernelAction(runtime, this.pendingObservations, {
674
+ kind: "cancel_operation",
675
+ reason: this.cancellationReason ?? "user",
676
+ pending_call_ids: [providerEffectId],
677
+ });
602
678
  break;
603
679
  }
604
680
  const assistantMessage = {
@@ -609,25 +685,14 @@ export class RuntimeRunner {
609
685
  };
610
686
  const providerEvent = {
611
687
  kind: "provider_result",
688
+ effect_id: providerEffectId,
612
689
  message: messageToKernelMessage(assistantMessage),
613
690
  ...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
614
691
  ...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
615
692
  now_ms: Date.now(),
616
693
  ...(turnStopReason ? { stop_reason: turnStopReason } : {}),
617
694
  };
618
- let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
619
- const hasSuspended = this.pendingObservations.some(o => o.kind === "suspended");
620
- if (!nextAction && hasSuspended) {
621
- const resolved = await this.resolveKernelSuspend(runtime, sessionId);
622
- for (const evt of resolved.events)
623
- yield evt;
624
- nextAction = kernelAction(runtime, this.pendingObservations, {
625
- kind: "resume",
626
- approved_calls: resolved.approved,
627
- denied_calls: resolved.denied,
628
- });
629
- }
630
- action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
695
+ action = kernelAction(runtime, this.pendingObservations, providerEvent);
631
696
  const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
632
697
  await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
633
698
  turn: runtime.turn(),
@@ -663,11 +728,117 @@ export class RuntimeRunner {
663
728
  catch { /* malformed skill args — leave activeSkill unchanged */ }
664
729
  }
665
730
  }
731
+ else if (action.kind === "request_approval") {
732
+ const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId);
733
+ for (const evt of resolved.events)
734
+ yield evt;
735
+ action = kernelAction(runtime, this.pendingObservations, {
736
+ kind: "approval_result",
737
+ effect_id: action.effectId,
738
+ approved_calls: resolved.approved,
739
+ denied_calls: resolved.denied,
740
+ });
741
+ }
742
+ else if (action.kind === "persist_memory") {
743
+ let error;
744
+ try {
745
+ if (!this.opts.dreamStore || !this.opts.agentId)
746
+ throw new Error("WASM memory persistence requires dreamStore and agentId");
747
+ await this.opts.dreamStore.upsert(this.opts.agentId, action.memory);
748
+ }
749
+ catch (cause) {
750
+ error = formatToolError(cause);
751
+ }
752
+ action = kernelAction(runtime, this.pendingObservations, {
753
+ kind: "memory_persist_result",
754
+ effect_id: action.effectId,
755
+ ...(error ? { error } : {}),
756
+ });
757
+ }
758
+ else if (action.kind === "query_memory") {
759
+ let hits = [];
760
+ let error;
761
+ try {
762
+ if (!this.opts.dreamStore || !this.opts.agentId)
763
+ throw new Error("WASM memory queries require dreamStore and agentId");
764
+ hits = await this.opts.dreamStore.search(this.opts.agentId, {
765
+ ...action.query, top_k: action.requestedK,
766
+ });
767
+ }
768
+ catch (cause) {
769
+ error = formatToolError(cause);
770
+ }
771
+ const obsStart = this.pendingObservations.length;
772
+ action = kernelAction(runtime, this.pendingObservations, {
773
+ kind: "memory_query_result",
774
+ effect_id: action.effectId,
775
+ hits,
776
+ ...(error ? { error } : {}),
777
+ });
778
+ // M3/M4: mirror the kernel's journaled recall lifecycle + surface promotion suggestions.
779
+ for (const obs of this.pendingObservations.slice(obsStart)) {
780
+ if (obs.kind === "memory_recalled" && obs.recalls?.length && this.opts.agentId) {
781
+ await this.opts.dreamStore?.recordRecall?.(this.opts.agentId, obs.recalls);
782
+ }
783
+ if (obs.kind === "promotion_suggested" && obs.record_id) {
784
+ this.opts.onPromotionSuggested?.({ recordId: obs.record_id, recallCount: obs.recall_count ?? 0 });
785
+ }
786
+ }
787
+ }
788
+ else if (action.kind === "spool_large_result") {
789
+ const spool = this.opts.resultSpool ?? new LargeResultSpool();
790
+ let spoolRef;
791
+ let error;
792
+ try {
793
+ spoolRef = await spool.persistOutput(action.callId, action.output);
794
+ }
795
+ catch (cause) {
796
+ error = formatToolError(cause);
797
+ }
798
+ action = kernelAction(runtime, this.pendingObservations, {
799
+ kind: "large_result_spool_result",
800
+ effect_id: action.effectId,
801
+ ...(spoolRef ? { spool_ref: spoolRef } : {}),
802
+ ...(error ? { error } : {}),
803
+ });
804
+ }
805
+ else if (action.kind === "archive_page_out") {
806
+ const archiveMeta = this.activePageOutArchive
807
+ ?? this.pendingPageOutArchives.shift()
808
+ ?? { archiveStart: this.nextArchiveStart, compressedSeq: await this.opts.sessionLog.latestSeq(sessionId) };
809
+ this.activePageOutArchive = archiveMeta;
810
+ let archiveRef;
811
+ let error;
812
+ try {
813
+ if (this.opts.compressionStore) {
814
+ archiveRef = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived);
815
+ }
816
+ }
817
+ catch (cause) {
818
+ error = formatToolError(cause);
819
+ }
820
+ const archived = action.archived;
821
+ const archiveAction = compressionAction(action.action) ?? "auto_compact";
822
+ const archiveTier = action.tier;
823
+ if (!error)
824
+ this.activePageOutArchive = undefined;
825
+ action = kernelAction(runtime, this.pendingObservations, {
826
+ kind: "page_out_archive_result",
827
+ effect_id: action.effectId,
828
+ ...(archiveRef ? { archive_ref: archiveRef } : {}),
829
+ ...(error ? { error } : {}),
830
+ });
831
+ if (!error && archiveTier === "semantic" && archived.length > 0) {
832
+ void this.archiveSemanticPageOut(archived, archiveAction);
833
+ }
834
+ }
666
835
  else if (action.kind === "execute_tool") {
836
+ const toolEffectId = action.effectId;
667
837
  const allCalls = action.calls;
668
838
  await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
669
839
  const runCtx = {
670
840
  agentId: this.opts.agentId,
841
+ memoryScope: this.opts.memoryScope,
671
842
  skillContentMap: this.opts.skillContentMap,
672
843
  dreamStore: this.opts.dreamStore,
673
844
  knowledgeSource: this.opts.knowledgeSource,
@@ -828,12 +999,6 @@ export class RuntimeRunner {
828
999
  token_count: r.tokenCount,
829
1000
  })),
830
1001
  });
831
- for (const call of allCalls) {
832
- const result = toolResults.find(r => r.callId === call.id);
833
- if (result) {
834
- this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
835
- }
836
- }
837
1002
  // P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
838
1003
  //
839
1004
  // Strict dynamic context control: a skill is METHOD content — how to do something — reused
@@ -869,6 +1034,7 @@ export class RuntimeRunner {
869
1034
  const entropyObsStart = this.pendingObservations.length;
870
1035
  action = kernelAction(runtime, this.pendingObservations, {
871
1036
  kind: "tool_results",
1037
+ effect_id: toolEffectId,
872
1038
  results: toolResults.map(toolResultToKernel),
873
1039
  });
874
1040
  // Surface the boundary's entropy measurement live (the heartbeat watch source).
@@ -901,6 +1067,7 @@ export class RuntimeRunner {
901
1067
  if (milestonePolicy === "auto_pass") {
902
1068
  action = kernelAction(runtime, this.pendingObservations, {
903
1069
  kind: "milestone_result",
1070
+ effect_id: action.effectId,
904
1071
  result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
905
1072
  });
906
1073
  nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
@@ -913,6 +1080,7 @@ export class RuntimeRunner {
913
1080
  });
914
1081
  action = kernelAction(runtime, this.pendingObservations, {
915
1082
  kind: "milestone_result",
1083
+ effect_id: action.effectId,
916
1084
  result: milestoneCheckResultToKernel(check),
917
1085
  });
918
1086
  nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
@@ -961,7 +1129,7 @@ export class RuntimeRunner {
961
1129
  }
962
1130
  const result = action.kind === "done" ? action.result : undefined;
963
1131
  // I0a: preserve preempt intent when loop exits without clean kernel-done (see Node runner for full rationale).
964
- const status = result?.termination ?? (this.interrupted ? "user_abort" : "error");
1132
+ const status = result?.termination ?? "error";
965
1133
  const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
966
1134
  const totalTokens = result?.totalTokensUsed ?? 0;
967
1135
  nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
@@ -979,14 +1147,20 @@ export class RuntimeRunner {
979
1147
  }));
980
1148
  if (newMsgs.length > 0) {
981
1149
  try {
982
- await this.opts.dreamStore.saveSession({
983
- sessionId: crypto.randomUUID(),
1150
+ const completedSession = {
1151
+ sessionId,
984
1152
  agentId: this.opts.agentId,
985
1153
  messages: newMsgs,
986
1154
  metadata: null,
987
1155
  createdAtMs: sessionStart,
988
1156
  updatedAtMs: Date.now(),
989
- });
1157
+ };
1158
+ await this.opts.dreamStore.saveSession(completedSession);
1159
+ if (this.opts.memoryScope) {
1160
+ const extracted = await extractSessionMemories(this.opts.dreamProvider ?? this.opts.provider, completedSession, this.opts.memoryScope, this.opts.dreamSystemPrompt);
1161
+ for (const memory of extracted)
1162
+ await this.writeMemory(memory, sessionId);
1163
+ }
990
1164
  }
991
1165
  catch { /* non-fatal */ }
992
1166
  }
@@ -1108,10 +1282,10 @@ export class RuntimeRunner {
1108
1282
  const schema = node.output_schema;
1109
1283
  if (!schema)
1110
1284
  return orchestrator.run(mkCtx(baseSpec.goal));
1111
- const MAX_ATTEMPTS = 2;
1285
+ const maxAttempts = this.opts.workflowSchemaValidationAttempts ?? 2;
1112
1286
  let last;
1113
1287
  let lastErrors = [];
1114
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
1288
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1115
1289
  const goal = attempt === 1
1116
1290
  ? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
1117
1291
  : `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
@@ -1124,7 +1298,7 @@ export class RuntimeRunner {
1124
1298
  last = result;
1125
1299
  lastErrors = v.errors;
1126
1300
  }
1127
- const reason = `output_schema validation failed after ${MAX_ATTEMPTS} attempts: ${lastErrors.join("; ")}`;
1301
+ const reason = `output_schema validation failed after ${maxAttempts} attempts: ${lastErrors.join("; ")}`;
1128
1302
  const fallback = last;
1129
1303
  return {
1130
1304
  ...fallback,
@@ -1169,17 +1343,49 @@ export class RuntimeRunner {
1169
1343
  */
1170
1344
  applyKernelPolicies(runtime) {
1171
1345
  const osProfile = assertNativeProfile(this.opts.osProfile ?? "native");
1172
- const attentionPolicy = this.opts.attentionPolicy ?? osProfile.attentionPolicy;
1346
+ const signalPolicy = this.opts.signalPolicy ?? osProfile.signalPolicy;
1173
1347
  const governancePolicy = this.opts.governancePolicy ?? osProfile.governancePolicy;
1174
1348
  // K2: lower governance / attention / scheduler / quota in ONE `configure_run` event (the 0.2.30
1175
1349
  // core applies each present field via its granular path). `set_memory_policy` stays separate below.
1176
1350
  const { kind: _govKind, ...governance } = governancePolicyToKernelEvent(governancePolicy);
1177
1351
  const config = { governance };
1178
- if (attentionPolicy.maxQueueSize !== undefined) {
1179
- config.attention_max_queue_size = attentionPolicy.maxQueueSize;
1352
+ if (this.opts.contextPolicy) {
1353
+ config.context_policy = normalizeContextPolicyV1(contextPolicyV1(this.opts.contextPolicy));
1354
+ }
1355
+ config.signal_policy = {
1356
+ version: 1,
1357
+ queue_max: signalPolicy.queueMax,
1358
+ ...(signalPolicy.ttlMs !== undefined ? { ttl_ms: signalPolicy.ttlMs } : {}),
1359
+ ...(signalPolicy.deadlineEscalation !== undefined
1360
+ ? { deadline_escalation: signalPolicy.deadlineEscalation }
1361
+ : {}),
1362
+ };
1363
+ if (this.opts.promptBudget) {
1364
+ config.prompt_budget = {
1365
+ prompt_overhead_tokens: this.opts.promptBudget.promptOverheadTokens,
1366
+ output_reserve_tokens: this.opts.promptBudget.outputReserveTokens,
1367
+ safety_margin_tokens: this.opts.promptBudget.safetyMarginTokens,
1368
+ };
1180
1369
  }
1181
- if (this.opts.schedulerBudget?.maxWallMs !== undefined) {
1182
- config.scheduler_max_wall_ms = this.opts.schedulerBudget.maxWallMs;
1370
+ if (this.opts.schedulerPolicy) {
1371
+ config.scheduler_policy = schedulerPolicyToKernel(this.opts.schedulerPolicy);
1372
+ }
1373
+ if (this.opts.kernelReliability) {
1374
+ const r = this.opts.kernelReliability;
1375
+ config.reliability = {
1376
+ ...(r.eventReplayCapacity !== undefined ? { event_replay_capacity: r.eventReplayCapacity } : {}),
1377
+ ...(r.completedEffectReplayCapacity !== undefined ? { completed_effect_replay_capacity: r.completedEffectReplayCapacity } : {}),
1378
+ ...(r.providerRecoveryAttempts !== undefined ? { provider_recovery_attempts: r.providerRecoveryAttempts } : {}),
1379
+ ...(r.outputRecoveryAttempts !== undefined ? { output_recovery_attempts: r.outputRecoveryAttempts } : {}),
1380
+ ...(r.hostEffectRetryAttempts !== undefined ? { host_effect_retry_attempts: r.hostEffectRetryAttempts } : {}),
1381
+ ...(r.spoolThresholdBytes !== undefined ? { spool_threshold_bytes: r.spoolThresholdBytes } : {}),
1382
+ ...(r.spoolPreviewBytes !== undefined ? { spool_preview_bytes: r.spoolPreviewBytes } : {}),
1383
+ ...(r.snapshotInputLimit !== undefined ? { snapshot_input_limit: r.snapshotInputLimit } : {}),
1384
+ ...(r.maxInputBytes !== undefined ? { max_input_bytes: r.maxInputBytes } : {}),
1385
+ ...(r.snapshotJournalBytesLimit !== undefined
1386
+ ? { snapshot_journal_bytes_limit: r.snapshotJournalBytesLimit }
1387
+ : {}),
1388
+ };
1183
1389
  }
1184
1390
  if (this.opts.resourceQuota) {
1185
1391
  const q = this.opts.resourceQuota;
@@ -1239,15 +1445,17 @@ export class RuntimeRunner {
1239
1445
  */
1240
1446
  async bootstrapWorkflowKernel(sessionId, spec) {
1241
1447
  this.interrupted = false;
1448
+ this.cancellationReason = undefined;
1242
1449
  this.pendingObservations = [];
1243
- this.pendingSpoolOutputs.clear();
1450
+ this.pendingPageOutArchives = [];
1451
+ this.activePageOutArchive = undefined;
1244
1452
  this.currentSessionId = sessionId;
1245
1453
  const kernel = await getKernel();
1246
1454
  const runtime = new kernel.KernelRuntime({
1247
1455
  maxTokens: this.opts.maxTokens,
1248
1456
  maxTurns: this.opts.maxTurns ?? 25,
1249
1457
  ...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
1250
- timeoutMs: this.opts.timeoutMs !== undefined ? BigInt(this.opts.timeoutMs) : undefined,
1458
+ timeoutMs: this.opts.timeoutMs,
1251
1459
  });
1252
1460
  this.activeKernel = runtime;
1253
1461
  const goal = `workflow:${spec.nodes.length} nodes`;
@@ -1259,9 +1467,10 @@ export class RuntimeRunner {
1259
1467
  ...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
1260
1468
  })).catch(() => { });
1261
1469
  this.applyKernelPolicies(runtime);
1262
- // K1: no explicit `start_run` — the host `load_workflow` (fired next by `runWorkflow`) self-bootstraps
1263
- // the run on the 0.2.30 core, matching the agent-reachable `submit_workflow` path.
1264
- void goal;
1470
+ kernelAction(runtime, this.pendingObservations, {
1471
+ kind: "start_run",
1472
+ task: { goal: `workflow session ${sessionId}`, criteria: [] },
1473
+ });
1265
1474
  return runtime;
1266
1475
  }
1267
1476
  async runWorkflow(spec, opts) {
@@ -1275,17 +1484,19 @@ export class RuntimeRunner {
1275
1484
  const parentSessionId = this.currentSessionId;
1276
1485
  const runtime = this.activeKernel;
1277
1486
  try {
1278
- const observations = kernelApply(runtime, this.pendingObservations, {
1487
+ const observationStart = this.pendingObservations.length;
1488
+ const initialAction = kernelMaybeAction(runtime, this.pendingObservations, {
1279
1489
  kind: "load_workflow",
1280
1490
  spec: workflowSpecToKernel(spec),
1281
1491
  parent_session_id: parentSessionId,
1282
- // W0-ABI resume: skip nodes already completed before an interruption.
1283
- ...(opts?.resumedCompleted && opts.resumedCompleted.length ? { resumed_completed: opts.resumedCompleted } : {}),
1284
- // W-1: signal-carrying completion records (classify branch / loop stop replay).
1285
- ...(opts?.resumedResults?.length
1492
+ // Exact typed terminal outcomes plus control-flow signals recovered from the journal.
1493
+ ...(opts?.resumedOutcomes?.length
1286
1494
  ? {
1287
- resumed_results: opts.resumedResults.map(r => ({
1495
+ resumed_outcomes: opts.resumedOutcomes.map(r => ({
1288
1496
  agent_id: r.agentId,
1497
+ status: r.status,
1498
+ termination: r.termination,
1499
+ ...(r.output ? { output: messageToKernelMessage(r.output) } : {}),
1289
1500
  ...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
1290
1501
  ...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
1291
1502
  ...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
@@ -1296,7 +1507,8 @@ export class RuntimeRunner {
1296
1507
  ...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
1297
1508
  ...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
1298
1509
  });
1299
- return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
1510
+ const observations = this.pendingObservations.slice(observationStart);
1511
+ return await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, recoveredOutputs(opts?.resumedOutcomes));
1300
1512
  }
1301
1513
  finally {
1302
1514
  if (bootstrapped) {
@@ -1318,7 +1530,9 @@ export class RuntimeRunner {
1318
1530
  }
1319
1531
  const parentSessionId = this.currentSessionId;
1320
1532
  const runtime = this.activeKernel;
1321
- const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
1533
+ const observationStart = this.pendingObservations.length;
1534
+ const initialAction = kernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
1535
+ const observations = this.pendingObservations.slice(observationStart);
1322
1536
  // W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
1323
1537
  // announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
1324
1538
  // had this spec, unlike the `runWorkflow` path.
@@ -1331,7 +1545,7 @@ export class RuntimeRunner {
1331
1545
  submitterAgentId: opts?.submitterAgentId,
1332
1546
  }));
1333
1547
  }
1334
- return this.driveWorkflow(observations, parentSessionId, runtime);
1548
+ return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
1335
1549
  }
1336
1550
  /**
1337
1551
  * M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
@@ -1343,14 +1557,15 @@ export class RuntimeRunner {
1343
1557
  async driveAuthoredWorkflows(runtime, action) {
1344
1558
  const specs = this.pendingAuthoredWorkflows;
1345
1559
  this.pendingAuthoredWorkflows = [];
1560
+ this.workflowContinuation = null;
1346
1561
  for (const spec of specs) {
1347
- const outcome = await this.bootstrapWorkflow(spec);
1348
- kernelApply(runtime, this.pendingObservations, {
1349
- kind: "add_history_message",
1350
- message: messageToKernelMessage({ role: "user", content: authoredWorkflowOutcomeNote(outcome) }),
1351
- });
1562
+ await this.bootstrapWorkflow(spec);
1563
+ }
1564
+ const continuation = this.workflowContinuation;
1565
+ if (!continuation) {
1566
+ throw new Error("authored workflow completed without a provider continuation");
1352
1567
  }
1353
- return { kind: "call_provider", context: runtime.render(), tools: action.tools };
1568
+ return continuation;
1354
1569
  }
1355
1570
  /**
1356
1571
  * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
@@ -1360,24 +1575,39 @@ export class RuntimeRunner {
1360
1575
  */
1361
1576
  async monitorWorkflowPreemption(runtime, controllers, batchState) {
1362
1577
  const source = this.opts.signalSource;
1363
- if (!source)
1578
+ if (!source && this.injectedSignals.length === 0)
1364
1579
  return null;
1365
1580
  while (!batchState.settled) {
1366
1581
  // O2: injected notes participate in the monitor too (drain order matches nextInboundSignal).
1367
- const sig = this.injectedSignals.shift() ?? await source.nextSignal();
1582
+ const delivery = await this.nextInboundSignal();
1368
1583
  if (batchState.settled)
1369
1584
  break;
1370
- if (!sig) {
1585
+ if (!delivery) {
1371
1586
  await new Promise(resolve => setTimeout(resolve, 5));
1372
1587
  continue;
1373
1588
  }
1374
- const obs = kernelApply(runtime, this.pendingObservations, signalToKernelEvent(sig));
1589
+ const observationStart = this.pendingObservations.length;
1590
+ const signalAction = await this.consumeInboundSignal(delivery, claimed => kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
1591
+ if (signalAction) {
1592
+ if (signalAction.kind !== "preempt_sub_agents") {
1593
+ throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
1594
+ }
1595
+ for (const id of signalAction.agentIds)
1596
+ controllers.get(id)?.abort();
1597
+ const continuation = kernelMaybeAction(runtime, this.pendingObservations, {
1598
+ kind: "preempt_result", effect_id: signalAction.effectId,
1599
+ });
1600
+ if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
1601
+ throw new Error(`workflow preemption returned unexpected effect: ${continuation.kind}`);
1602
+ }
1603
+ }
1604
+ const obs = this.pendingObservations.slice(observationStart);
1375
1605
  const preempted = obs.find(o => o.kind === "agent_preempted");
1376
1606
  if (preempted) {
1377
1607
  for (const id of preempted.agent_ids ?? [])
1378
1608
  controllers.get(id)?.abort();
1379
1609
  const wc = obs.find(o => o.kind === "workflow_completed");
1380
- return { completed: wc?.completed ?? [], failed: wc?.failed ?? [] };
1610
+ return (wc?.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
1381
1611
  }
1382
1612
  }
1383
1613
  return null;
@@ -1387,25 +1617,43 @@ export class RuntimeRunner {
1387
1617
  * `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
1388
1618
  * agent-submitted nodes first), and loop until the kernel reports the workflow complete.
1389
1619
  */
1390
- async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
1620
+ async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
1391
1621
  const observations = initial;
1392
1622
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
1393
- const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
1394
- // G4: the batch observation carries the workflow's remaining budget; track the latest.
1395
- const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
1396
1623
  const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
1624
+ const acceptSpawn = (spawn) => {
1625
+ const observationStart = this.pendingObservations.length;
1626
+ const continuation = kernelMaybeAction(runtime, this.pendingObservations, {
1627
+ kind: "workflow_spawn_result",
1628
+ effect_id: spawn.effectId,
1629
+ started_agent_ids: spawn.nodes.map(node => String(node.agent_id ?? "")),
1630
+ failures: [],
1631
+ });
1632
+ if (continuation)
1633
+ throw new Error(`workflow spawn acknowledgement returned unexpected effect: ${continuation.kind}`);
1634
+ return this.pendingObservations.slice(observationStart);
1635
+ };
1397
1636
  let done = findDone(observations);
1398
- if (done)
1399
- return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: {} };
1400
- let nodes = collectNodes(observations);
1401
- let budget = collectBudget(observations);
1637
+ if (done) {
1638
+ if (initialAction?.kind === "call_provider")
1639
+ this.workflowContinuation = initialAction;
1640
+ return { nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel), outputs: {} };
1641
+ }
1642
+ if (!initialAction)
1643
+ return { nodeOutcomes: [], outputs: {} };
1644
+ if (initialAction.kind !== "spawn_workflow") {
1645
+ throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
1646
+ }
1647
+ let nodes = initialAction.nodes;
1648
+ let budget = initialAction.budget;
1649
+ acceptSpawn(initialAction);
1402
1650
  // G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
1403
1651
  // W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
1404
1652
  // still see their (pre-crash) dependencies' outputs.
1405
1653
  const outputs = new Map(seedOutputs ?? []);
1406
1654
  for (;;) {
1407
1655
  if (nodes.length === 0)
1408
- return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
1656
+ return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) };
1409
1657
  const roundBudget = budget;
1410
1658
  // #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
1411
1659
  const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
@@ -1415,7 +1663,7 @@ export class RuntimeRunner {
1415
1663
  batchState.settled = true;
1416
1664
  const preempted = await monitor;
1417
1665
  if (preempted)
1418
- return { ...preempted, outputs: Object.fromEntries(outputs) };
1666
+ return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
1419
1667
  // Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
1420
1668
  const nextNodes = [];
1421
1669
  done = undefined;
@@ -1436,26 +1684,47 @@ export class RuntimeRunner {
1436
1684
  // G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
1437
1685
  // nodes to quarantined (no topological privilege escalation).
1438
1686
  const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
1439
- const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
1440
- nextNodes.push(...collectNodes(subObs));
1441
- budget = collectBudget(subObs) ?? budget;
1687
+ const observationStart = this.pendingObservations.length;
1688
+ const submitAction = kernelMaybeAction(runtime, this.pendingObservations, submitEvent);
1689
+ const subObs = this.pendingObservations.slice(observationStart);
1690
+ if (submitAction?.kind === "spawn_workflow") {
1691
+ nextNodes.push(...submitAction.nodes);
1692
+ budget = submitAction.budget ?? budget;
1693
+ acceptSpawn(submitAction);
1694
+ }
1695
+ else if (submitAction) {
1696
+ throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
1697
+ }
1442
1698
  // R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
1443
1699
  // so resume can re-apply the batch at the exact original graph position. W-N3: also the
1444
1700
  // submitter, so resume drops batches whose submitter re-runs (it will re-submit).
1445
1701
  const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
1446
- await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1447
- turn: runtime.turn(),
1448
- nodes: submitEvent.nodes ?? [],
1449
- baseIndex: submitted?.base,
1450
- submitterAgentId: result.agentId,
1451
- }));
1702
+ if (submitted) {
1703
+ await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1704
+ turn: runtime.turn(),
1705
+ nodes: submitEvent.nodes ?? [],
1706
+ baseIndex: submitted.base,
1707
+ submitterAgentId: result.agentId,
1708
+ }));
1709
+ }
1452
1710
  }
1453
- const obs = kernelApply(runtime, this.pendingObservations, {
1711
+ const observationStart = this.pendingObservations.length;
1712
+ const completionAction = kernelMaybeAction(runtime, this.pendingObservations, {
1454
1713
  kind: "sub_agent_completed",
1455
1714
  result: subAgentResultToKernel(result),
1456
1715
  });
1457
- nextNodes.push(...collectNodes(obs));
1458
- budget = collectBudget(obs) ?? budget;
1716
+ let obs = this.pendingObservations.slice(observationStart);
1717
+ if (completionAction?.kind === "spawn_workflow") {
1718
+ nextNodes.push(...completionAction.nodes);
1719
+ budget = completionAction.budget ?? budget;
1720
+ obs = [...obs, ...acceptSpawn(completionAction)];
1721
+ }
1722
+ else if (completionAction?.kind === "call_provider") {
1723
+ this.workflowContinuation = completionAction;
1724
+ }
1725
+ else if (completionAction) {
1726
+ throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
1727
+ }
1459
1728
  const d = findDone(obs);
1460
1729
  if (d)
1461
1730
  done = d;
@@ -1465,15 +1734,19 @@ export class RuntimeRunner {
1465
1734
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
1466
1735
  turn: runtime.turn(),
1467
1736
  agentId: result.agentId,
1737
+ status: workflowNodeStatusFromTermination(result.result.termination),
1468
1738
  termination: result.result.termination,
1469
1739
  classifyBranch: result.result.classifyBranch,
1470
1740
  tournamentWinner: result.result.tournamentWinner,
1471
1741
  loopContinue: result.result.loopContinue,
1472
- ...(result.result.termination === "completed" && outText ? { output: outText } : {}),
1742
+ ...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
1473
1743
  }));
1474
1744
  }
1475
1745
  if (done && nextNodes.length === 0) {
1476
- return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: Object.fromEntries(outputs) };
1746
+ return {
1747
+ nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel),
1748
+ outputs: Object.fromEntries(outputs),
1749
+ };
1477
1750
  }
1478
1751
  nodes = nextNodes;
1479
1752
  }
@@ -1491,32 +1764,22 @@ export class RuntimeRunner {
1491
1764
  throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
1492
1765
  }
1493
1766
  const events = await this.opts.sessionLog.read(sessionId);
1494
- const resumedResults = recoverCompletedWorkflowNodes(events);
1495
- const completedIds = new Set(resumedResults.map(r => r.agentId));
1767
+ const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
1768
+ const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
1496
1769
  const recovered = recoverSubmittedWorkflowNodes(events);
1497
1770
  // W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
1498
1771
  // re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
1499
- // Only safe with exact bases (the dropped batch's slots become inert placeholders); a legacy
1500
- // order-only log keeps every batch, since dropping would shift all later indices.
1772
+ // Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
1501
1773
  let { submissions, bases } = recovered;
1502
- if (bases.length === submissions.length && submissions.length > 0) {
1774
+ if (submissions.length > 0) {
1503
1775
  const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
1504
1776
  submissions = submissions.filter((_, i) => keep[i]);
1505
1777
  bases = bases.filter((_, i) => keep[i]);
1506
1778
  }
1507
- const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
1508
- // Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
1509
- // `wf-node{N}`, not `wf-node{N}-i{k}`.
1510
- for (const r of resumedResults) {
1511
- const stableId = r.agentId.replace(/-i\d+$/, "");
1512
- if (stableId !== r.agentId && r.output)
1513
- resumedOutputs.set(stableId, r.output);
1514
- }
1515
1779
  return this.runWorkflow(spec, {
1516
- resumedResults,
1780
+ resumedOutcomes,
1517
1781
  resumedSubmissions: submissions,
1518
1782
  resumedSubmissionBases: bases,
1519
- resumedOutputs,
1520
1783
  sessionId,
1521
1784
  });
1522
1785
  }
@@ -1524,56 +1787,24 @@ export class RuntimeRunner {
1524
1787
  const turn = runtime.turn();
1525
1788
  const preservedRefs = runtime.preservedRefs();
1526
1789
  const observations = this.pendingObservations.splice(0);
1527
- for (let obs of observations) {
1790
+ for (const obs of observations) {
1528
1791
  if (obs.kind === "page_in_requested")
1529
1792
  continue;
1530
- let spoolRef;
1531
- if (obs.kind === "large_result_spooled") {
1532
- const pending = this.pendingSpoolOutputs.get(obs.call_id ?? "");
1533
- if (pending) {
1534
- const spool = this.opts.resultSpool ?? new LargeResultSpool();
1535
- try {
1536
- spoolRef = await spool.persistOutput(obs.call_id ?? "", pending.output);
1537
- }
1538
- catch {
1539
- // non-fatal
1540
- }
1541
- if (!obs.tool && pending.tool) {
1542
- obs = { ...obs, tool: pending.tool };
1543
- }
1544
- this.pendingSpoolOutputs.delete(obs.call_id ?? "");
1545
- }
1546
- }
1547
1793
  const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
1548
1794
  const event = kernelObservationToSessionEvent(obs, turn, {
1549
1795
  nextArchiveStart,
1550
1796
  latestSeq: latest,
1551
1797
  preservedRefs,
1552
- spoolRef,
1553
1798
  compressionAction,
1554
1799
  });
1555
1800
  if (!event)
1556
1801
  continue;
1557
1802
  const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
1558
1803
  if (event.kind === "compressed") {
1559
- nextArchiveStart = compressedSeq + 1;
1560
- // One compaction = one kernel observation: the page_out session record (and the
1561
- // semantic-archive branch) is DERIVED from Compressed.tier_hint, preserving the
1562
- // session-log format and OsSnapshot page_out_count.
1563
- const archived = obs.archived;
1564
- if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
1565
- await this.opts.sessionLog.append(sessionId, {
1566
- kind: "page_out",
1567
- turn: obs.turn ?? turn,
1568
- action: compressionAction(obs.action),
1569
- summary: obs.summary,
1570
- tier_hint: obs.tier_hint ?? "durable",
1571
- message_count: archived.length,
1572
- });
1573
- if (obs.tier_hint === "semantic") {
1574
- void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
1575
- }
1804
+ if ((obs.archived_count ?? 0) > 0) {
1805
+ this.pendingPageOutArchives.push({ archiveStart: nextArchiveStart, compressedSeq });
1576
1806
  }
1807
+ nextArchiveStart = compressedSeq + 1;
1577
1808
  }
1578
1809
  // K4: a sprint renewal dropped the old history — including any earlier memory hits — so
1579
1810
  // re-run the preQueryMemory prefetch for the new sprint (live observations only).
@@ -1589,21 +1820,21 @@ export class RuntimeRunner {
1589
1820
  * re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
1590
1821
  * hits). Errs-open throughout. */
1591
1822
  async prefetchMemoryIntoHistory(runtime, phase) {
1592
- if (!this.opts.dreamStore || !this.opts.agentId)
1823
+ if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
1593
1824
  return;
1594
1825
  // P10: recall is default-on (CC session-start recall) — with no hook configured,
1595
1826
  // the goal itself is the query. preQueryMemory stays as the targeting override.
1596
1827
  const preQuery = this.opts.preQueryMemory
1597
- ?? ((ctx) => [ctx.goal]);
1828
+ ?? ((ctx) => [{ scope: this.opts.memoryScope, query: ctx.goal, top_k: 5, kinds: [] }]);
1598
1829
  try {
1599
1830
  const queries = await preQuery({ goal: this.currentGoal, phase });
1600
1831
  const lines = [];
1601
1832
  for (const q of queries ?? []) {
1602
- if (typeof q !== "string" || !q.trim())
1833
+ if (!q.query.trim())
1603
1834
  continue;
1604
- const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
1835
+ const hits = await this.opts.dreamStore.search(this.opts.agentId, q);
1605
1836
  for (const hit of hits) {
1606
- lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.text}`);
1837
+ lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
1607
1838
  }
1608
1839
  }
1609
1840
  if (lines.length > 0) {
@@ -1616,46 +1847,27 @@ export class RuntimeRunner {
1616
1847
  catch { /* errs-open */ }
1617
1848
  }
1618
1849
  async archiveSemanticPageOut(archived, action) {
1619
- if (!this.opts.dreamStore || !this.opts.agentId)
1850
+ if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
1620
1851
  return;
1621
1852
  try {
1622
1853
  const summary = this.opts.dreamSummarizer
1623
1854
  ? await this.opts.dreamSummarizer.summarize(archived, { action })
1624
1855
  : await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
1625
- const existing = await this.opts.dreamStore.loadMemories(this.opts.agentId);
1626
- // P2 write-funnel (wasm surface has no kernel write gate yet): jaccard dedup +
1627
- // advisory 0.6 score — an automatic summary must never outrank curated content.
1628
- if (existing.some(e => jaccardSimilarity(e.text, summary) >= 0.9))
1629
- return;
1630
- await this.opts.dreamStore.commit(this.opts.agentId, {
1631
- toAdd: [{ text: summary, score: 0.6, metadata: { source: "semantic_page_out", action } }],
1632
- toRemoveIndices: [],
1633
- stats: {
1634
- insightsProcessed: 1,
1635
- duplicatesRemoved: 0,
1636
- conflictsResolved: 0,
1637
- entriesAdded: 1,
1638
- },
1639
- }, existing);
1856
+ const now = Date.now();
1857
+ const name = `page-out-${now}`;
1858
+ await this.writeMemory({
1859
+ record_id: `${this.opts.memoryScope.tenant_id}:${this.opts.memoryScope.namespace}:project:${name}`,
1860
+ scope: this.opts.memoryScope, name, kind: "project", content: summary,
1861
+ description: `auto summary of ${action ?? "compaction"} archive`,
1862
+ provenance: { author: "extraction", trust: "untrusted", evidence_refs: [] },
1863
+ created_at: now, updated_at: now, recall_count: 0, confidence: 0.6, links: [], pinned: false,
1864
+ }, this.currentSessionId ?? undefined);
1640
1865
  }
1641
1866
  catch {
1642
1867
  // non-fatal
1643
1868
  }
1644
1869
  }
1645
1870
  }
1646
- /** Word-set jaccard similarity — the curator's dedup rule at the write funnel. */
1647
- function jaccardSimilarity(a, b) {
1648
- const sa = new Set(a.split(/\s+/).filter(Boolean));
1649
- const sb = new Set(b.split(/\s+/).filter(Boolean));
1650
- if (sa.size === 0 && sb.size === 0)
1651
- return 1;
1652
- let inter = 0;
1653
- for (const w of sa)
1654
- if (sb.has(w))
1655
- inter++;
1656
- const union = sa.size + sb.size - inter;
1657
- return union === 0 ? 0 : inter / union;
1658
- }
1659
1871
  async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
1660
1872
  const transcript = archived
1661
1873
  .map(m => `${m.role}: ${m.content}`)
@@ -1687,7 +1899,7 @@ function compressionAction(action) {
1687
1899
  }
1688
1900
  return undefined;
1689
1901
  }
1690
- function replayMessages(events, maxBytes) {
1902
+ async function replayMessages(events, maxBytes, archiveStore) {
1691
1903
  // Build upgraded-summary index: compressed_seq -> upgraded summary
1692
1904
  const upgradedSummaries = new Map();
1693
1905
  for (const { event: e } of events) {
@@ -1695,19 +1907,29 @@ function replayMessages(events, maxBytes) {
1695
1907
  upgradedSummaries.set(e.compressed_seq, e.summary);
1696
1908
  }
1697
1909
  const messages = [];
1910
+ const archivedTurns = new Set(events.flatMap(({ event }) => event.kind === "page_out" && event.archive_ref && archiveStore?.read ? [event.turn] : []));
1698
1911
  for (const { seq, event: e } of events) {
1699
1912
  if (e.kind === "run_started") {
1700
1913
  const userText = e.criteria.length
1701
1914
  ? `${e.goal}\n\nCriteria:\n${e.criteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`
1702
1915
  : e.goal;
1916
+ // Multimodal parity: the live seed of `attachments` is gated behind `!resumeMidRun`, so on
1917
+ // resume the image/audio must be recovered from the persisted run_started event or it is lost.
1918
+ const attachments = (e.attachments ?? []);
1919
+ const contentParts = attachments.length
1920
+ ? [...(userText ? [{ type: "text", text: userText }] : []), ...attachments]
1921
+ : undefined;
1703
1922
  messages.push({
1704
1923
  role: "user",
1705
1924
  content: userText,
1925
+ ...(contentParts ? { contentParts } : {}),
1706
1926
  toolCalls: [],
1707
1927
  tokenCount: Math.max(1, Math.ceil(userText.length / 4)),
1708
1928
  });
1709
1929
  }
1710
1930
  else if (e.kind === "compressed") {
1931
+ if (archivedTurns.has(e.turn))
1932
+ continue;
1711
1933
  const summary = upgradedSummaries.get(seq) ?? e.summary;
1712
1934
  if (summary) {
1713
1935
  const systemText = `[Compressed context: turn ${e.turn}]\n${summary}`;
@@ -1719,6 +1941,24 @@ function replayMessages(events, maxBytes) {
1719
1941
  });
1720
1942
  }
1721
1943
  }
1944
+ else if (e.kind === "page_out" && e.archive_ref && archiveStore?.read) {
1945
+ try {
1946
+ const archived = await archiveStore.read(e.archive_ref);
1947
+ messages.push(...archived.map(message => ({
1948
+ ...message,
1949
+ content: sanitizeReplayText(message.content, maxBytes),
1950
+ })));
1951
+ }
1952
+ catch {
1953
+ if (e.summary) {
1954
+ const systemText = `[Compressed context: turn ${e.turn}]\n${e.summary}`;
1955
+ messages.push({
1956
+ role: "system", content: systemText, toolCalls: [],
1957
+ tokenCount: Math.max(1, Math.ceil(systemText.length / 4)),
1958
+ });
1959
+ }
1960
+ }
1961
+ }
1722
1962
  else if (e.kind === "llm_completed") {
1723
1963
  messages.push({
1724
1964
  role: "assistant",
@@ -1754,14 +1994,6 @@ function nextArchivedSeqStart(events) {
1754
1994
  }
1755
1995
  return next;
1756
1996
  }
1757
- function tryParseJson(s) {
1758
- try {
1759
- return JSON.parse(s);
1760
- }
1761
- catch {
1762
- return null;
1763
- }
1764
- }
1765
1997
  export async function collectText(stream) {
1766
1998
  let text = "";
1767
1999
  for await (const evt of stream) {
@@ -1811,32 +2043,72 @@ function parseStartWorkflowSpec(argsStr) {
1811
2043
  }
1812
2044
  /** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
1813
2045
  * agent's context, so its next turn continues with the sub-workflow's results in view. */
2046
+ function recoveredOutputs(outcomes) {
2047
+ const outputs = new Map();
2048
+ for (const outcome of outcomes ?? []) {
2049
+ if (!outcome.output)
2050
+ continue;
2051
+ outputs.set(outcome.agentId, outcome.output.content);
2052
+ outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
2053
+ }
2054
+ return outputs;
2055
+ }
1814
2056
  function authoredWorkflowOutcomeNote(outcome) {
2057
+ const counts = new Map();
2058
+ for (const node of outcome.nodeOutcomes)
2059
+ counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
1815
2060
  const lines = [
1816
- `[authored workflow result] ${outcome.completed.length} node(s) completed` +
1817
- (outcome.failed.length ? `, ${outcome.failed.length} failed` : "") + ".",
2061
+ `[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
2062
+ [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
1818
2063
  ];
1819
- for (const id of outcome.completed) {
1820
- const out = outcome.outputs[id];
2064
+ for (const node of outcome.nodeOutcomes) {
2065
+ const out = outcome.outputs[node.nodeId] ?? node.output?.content;
1821
2066
  if (out)
1822
- lines.push(`- ${id}: ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
2067
+ lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
1823
2068
  }
1824
2069
  return lines.join("\n");
1825
2070
  }
1826
- /** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
2071
+ /** Lower a claimed signal delivery to the kernel's `deliver_signal` input event. Shared by the main
1827
2072
  * loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
1828
- function signalToKernelEvent(sig) {
2073
+ function signalToKernelEvent(delivery) {
2074
+ const sig = delivery.signal;
1829
2075
  return {
1830
- kind: "signal",
2076
+ kind: "deliver_signal",
2077
+ delivery_id: delivery.deliveryId,
2078
+ attempt: delivery.deliveryAttempt,
1831
2079
  signal: {
1832
- id: crypto.randomUUID(),
2080
+ id: delivery.signalId,
1833
2081
  source: sig.source ?? "custom",
1834
2082
  signal_type: sig.signalType ?? "event",
1835
2083
  urgency: sig.urgency ?? "normal",
1836
2084
  summary: String(sig.payload?.goal ?? "signal"),
1837
2085
  payload: sig.payload ?? {},
1838
2086
  ...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
2087
+ ...(sig.recipient ? { recipient: sig.recipient } : {}),
2088
+ ...(sig.deadlineMs !== undefined ? { deadline_ms: sig.deadlineMs } : {}),
2089
+ ...(sig.coalesceKey ? { coalesce_key: sig.coalesceKey } : {}),
2090
+ coalesced_count: Math.max(1, sig.coalescedCount ?? 1),
1839
2091
  timestamp_ms: Date.now(),
1840
2092
  },
1841
2093
  };
1842
2094
  }
2095
+ /** Convert SDK ContentParts (camelCase mediaType) to kernel serde shape (media_type). */
2096
+ function attachmentsToKernelMessage(parts) {
2097
+ const content = parts.map(p => {
2098
+ if (p.type === "image") {
2099
+ return {
2100
+ type: "image",
2101
+ ...(p.url ? { url: p.url } : {}),
2102
+ ...(p.data ? { data: p.data } : {}),
2103
+ ...(p.mediaType ? { media_type: p.mediaType } : {}),
2104
+ ...(p.detail ? { detail: p.detail } : {}),
2105
+ };
2106
+ }
2107
+ if (p.type === "audio")
2108
+ return { type: "audio", data: p.data, media_type: p.mediaType };
2109
+ if (p.type === "text")
2110
+ return { type: "text", text: p.text };
2111
+ return { type: "text", text: "" };
2112
+ });
2113
+ return { role: "user", content };
2114
+ }