@deepstrike/sdk 0.2.41 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -245,7 +245,7 @@ A node's `kind` selects the control-flow shape; the same executor drives them al
245
245
 
246
246
  ### 0.2.11 capabilities
247
247
 
248
- - **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`.
248
+ - **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`. Governance rejection fails the submitting node instead of acknowledging work that was never appended.
249
249
  - **Quarantine, no escape** — set `trust: "quarantined"` on a node that reads untrusted content; it's denied write-capable isolation in-kernel, and any nodes it submits are coerced to quarantined too (no privilege escalation).
250
250
  - **Structured output** — set `outputSchema` on a node; the runner instructs the agent, validates the result against the JSON-Schema subset, and re-runs once with the errors on mismatch. A node that never conforms fails (its dependents starve).
251
251
  - **Budget as signal** — with a `maxWorkflowNodes` / `maxConcurrentSubagents` quota installed, each spawned node's goal carries its remaining headroom so a coordinator can size its fan-out to fit.
@@ -719,7 +719,7 @@ for await (const evt of runner.spawnSubAgent({
719
719
  }
720
720
  ```
721
721
 
722
- Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back.
722
+ Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back. If governance rejects the spawn before execution, the iterator yields one `error` event containing the denial reason; the parent turn is not rolled back.
723
723
 
724
724
  ---
725
725
 
@@ -30,11 +30,10 @@ export interface GovernancePolicy {
30
30
  }[];
31
31
  constraints?: GovernanceConstraint[];
32
32
  /** I5: when true (default), the runner pre-filters denied tools out of the schema passed to the
33
- * provider — the model never sees them and never tries to call them, eliminating the rollback
34
- * turn the kernel would otherwise produce. The denied tool names are also surfaced as a single
35
- * line on the system slot so the model knows not to plan around them. Set to false to fall
36
- * back to the v0.2.22 rollback-based behavior (useful for measuring the delta or when the
37
- * agent should learn the denial via a real attempt). */
33
+ * provider — the model never sees them and never tries to call them. The denied tool names are
34
+ * also surfaced as a single line on the system slot so the model knows not to plan around them.
35
+ * Set to false when the agent should learn the denial through a real attempted call and its
36
+ * visible error tool result. */
38
37
  surfaceDeniedInSystem?: boolean;
39
38
  }
40
39
  /** I5: walk the tool list and bucket each tool into `allowed` / `denied` based on a declarative
@@ -1,5 +1,6 @@
1
1
  import type { RuntimeRunner } from "../runtime/runner.js";
2
2
  import type { SessionEvent } from "../runtime/session-log.js";
3
+ import type { ContentPart } from "../types.js";
3
4
  import type { WorkflowNodeSpec } from "../types/agent.js";
4
5
  import type { Criterion, Verdict } from "../runtime/eval.js";
5
6
  import type { AttemptJudge, JudgeResult } from "./judge.js";
@@ -8,6 +9,12 @@ export interface AttemptRequest {
8
9
  sessionId?: string;
9
10
  goal: string;
10
11
  criteria?: Criterion[];
12
+ /**
13
+ * Multimodal inputs (images / audio) attached to the task. Forwarded to every attempt
14
+ * unconditionally; the runner seeds them per session idempotently, so fresh-session carries
15
+ * re-seed while same-session carries do not double.
16
+ */
17
+ attachments?: ContentPart[];
11
18
  extensions?: Record<string, unknown>;
12
19
  /** Parent transcript inherited by the first attempt only. */
13
20
  inheritEvents?: Array<{
@@ -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 }
package/dist/kernel.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface ResourceQuota {
25
25
  maxTotalSubagents?: number;
26
26
  /** Max sub-agent nesting depth (direct children of the root loop are depth 1). */
27
27
  maxSpawnDepth?: number;
28
+ /** Max nodes in one in-kernel workflow DAG, including dynamically submitted nodes. */
29
+ maxWorkflowNodes?: number;
28
30
  /** Rolling-window memory-write rate limit: at most `maxWrites` per any `windowMs` span. */
29
31
  memoryWritesPerWindow?: MemoryWriteRateLimit;
30
32
  }
@@ -50,6 +52,9 @@ export interface MemoryPolicy {
50
52
  maxContentBytes?: number;
51
53
  /** Override the kernel's `write_memory` name-length limit. */
52
54
  maxNameLength?: number;
55
+ /** M4: recall count at which the kernel emits an advisory (edge-triggered)
56
+ * `promotion_suggested` for a recalled record. Omitted = suggestions disabled. */
57
+ promotionRecallThreshold?: number;
53
58
  }
54
59
  export interface GovernanceInstance {
55
60
  setIdentity(agentId: string, sessionId: string): void;
@@ -157,6 +157,9 @@ export type KernelRunnerAction = {
157
157
  };
158
158
  export interface KernelObservation {
159
159
  kind: string;
160
+ /** control_request_rejected: stable control-plane operation name and optional subject id. */
161
+ operation?: string;
162
+ subject?: string;
160
163
  action?: string;
161
164
  rho_after?: number;
162
165
  sprint?: number;
@@ -374,6 +374,23 @@ export declare class RuntimeRunner {
374
374
  private commitKernelAction;
375
375
  private persistMemoryToStore;
376
376
  private retrieveMemoryFromStore;
377
+ /**
378
+ * T5: run one memory query through the kernel's `query_memory → memory_query_result`
379
+ * effect lifecycle on the given runtime. The kernel injects each routed hit into history
380
+ * itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
381
+ * `promotion_suggested`) from the routed hits — the store stays a pure query.
382
+ *
383
+ * `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
384
+ * prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
385
+ * each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
386
+ *
387
+ * The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
388
+ * must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
389
+ * drain loop where queued observations would not be consumed until the next boundary.
390
+ * Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
391
+ * and discarded for detached syscall runtimes (their kernel is throwaway).
392
+ */
393
+ private queryMemoryThroughKernel;
377
394
  writeMemory(memory: MemoryRecord, opts?: {
378
395
  sessionId?: string;
379
396
  agentId?: string;
@@ -394,7 +411,17 @@ export declare class RuntimeRunner {
394
411
  * every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
395
412
  */
396
413
  private applyKernelPolicies;
397
- private appendMemorySyscallObservations;
414
+ /**
415
+ * Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
416
+ * Shared by the main run drain, the prefetch path, and the host memory syscalls so every
417
+ * query route has identical recall + promotion semantics (T5).
418
+ *
419
+ * M3: `memory_recalled` carries the kernel-derived count — the runner never computes
420
+ * `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
421
+ * edge-triggered by the kernel; the runner surfaces it and never auto-pins.
422
+ */
423
+ private mirrorMemoryLifecycle;
424
+ private consumeMemoryLifecycleObservations;
398
425
  /** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
399
426
  mountTool(schema: ToolSchema): Promise<void>;
400
427
  /** Mount a skill capability on the currently-running kernel runtime. No-op if not running. */
@@ -34,6 +34,25 @@ export function schedulerPolicyToKernel(policy) {
34
34
  token_cost_weight: policy.tokenCostWeight,
35
35
  };
36
36
  }
37
+ /** Kernel observation kinds owned by the memory lifecycle consumer (journal + store mirror). */
38
+ function isMemoryLifecycleObservation(obs) {
39
+ return obs.kind === "memory_written"
40
+ || obs.kind === "memory_queried"
41
+ || obs.kind === "memory_validation_failed"
42
+ || obs.kind === "memory_recalled"
43
+ || obs.kind === "promotion_suggested";
44
+ }
45
+ function controlRequestRejection(observations, operation) {
46
+ const rejected = observations.find(observation => observation.kind === "control_request_rejected"
47
+ && (!operation || observation.operation === operation));
48
+ if (!rejected)
49
+ return undefined;
50
+ return {
51
+ operation: rejected.operation ?? operation ?? "control_request",
52
+ ...(rejected.subject ? { subject: rejected.subject } : {}),
53
+ reason: typeof rejected.reason === "string" ? rejected.reason : "request denied",
54
+ };
55
+ }
37
56
  function pendingCallIds(action) {
38
57
  switch (action.kind) {
39
58
  case "call_provider":
@@ -126,6 +145,66 @@ export class RuntimeRunner {
126
145
  return (await this.opts.dreamStore.search(agentId, { ...query, top_k: requestedK }))
127
146
  .slice(0, requestedK);
128
147
  }
148
+ /**
149
+ * T5: run one memory query through the kernel's `query_memory → memory_query_result`
150
+ * effect lifecycle on the given runtime. The kernel injects each routed hit into history
151
+ * itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
152
+ * `promotion_suggested`) from the routed hits — the store stays a pure query.
153
+ *
154
+ * `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
155
+ * prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
156
+ * each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
157
+ *
158
+ * The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
159
+ * must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
160
+ * drain loop where queued observations would not be consumed until the next boundary.
161
+ * Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
162
+ * and discarded for detached syscall runtimes (their kernel is throwaway).
163
+ */
164
+ async queryMemoryThroughKernel(runtime, query, agentId, sessionId, seenRecordIds, leftovers) {
165
+ const observations = [];
166
+ const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, sessionId);
167
+ if (action.kind !== "query_memory") {
168
+ throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
169
+ }
170
+ let hits = [];
171
+ let ioError;
172
+ try {
173
+ hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
174
+ if (seenRecordIds) {
175
+ hits = hits.filter(hit => {
176
+ const id = hit.record.record_id;
177
+ if (seenRecordIds.has(id))
178
+ return false;
179
+ seenRecordIds.add(id);
180
+ return true;
181
+ });
182
+ }
183
+ }
184
+ catch (cause) {
185
+ ioError = cause;
186
+ }
187
+ // Close the kernel effect even when the store failed — never leave a dangling query.
188
+ // The result commit resumes the kernel's reasoning path (`resume_after_preload`), which
189
+ // re-emits the next loop action with the routed hits now in history — callers on a live
190
+ // run kernel must adopt it (a query is a preload, not a detached side-channel).
191
+ const resumed = await this.commitKernelMaybeAction(runtime, observations, {
192
+ kind: "memory_query_result",
193
+ effect_id: action.effectId,
194
+ hits,
195
+ ...(ioError ? { error: formatToolError(ioError) } : {}),
196
+ }, sessionId);
197
+ await this.consumeMemoryLifecycleObservations(sessionId, observations);
198
+ if (leftovers) {
199
+ for (const obs of observations) {
200
+ if (!isMemoryLifecycleObservation(obs))
201
+ leftovers.push(obs);
202
+ }
203
+ }
204
+ if (ioError)
205
+ throw ioError;
206
+ return { hits, action: resumed };
207
+ }
129
208
  async writeMemory(memory, opts = {}) {
130
209
  const sessionId = opts.sessionId ?? this.currentSessionId;
131
210
  const agentId = opts.agentId ?? this.opts.agentId;
@@ -136,7 +215,7 @@ export class RuntimeRunner {
136
215
  const runtime = this.createSyscallRuntime();
137
216
  const action = await this.commitKernelMaybeAction(runtime, observations, { kind: "write_memory", memory }, durableSessionId);
138
217
  if (!action) {
139
- await this.appendMemorySyscallObservations(sessionId, observations);
218
+ await this.consumeMemoryLifecycleObservations(sessionId, observations);
140
219
  return;
141
220
  }
142
221
  if (action.kind !== "persist_memory") {
@@ -154,7 +233,7 @@ export class RuntimeRunner {
154
233
  effect_id: action.effectId,
155
234
  ...(ioError ? { error: formatToolError(ioError) } : {}),
156
235
  }, durableSessionId);
157
- await this.appendMemorySyscallObservations(durableSessionId, observations);
236
+ await this.consumeMemoryLifecycleObservations(durableSessionId, observations);
158
237
  if (ioError)
159
238
  throw ioError;
160
239
  }
@@ -164,29 +243,11 @@ export class RuntimeRunner {
164
243
  if (!this.opts.dreamStore || !agentId)
165
244
  return [];
166
245
  const durableSessionId = this.durableSessionId(sessionId);
167
- const observations = [];
246
+ // Detached syscall runtime: the kernel's history injection and resumed action are inert
247
+ // (the kernel is throwaway), but the recall lifecycle is identical to an in-run query
248
+ // (T5 parity).
168
249
  const runtime = this.createSyscallRuntime();
169
- const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, durableSessionId);
170
- if (action.kind !== "query_memory") {
171
- throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
172
- }
173
- let hits = [];
174
- let ioError;
175
- try {
176
- hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
177
- }
178
- catch (cause) {
179
- ioError = cause;
180
- }
181
- await this.commitKernelApply(runtime, observations, {
182
- kind: "memory_query_result",
183
- effect_id: action.effectId,
184
- hits,
185
- ...(ioError ? { error: formatToolError(ioError) } : {}),
186
- }, durableSessionId);
187
- await this.appendMemorySyscallObservations(durableSessionId, observations);
188
- if (ioError)
189
- throw ioError;
250
+ const { hits } = await this.queryMemoryThroughKernel(runtime, query, agentId, durableSessionId);
190
251
  await this.logMemoryRetrievalResult(durableSessionId, hits);
191
252
  return hits;
192
253
  }
@@ -322,6 +383,7 @@ export class RuntimeRunner {
322
383
  ...(q.maxConcurrentSubagents !== undefined ? { max_concurrent_subagents: q.maxConcurrentSubagents } : {}),
323
384
  ...(q.maxTotalSubagents !== undefined ? { max_total_subagents: q.maxTotalSubagents } : {}),
324
385
  ...(q.maxSpawnDepth !== undefined ? { max_spawn_depth: q.maxSpawnDepth } : {}),
386
+ ...(q.maxWorkflowNodes !== undefined ? { max_workflow_nodes: q.maxWorkflowNodes } : {}),
325
387
  ...(q.memoryWritesPerWindow !== undefined
326
388
  ? { memory_writes_per_window: [q.memoryWritesPerWindow.maxWrites, q.memoryWritesPerWindow.windowMs] }
327
389
  : {}),
@@ -371,14 +433,36 @@ export class RuntimeRunner {
371
433
  }
372
434
  await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
373
435
  }
374
- async appendMemorySyscallObservations(sessionId, observations) {
375
- if (!sessionId)
376
- return;
436
+ /**
437
+ * Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
438
+ * Shared by the main run drain, the prefetch path, and the host memory syscalls so every
439
+ * query route has identical recall + promotion semantics (T5).
440
+ *
441
+ * M3: `memory_recalled` carries the kernel-derived count — the runner never computes
442
+ * `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
443
+ * edge-triggered by the kernel; the runner surfaces it and never auto-pins.
444
+ */
445
+ async mirrorMemoryLifecycle(obs) {
446
+ if (obs.kind === "memory_recalled" && obs.recalls?.length) {
447
+ const agentId = this.opts.agentId;
448
+ if (agentId && this.opts.dreamStore?.recordRecall) {
449
+ await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
450
+ }
451
+ }
452
+ if (obs.kind === "promotion_suggested" && obs.record_id) {
453
+ this.opts.onPromotionSuggested?.({
454
+ recordId: obs.record_id,
455
+ recallCount: obs.recall_count ?? 0,
456
+ });
457
+ }
458
+ }
459
+ async consumeMemoryLifecycleObservations(sessionId, observations) {
377
460
  const turn = this.activeKernel?.turn() ?? 0;
378
461
  for (const obs of observations) {
379
- if (obs.kind !== "memory_written"
380
- && obs.kind !== "memory_queried"
381
- && obs.kind !== "memory_validation_failed")
462
+ if (!isMemoryLifecycleObservation(obs))
463
+ continue;
464
+ await this.mirrorMemoryLifecycle(obs);
465
+ if (!sessionId)
382
466
  continue;
383
467
  const event = kernelObservationToSessionEvent(obs, turn);
384
468
  if (event)
@@ -459,8 +543,14 @@ export class RuntimeRunner {
459
543
  });
460
544
  this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
461
545
  const spawned = findSpawnProcessObservation(observations);
462
- if (!spawned)
546
+ if (!spawned) {
547
+ const rejected = controlRequestRejection(observations, "spawn_sub_agent");
548
+ if (rejected) {
549
+ yield { type: "error", message: `spawn_sub_agent denied: ${rejected.reason}` };
550
+ return;
551
+ }
463
552
  throw new Error("spawn_sub_agent did not emit agent_process_changed");
553
+ }
464
554
  const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
465
555
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
466
556
  const result = await orchestrator.run({
@@ -856,6 +946,11 @@ export class RuntimeRunner {
856
946
  }
857
947
  if (!initialAction)
858
948
  return { nodeOutcomes: [], outputs: {} };
949
+ const workflowRejection = controlRequestRejection(observations);
950
+ if (initialAction.kind === "call_provider" && workflowRejection) {
951
+ this.workflowContinuation = initialAction;
952
+ return { nodeOutcomes: [], outputs: {}, rejection: workflowRejection };
953
+ }
859
954
  if (initialAction.kind !== "spawn_workflow") {
860
955
  throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
861
956
  }
@@ -915,6 +1010,21 @@ export class RuntimeRunner {
915
1010
  const observationStart = this.pendingObservations.length;
916
1011
  const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
917
1012
  const subObs = this.pendingObservations.slice(observationStart);
1013
+ const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
1014
+ ?? (subObs.find(o => o.kind === "nodes_rejected")
1015
+ ? { operation: "submit_workflow_nodes", reason: String(subObs.find(o => o.kind === "nodes_rejected")?.reason ?? "request denied") }
1016
+ : undefined);
1017
+ if (rejected) {
1018
+ const denial = `workflow node submission denied: ${rejected.reason}`;
1019
+ result.result = {
1020
+ ...result.result,
1021
+ termination: "error",
1022
+ finalMessage: { role: "assistant", content: denial, toolCalls: [] },
1023
+ };
1024
+ outputs.set(result.agentId, denial);
1025
+ if (stableId !== result.agentId)
1026
+ outputs.set(stableId, denial);
1027
+ }
918
1028
  if (submitAction?.kind === "spawn_workflow") {
919
1029
  nextNodes.push(...submitAction.nodes);
920
1030
  budget = submitAction.budget ?? budget;
@@ -1098,6 +1208,12 @@ export class RuntimeRunner {
1098
1208
  const runId = midRun && resumedStart?.event.kind === "run_started"
1099
1209
  ? resumedStart.event.run_id
1100
1210
  : crypto.randomUUID();
1211
+ // Idempotent per session: an earlier run's `run_started` already carries these attachments
1212
+ // (same-session retry attempt), so replay reconstructs them — recording and seeding again
1213
+ // would double them in history. Deduping at the append keeps live and replay in agreement.
1214
+ const attachments = req.attachments?.length && !attachmentsAlreadySeeded(prior, req.attachments)
1215
+ ? req.attachments
1216
+ : undefined;
1101
1217
  if (!midRun) {
1102
1218
  await this.opts.sessionLog.append(req.sessionId, {
1103
1219
  kind: "run_started",
@@ -1106,10 +1222,10 @@ export class RuntimeRunner {
1106
1222
  criteria: req.criteria ?? [],
1107
1223
  agent_id: this.opts.agentId,
1108
1224
  system_prompt: this.opts.systemPrompt,
1109
- ...(req.attachments?.length ? { attachments: req.attachments } : {}),
1225
+ ...(attachments ? { attachments } : {}),
1110
1226
  });
1111
1227
  }
1112
- yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments, runId);
1228
+ yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments, runId);
1113
1229
  }
1114
1230
  async *wake(sessionId, extensions) {
1115
1231
  const events = await this.opts.sessionLog.read(sessionId);
@@ -1356,6 +1472,9 @@ export class RuntimeRunner {
1356
1472
  ...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
1357
1473
  ...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
1358
1474
  ...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
1475
+ ...(m.promotionRecallThreshold !== undefined
1476
+ ? { promotion_recall_threshold: m.promotionRecallThreshold }
1477
+ : {}),
1359
1478
  });
1360
1479
  }
1361
1480
  if (this.opts.knowledgeSource) {
@@ -1465,21 +1584,24 @@ export class RuntimeRunner {
1465
1584
  message: attachmentsToKernelMessage(attachments),
1466
1585
  });
1467
1586
  }
1468
- // I4: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead of
1469
- // discovering it via the `memory` tool on turn 3+. Skipped on resumes (already in prior
1470
- // context) and when dreamStore/agentId is absent.
1471
- //
1472
- // Strict dynamic context control: this is single-use retrieval content (facts relevant to
1473
- // THIS run's goal right now), not a stable method/skill — so it lands in `history` as an
1474
- // ordinary turn, exactly like a real `memory` tool result would, and decays with the
1475
- // compression pyramid over subsequent turns instead of pinning itself in `knowledge` forever.
1476
1587
  this.currentGoal = goal;
1477
- if (!resumeMidRun) {
1478
- await this.prefetchMemoryIntoHistory(runtime, "initial");
1479
- }
1480
1588
  let action = resumeMidRun
1481
1589
  ? await this.commitKernelAction(runtime, this.pendingObservations, { kind: "resume" })
1482
1590
  : await this.commitKernelAction(runtime, this.pendingObservations, startPayload);
1591
+ // I4/T5: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead
1592
+ // of discovering it via the `memory` tool on turn 3+. It routes through the kernel's
1593
+ // query_memory lifecycle and therefore runs AFTER start_run — a memory query is a kernel
1594
+ // preload whose result resumes the reasoning path, so issuing it pre-run would leave the
1595
+ // kernel Running and start_run would fault. The resumed action from the last query
1596
+ // supersedes start_run's (same pull contract; it renders the injected hits). Hits land in
1597
+ // `history` as ordinary turns — single-use retrieval content that decays with the
1598
+ // compression pyramid, never pinned into `knowledge`. Skipped on resumes (already in
1599
+ // prior context) and when dreamStore/agentId is absent.
1600
+ if (!resumeMidRun) {
1601
+ const resumed = await this.prefetchMemoryIntoHistory(runtime, "initial");
1602
+ if (resumed)
1603
+ action = resumed;
1604
+ }
1483
1605
  // P0-C: the skill loaded and in effect going into the current turn (updated when the model's
1484
1606
  // `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
1485
1607
  let activeSkill;
@@ -1842,7 +1964,7 @@ export class RuntimeRunner {
1842
1964
  const spec = parseStartWorkflowSpec(call.arguments);
1843
1965
  if (spec) {
1844
1966
  this.pendingAuthoredWorkflows.push(spec);
1845
- const out = "workflow authored; executing now";
1967
+ const out = "workflow submitted for governance adjudication";
1846
1968
  toolResults.push({ callId: call.id, output: out, isError: false });
1847
1969
  yield { type: "tool_result", callId: call.id, content: out, isError: false };
1848
1970
  continue;
@@ -1853,13 +1975,13 @@ export class RuntimeRunner {
1853
1975
  ? parseStartWorkflowArgs(call.arguments)
1854
1976
  : parseSubmitWorkflowNodesArgs(call.arguments);
1855
1977
  yield { type: "workflow_nodes_submitted", nodes };
1856
- const result = { callId: call.id, output: "submitted", isError: false };
1978
+ const result = { callId: call.id, output: "workflow nodes submitted for parent governance adjudication", isError: false };
1857
1979
  toolResults.push(result);
1858
- yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
1980
+ yield { type: "tool_result", callId: call.id, content: result.output, isError: false };
1859
1981
  }
1860
1982
  // O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
1861
- // call. A blocked call never executes; its reason reaches the model as a governance-denied
1862
- // tool result (the kernel rolls the turn back with the note). Decision failures are closed
1983
+ // call. A blocked call never executes; its reason reaches the model as a committed
1984
+ // governance-denied tool result. Decision failures are closed
1863
1985
  // unless the host explicitly marks this hook advisory with `onToolCallFailure: "open"`.
1864
1986
  let executableCalls = normalCalls;
1865
1987
  if (this.opts.onToolCall) {
@@ -2195,7 +2317,7 @@ export class RuntimeRunner {
2195
2317
  * earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
2196
2318
  async prefetchMemoryIntoHistory(runtime, phase) {
2197
2319
  if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
2198
- return;
2320
+ return undefined;
2199
2321
  // P10: recall is default-on (CC session-start recall) — with no hook configured,
2200
2322
  // the goal itself is the query. preQueryMemory stays as the targeting override.
2201
2323
  const preQuery = this.opts.preQueryMemory
@@ -2211,23 +2333,27 @@ export class RuntimeRunner {
2211
2333
  runSpec: this.opts.runSpec,
2212
2334
  phase,
2213
2335
  });
2214
- const lines = [];
2336
+ // T5: route each query through the kernel's query_memory lifecycle instead of calling
2337
+ // the store directly — the kernel injects each routed hit into history itself and the
2338
+ // recall lifecycle (recordRecall / promotion) fires exactly like an in-run query.
2339
+ //
2340
+ // One prefetch = one dedupe horizon: a record hit by several short queries recalls and
2341
+ // injects once. A renewal prefetch starts a fresh horizon — renewal dropped the earlier
2342
+ // injection with the old history, so re-exposure is a genuine new recall.
2343
+ const seenRecordIds = new Set();
2344
+ let resumed;
2215
2345
  for (const q of queries ?? []) {
2216
2346
  if (!q.query.trim())
2217
2347
  continue;
2218
- const hits = await this.opts.dreamStore.search(this.opts.agentId, q);
2219
- for (const hit of hits) {
2220
- lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
2221
- }
2222
- }
2223
- if (lines.length > 0) {
2224
- await this.commitKernelApply(runtime, this.pendingObservations, {
2225
- kind: "add_history_message",
2226
- message: { role: "user", content: lines.join("\n") },
2227
- });
2348
+ const { action } = await this.queryMemoryThroughKernel(runtime, q, this.opts.agentId, this.durableSessionId(this.currentSessionId), seenRecordIds, this.pendingObservations);
2349
+ resumed = action ?? resumed;
2228
2350
  }
2351
+ // Each memory_query_result resumes the reasoning path and re-emits the pending loop
2352
+ // action; the caller must continue from the LAST one (it renders the injected hits).
2353
+ return resumed;
2229
2354
  }
2230
2355
  catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
2356
+ return undefined;
2231
2357
  }
2232
2358
  async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
2233
2359
  const turn = runtime.turn();
@@ -2248,22 +2374,9 @@ export class RuntimeRunner {
2248
2374
  });
2249
2375
  this.activeGroupBudgetScope = undefined;
2250
2376
  }
2251
- // M3: mirror the kernel's journaled recall lifecycle into the durable store so recall
2252
- // history (count + last-recalled turn) survives across sessions.
2253
- if (obs.kind === "memory_recalled" && obs.recalls?.length) {
2254
- const agentId = this.opts.agentId;
2255
- if (agentId && this.opts.dreamStore?.recordRecall) {
2256
- await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
2257
- }
2258
- }
2259
- // M4: a recall crossed the promotion threshold. Advisory — surface it for the host/model to
2260
- // act on (pin or promote to knowledge); the runner does not auto-pin.
2261
- if (obs.kind === "promotion_suggested" && obs.record_id) {
2262
- this.opts.onPromotionSuggested?.({
2263
- recordId: obs.record_id,
2264
- recallCount: obs.recall_count ?? 0,
2265
- });
2266
- }
2377
+ // M3/M4: mirror the kernel's journaled recall lifecycle into the durable store /
2378
+ // host callbacks (shared consumer identical semantics on every query route).
2379
+ await this.mirrorMemoryLifecycle(obs);
2267
2380
  const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
2268
2381
  const event = kernelObservationToSessionEvent(obs, turn, {
2269
2382
  nextArchiveStart,
@@ -2360,6 +2473,15 @@ function isMidRun(events) {
2360
2473
  }
2361
2474
  return lastStarted >= 0 && lastStarted > lastTerminal;
2362
2475
  }
2476
+ /**
2477
+ * True when an earlier run in this session already seeded the same attachments. Replay
2478
+ * reconstructs the attachment message from that run's `run_started`, so recording and
2479
+ * live-seeding them again (a same-session retry attempt) would double them in history.
2480
+ */
2481
+ function attachmentsAlreadySeeded(prior, attachments) {
2482
+ const wanted = JSON.stringify(attachments);
2483
+ return prior.some(({ event }) => event.kind === "run_started" && JSON.stringify(event.attachments ?? []) === wanted);
2484
+ }
2363
2485
  /**
2364
2486
  * Build a kernel `add_history_message` payload from user attachments: a `user`
2365
2487
  * message whose content is the multimodal parts in the kernel's serde shape
@@ -210,9 +210,17 @@ export interface WorkflowNodeOutcome {
210
210
  termination?: TerminationReason;
211
211
  output?: Message;
212
212
  }
213
+ /** A control-plane request rejected before any workflow effect started. */
214
+ export interface ControlRequestRejection {
215
+ operation: string;
216
+ subject?: string;
217
+ reason: string;
218
+ }
213
219
  export interface WorkflowOutcome {
214
220
  nodeOutcomes: WorkflowNodeOutcome[];
215
221
  outputs: Record<string, string>;
222
+ /** Present when the workflow itself was rejected before any node ran. */
223
+ rejection?: ControlRequestRejection;
216
224
  }
217
225
  export declare function workflowNodeOutcomeFromKernel(raw: KernelWorkflowNodeOutcome): WorkflowNodeOutcome;
218
226
  /** Per-node spawn descriptor carried in the `workflow_batch_spawned` observation. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.41",
3
+ "version": "0.2.43",
4
4
  "description": "DeepStrike Node.js SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@anthropic-ai/sdk": "^0.99.0",
75
- "@deepstrike/core": "0.2.41",
75
+ "@deepstrike/core": "0.2.43",
76
76
  "@google/generative-ai": "^0.24.1",
77
77
  "openai": "^5.23.2"
78
78
  },