@deepstrike/wasm 0.2.36 → 0.2.38

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.
@@ -10,7 +10,7 @@ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass,
10
10
  import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
11
11
  import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
12
12
  import { resolveReducer } from "./reducers.js";
13
- import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
13
+ import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
14
14
  import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
15
15
  import { assertNativeProfile } from "./os-profile.js";
16
16
  import { LargeResultSpool } from "./large-result-spool.js";
@@ -28,6 +28,8 @@ export class RuntimeRunner {
28
28
  * run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
29
29
  * an already-active skill (loading is idempotent; the knowledge push should be too). */
30
30
  knowledgePushedSkills = new Set();
31
+ /** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
32
+ lastEntropySample = null;
31
33
  /** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
32
34
  currentGoal = "";
33
35
  nextArchiveStart = 0;
@@ -48,6 +50,11 @@ export class RuntimeRunner {
48
50
  injectNote(text, urgency = "normal") {
49
51
  this.injectedSignals.push({ source: "custom", signalType: "event", urgency, payload: { goal: text } });
50
52
  }
53
+ /** The most recent kernel session-entropy sample (one per completed turn), or `null` before the
54
+ * first boundary. A pull companion to the streamed `entropy_sample` events. */
55
+ latestEntropy() {
56
+ return this.lastEntropySample;
57
+ }
51
58
  /** Injected-note drain shared with the main loop's per-turn poll: injected notes first (FIFO), then
52
59
  * the configured `signalSource` — one code path so the two inbound channels never drift. */
53
60
  async nextInboundSignal() {
@@ -859,10 +866,35 @@ export class RuntimeRunner {
859
866
  }
860
867
  catch { /* skip */ }
861
868
  }
869
+ const entropyObsStart = this.pendingObservations.length;
862
870
  action = kernelAction(runtime, this.pendingObservations, {
863
871
  kind: "tool_results",
864
872
  results: toolResults.map(toolResultToKernel),
865
873
  });
874
+ // Surface the boundary's entropy measurement live (the heartbeat watch source).
875
+ for (const obs of this.pendingObservations.slice(entropyObsStart)) {
876
+ if (obs.kind === "entropy_sample") {
877
+ this.lastEntropySample = {
878
+ turn: obs.turn ?? 0,
879
+ score: obs.score ?? 0,
880
+ scoreVersion: obs.score_version ?? 0,
881
+ rho: obs.rho ?? 0,
882
+ repeatPressure: obs.repeat_pressure ?? 0,
883
+ failureRate: obs.failure_rate ?? 0,
884
+ rollbacksInWindow: obs.rollbacks_in_window ?? 0,
885
+ windowTurns: obs.window_turns ?? 0,
886
+ };
887
+ yield { type: "entropy_sample", sample: this.lastEntropySample };
888
+ }
889
+ else if (obs.kind === "entropy_alert") {
890
+ yield {
891
+ type: "entropy_alert",
892
+ turn: obs.turn ?? 0,
893
+ score: obs.score ?? 0,
894
+ threshold: obs.threshold ?? 0,
895
+ };
896
+ }
897
+ }
866
898
  }
867
899
  else if (action.kind === "evaluate_milestone") {
868
900
  const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
@@ -959,7 +991,14 @@ export class RuntimeRunner {
959
991
  catch { /* non-fatal */ }
960
992
  }
961
993
  }
962
- yield { type: "done", iterations: turnsUsed, totalTokens, status };
994
+ yield {
995
+ type: "done",
996
+ iterations: turnsUsed,
997
+ totalTokens,
998
+ status,
999
+ // ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
1000
+ ...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
1001
+ };
963
1002
  this.activeKernel = null;
964
1003
  this.currentSessionId = null;
965
1004
  }
@@ -1009,7 +1048,10 @@ export class RuntimeRunner {
1009
1048
  const manifest = workflowNodeToManifest(node, parentSessionId);
1010
1049
  // G4: surface remaining workflow budget so a coordinator node can size its submission.
1011
1050
  const budgetNote = workflowBudgetNote(budget);
1012
- const withBudget = (goal) => (budgetNote ? `${goal}\n\n${budgetNote}` : goal);
1051
+ // W-N2: a DAG edge carries data every dependent node sees its dependencies' outputs (the
1052
+ // kernel sends `input_agent_ids` for all dependents; judges/reduce keep their special paths).
1053
+ const depsNote = dependencyOutputsNote(node.input_agent_ids, outputs);
1054
+ const withBudget = (goal) => [goal, depsNote, budgetNote].filter(Boolean).join("\n\n");
1013
1055
  const mkCtx = (goal) => ({
1014
1056
  parentOpts: this.opts,
1015
1057
  parentSessionId,
@@ -1018,6 +1060,10 @@ export class RuntimeRunner {
1018
1060
  sessionLog: this.opts.sessionLog,
1019
1061
  // M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel.
1020
1062
  isWorkflowNode: true,
1063
+ // W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
1064
+ // by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
1065
+ // stay deny-all filtered (they read untrusted content).
1066
+ toolAccess: (node.trust === "quarantined" ? "filtered" : "inherit"),
1021
1067
  // #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
1022
1068
  ...(abortSignal ? { abortSignal } : {}),
1023
1069
  });
@@ -1037,9 +1083,18 @@ export class RuntimeRunner {
1037
1083
  const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
1038
1084
  return withSignal(result, { tournamentWinner: winnerId });
1039
1085
  }
1040
- // A#2 v2 loop iteration: run the increment, then extract a stop signal. No signal ⇒ run to cap.
1086
+ // A#2 v2 loop iteration: run the increment under the armed pacing trap (workflowNodeToSpec set
1087
+ // `loopRound`, and the iteration resumes the loop's stable session — transcript-as-carry).
1088
+ // DW-3 one vocabulary: the kernel-adjudicated `pace` verb IS the continuation signal
1089
+ // (stop → loopContinue=false); the legacy text-sniffed JSON blob survives only as the fallback
1090
+ // when no pace decision arrives (stub orchestrators, harness children), where no signal still
1091
+ // means "run to max_iters" (v1).
1041
1092
  if (node.loop_max_iters != null) {
1042
- const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters)}`));
1093
+ const iteration = Number(/-i(\d+)$/.exec(node.agent_id)?.[1] ?? "0");
1094
+ const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters, iteration)}`));
1095
+ const pace = result.result.paceDecision;
1096
+ if (pace)
1097
+ return withSignal(result, { loopContinue: pace.action !== "stop" });
1043
1098
  const cont = extractLoopContinue(textOf(result));
1044
1099
  return cont === undefined ? result : withSignal(result, { loopContinue: cont });
1045
1100
  }
@@ -1151,6 +1206,17 @@ export class RuntimeRunner {
1151
1206
  if (this.opts.knowledgeBudgetRatio !== undefined) {
1152
1207
  config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
1153
1208
  }
1209
+ // Entropy watch (opt-in): threshold alerting over the per-turn session-entropy score.
1210
+ if (this.opts.entropyWatch !== undefined) {
1211
+ const ew = this.opts.entropyWatch;
1212
+ config.entropy_watch = {
1213
+ enabled: ew.enabled ?? true,
1214
+ ...(ew.threshold !== undefined ? { threshold: ew.threshold } : {}),
1215
+ ...(ew.hysteresis !== undefined ? { hysteresis: ew.hysteresis } : {}),
1216
+ ...(ew.cooldownTurns !== undefined ? { cooldown_turns: ew.cooldownTurns } : {}),
1217
+ ...(ew.notifyModel !== undefined ? { notify_model: ew.notifyModel } : {}),
1218
+ };
1219
+ }
1154
1220
  kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
1155
1221
  if (this.opts.memoryPolicy) {
1156
1222
  const m = this.opts.memoryPolicy;
@@ -1215,10 +1281,22 @@ export class RuntimeRunner {
1215
1281
  parent_session_id: parentSessionId,
1216
1282
  // W0-ABI resume: skip nodes already completed before an interruption.
1217
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
1286
+ ? {
1287
+ resumed_results: opts.resumedResults.map(r => ({
1288
+ agent_id: r.agentId,
1289
+ ...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
1290
+ ...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
1291
+ ...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
1292
+ })),
1293
+ }
1294
+ : {}),
1218
1295
  // R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
1219
1296
  ...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
1297
+ ...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
1220
1298
  });
1221
- return await this.driveWorkflow(observations, parentSessionId, runtime);
1299
+ return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
1222
1300
  }
1223
1301
  finally {
1224
1302
  if (bootstrapped) {
@@ -1241,6 +1319,18 @@ export class RuntimeRunner {
1241
1319
  const parentSessionId = this.currentSessionId;
1242
1320
  const runtime = this.activeKernel;
1243
1321
  const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
1322
+ // W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
1323
+ // announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
1324
+ // had this spec, unlike the `runWorkflow` path.
1325
+ const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
1326
+ if (submitted) {
1327
+ await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1328
+ turn: runtime.turn(),
1329
+ nodes: workflowSpecToKernel(spec).nodes ?? [],
1330
+ baseIndex: submitted.base,
1331
+ submitterAgentId: opts?.submitterAgentId,
1332
+ }));
1333
+ }
1244
1334
  return this.driveWorkflow(observations, parentSessionId, runtime);
1245
1335
  }
1246
1336
  /**
@@ -1297,7 +1387,7 @@ export class RuntimeRunner {
1297
1387
  * `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
1298
1388
  * agent-submitted nodes first), and loop until the kernel reports the workflow complete.
1299
1389
  */
1300
- async driveWorkflow(initial, parentSessionId, runtime) {
1390
+ async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
1301
1391
  const observations = initial;
1302
1392
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
1303
1393
  const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
@@ -1310,7 +1400,9 @@ export class RuntimeRunner {
1310
1400
  let nodes = collectNodes(observations);
1311
1401
  let budget = collectBudget(observations);
1312
1402
  // G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
1313
- const outputs = new Map();
1403
+ // W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
1404
+ // still see their (pre-crash) dependencies' outputs.
1405
+ const outputs = new Map(seedOutputs ?? []);
1314
1406
  for (;;) {
1315
1407
  if (nodes.length === 0)
1316
1408
  return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
@@ -1330,7 +1422,13 @@ export class RuntimeRunner {
1330
1422
  for (const result of results) {
1331
1423
  // G2: record this node's output so a downstream reduce node can consume it.
1332
1424
  const outContent = result.result.finalMessage?.content;
1333
- outputs.set(result.agentId, typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "");
1425
+ const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
1426
+ outputs.set(result.agentId, outText);
1427
+ // A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
1428
+ // node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
1429
+ const stableId = result.agentId.replace(/-i\d+$/, "");
1430
+ if (stableId !== result.agentId)
1431
+ outputs.set(stableId, outText);
1334
1432
  // R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
1335
1433
  // reporting the node's completion — the workflow is still active, so even a last-node
1336
1434
  // submission keeps the DAG alive.
@@ -1341,10 +1439,15 @@ export class RuntimeRunner {
1341
1439
  const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
1342
1440
  nextNodes.push(...collectNodes(subObs));
1343
1441
  budget = collectBudget(subObs) ?? budget;
1344
- // R3-1: persist the submission (kernel-shape nodes) so resume can re-apply it.
1442
+ // R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
1443
+ // so resume can re-apply the batch at the exact original graph position. W-N3: also the
1444
+ // submitter, so resume drops batches whose submitter re-runs (it will re-submit).
1445
+ const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
1345
1446
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1346
1447
  turn: runtime.turn(),
1347
1448
  nodes: submitEvent.nodes ?? [],
1449
+ baseIndex: submitted?.base,
1450
+ submitterAgentId: result.agentId,
1348
1451
  }));
1349
1452
  }
1350
1453
  const obs = kernelApply(runtime, this.pendingObservations, {
@@ -1356,11 +1459,17 @@ export class RuntimeRunner {
1356
1459
  const d = findDone(obs);
1357
1460
  if (d)
1358
1461
  done = d;
1359
- // Persist node completion for resume recovery.
1462
+ // Persist node completion for resume recovery. W-1: the result-borne control signals ride
1463
+ // along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
1464
+ // text (post-resume dependents/reduce still see this node's output).
1360
1465
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
1361
1466
  turn: runtime.turn(),
1362
1467
  agentId: result.agentId,
1363
1468
  termination: result.result.termination,
1469
+ classifyBranch: result.result.classifyBranch,
1470
+ tournamentWinner: result.result.tournamentWinner,
1471
+ loopContinue: result.result.loopContinue,
1472
+ ...(result.result.termination === "completed" && outText ? { output: outText } : {}),
1364
1473
  }));
1365
1474
  }
1366
1475
  if (done && nextNodes.length === 0) {
@@ -1371,8 +1480,9 @@ export class RuntimeRunner {
1371
1480
  }
1372
1481
  /**
1373
1482
  * Resume a workflow from the parent session's completed nodes.
1374
- * Reads the session log, extracts completed workflow node agent_ids, and
1375
- * calls runWorkflow with resumedCompleted so the kernel skips those nodes.
1483
+ * Reads the session log, extracts completed workflow node records (with their W-1 control
1484
+ * signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
1485
+ * flow (classify prune / loop stop), and the driver re-seeds its outputs map.
1376
1486
  */
1377
1487
  async resumeWorkflow(spec, opts) {
1378
1488
  // Standalone resume: a stateless handler passes the prior `sessionId`; mid-run callers omit it.
@@ -1381,9 +1491,34 @@ export class RuntimeRunner {
1381
1491
  throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
1382
1492
  }
1383
1493
  const events = await this.opts.sessionLog.read(sessionId);
1384
- const resumedCompleted = recoverCompletedWorkflowNodes(events);
1385
- const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
1386
- return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions, sessionId });
1494
+ const resumedResults = recoverCompletedWorkflowNodes(events);
1495
+ const completedIds = new Set(resumedResults.map(r => r.agentId));
1496
+ const recovered = recoverSubmittedWorkflowNodes(events);
1497
+ // W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
1498
+ // 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.
1501
+ let { submissions, bases } = recovered;
1502
+ if (bases.length === submissions.length && submissions.length > 0) {
1503
+ const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
1504
+ submissions = submissions.filter((_, i) => keep[i]);
1505
+ bases = bases.filter((_, i) => keep[i]);
1506
+ }
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
+ return this.runWorkflow(spec, {
1516
+ resumedResults,
1517
+ resumedSubmissions: submissions,
1518
+ resumedSubmissionBases: bases,
1519
+ resumedOutputs,
1520
+ sessionId,
1521
+ });
1387
1522
  }
1388
1523
  async appendObservations(sessionId, runtime, nextArchiveStart) {
1389
1524
  const turn = runtime.turn();
@@ -1422,12 +1557,23 @@ export class RuntimeRunner {
1422
1557
  const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
1423
1558
  if (event.kind === "compressed") {
1424
1559
  nextArchiveStart = compressedSeq + 1;
1425
- }
1426
- if (obs.kind === "page_out"
1427
- && obs.tier_hint === "semantic"
1428
- && Array.isArray(obs.archived)
1429
- && obs.archived.length > 0) {
1430
- void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
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
+ }
1576
+ }
1431
1577
  }
1432
1578
  // K4: a sprint renewal dropped the old history — including any earlier memory hits — so
1433
1579
  // re-run the preQueryMemory prefetch for the new sprint (live observations only).
@@ -1443,10 +1589,14 @@ export class RuntimeRunner {
1443
1589
  * re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
1444
1590
  * hits). Errs-open throughout. */
1445
1591
  async prefetchMemoryIntoHistory(runtime, phase) {
1446
- if (!this.opts.preQueryMemory || !this.opts.dreamStore || !this.opts.agentId)
1592
+ if (!this.opts.dreamStore || !this.opts.agentId)
1447
1593
  return;
1594
+ // P10: recall is default-on (CC session-start recall) — with no hook configured,
1595
+ // the goal itself is the query. preQueryMemory stays as the targeting override.
1596
+ const preQuery = this.opts.preQueryMemory
1597
+ ?? ((ctx) => [ctx.goal]);
1448
1598
  try {
1449
- const queries = await this.opts.preQueryMemory({ goal: this.currentGoal, phase });
1599
+ const queries = await preQuery({ goal: this.currentGoal, phase });
1450
1600
  const lines = [];
1451
1601
  for (const q of queries ?? []) {
1452
1602
  if (typeof q !== "string" || !q.trim())
@@ -1473,8 +1623,12 @@ export class RuntimeRunner {
1473
1623
  ? await this.opts.dreamSummarizer.summarize(archived, { action })
1474
1624
  : await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
1475
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;
1476
1630
  await this.opts.dreamStore.commit(this.opts.agentId, {
1477
- toAdd: [{ text: summary, score: 1.0, metadata: { source: "semantic_page_out", action } }],
1631
+ toAdd: [{ text: summary, score: 0.6, metadata: { source: "semantic_page_out", action } }],
1478
1632
  toRemoveIndices: [],
1479
1633
  stats: {
1480
1634
  insightsProcessed: 1,
@@ -1489,6 +1643,19 @@ export class RuntimeRunner {
1489
1643
  }
1490
1644
  }
1491
1645
  }
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
+ }
1492
1659
  async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
1493
1660
  const transcript = archived
1494
1661
  .map(m => `${m.role}: ${m.content}`)
@@ -1,5 +1,5 @@
1
1
  import type { ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
2
- import type { KernelEventCategory, KernelPrimitive } from "./kernel-event-log.js";
2
+ import type { KernelPrimitive } from "./kernel-event-log.js";
3
3
  export type RollbackReason = {
4
4
  kind: "fatal_tool_error";
5
5
  tool_name: string;
@@ -74,8 +74,6 @@ export type SessionEvent = {
74
74
  } | {
75
75
  kind: "compressed";
76
76
  turn: number;
77
- category?: KernelEventCategory;
78
- primitive?: KernelPrimitive;
79
77
  archived_seq_range: [number, number];
80
78
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
81
79
  summary?: string;
@@ -85,8 +83,6 @@ export type SessionEvent = {
85
83
  } | {
86
84
  kind: "page_out";
87
85
  turn: number;
88
- category?: KernelEventCategory;
89
- primitive?: KernelPrimitive;
90
86
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
91
87
  summary?: string;
92
88
  tier_hint?: string;
@@ -94,14 +90,10 @@ export type SessionEvent = {
94
90
  } | {
95
91
  kind: "page_in";
96
92
  turn: number;
97
- category?: KernelEventCategory;
98
- primitive?: KernelPrimitive;
99
93
  entry_count: number;
100
94
  } | {
101
95
  kind: "large_result_spooled";
102
96
  turn: number;
103
- category?: KernelEventCategory;
104
- primitive?: KernelPrimitive;
105
97
  call_id: string;
106
98
  tool: string;
107
99
  original_size: number;
@@ -110,15 +102,11 @@ export type SessionEvent = {
110
102
  } | {
111
103
  kind: "rollbacked";
112
104
  turn: number;
113
- category?: KernelEventCategory;
114
- primitive?: KernelPrimitive;
115
105
  checkpoint_history_len: number;
116
106
  reason?: RollbackReason;
117
107
  } | {
118
108
  kind: "capability_changed";
119
109
  turn: number;
120
- category?: KernelEventCategory;
121
- primitive?: KernelPrimitive;
122
110
  added: string[];
123
111
  removed: string[];
124
112
  change_kind?: string;
@@ -129,78 +117,66 @@ export type SessionEvent = {
129
117
  } | {
130
118
  kind: "context_renewed";
131
119
  turn: number;
132
- category?: KernelEventCategory;
133
- primitive?: KernelPrimitive;
134
120
  sprint: number;
135
121
  handoff_ref: string;
136
122
  } | {
137
123
  kind: "suspended";
138
124
  turn: number;
139
- category?: KernelEventCategory;
140
- primitive?: KernelPrimitive;
141
125
  reason: string;
142
126
  pending_calls?: string[];
143
127
  } | {
144
128
  kind: "resumed";
145
129
  turn: number;
146
- category?: KernelEventCategory;
147
- primitive?: KernelPrimitive;
148
130
  approved?: string[];
149
131
  denied?: string[];
150
132
  } | {
151
133
  kind: "tool_gated";
152
134
  turn: number;
153
- category?: KernelEventCategory;
154
- primitive?: KernelPrimitive;
155
135
  call_id: string;
156
136
  tool: string;
157
137
  reason: string;
158
138
  } | {
159
139
  kind: "signal_disposed";
160
140
  turn: number;
161
- category?: KernelEventCategory;
162
- primitive?: KernelPrimitive;
163
141
  signal_id: string;
164
142
  disposition: string;
165
143
  queue_depth: number;
166
144
  } | {
167
145
  kind: "budget_exceeded";
168
146
  turn: number;
169
- category?: KernelEventCategory;
170
- primitive?: KernelPrimitive;
171
147
  budget: string;
172
148
  } | {
173
149
  kind: "milestone_advanced";
174
150
  turn: number;
175
- category?: KernelEventCategory;
176
- primitive?: KernelPrimitive;
177
151
  phase_id: string;
178
152
  capabilities_unlocked: string[];
179
153
  } | {
180
154
  kind: "milestone_blocked";
181
155
  turn: number;
182
- category?: KernelEventCategory;
183
- primitive?: KernelPrimitive;
184
156
  phase_id: string;
185
157
  reason: string;
186
- } | {
187
- kind: "milestone_evidence";
188
- turn: number;
189
- category?: KernelEventCategory;
190
- primitive?: KernelPrimitive;
191
- phase_id: string;
192
- evidence: string[];
193
158
  } | {
194
159
  kind: "checkpoint_taken";
195
160
  turn: number;
196
- category?: KernelEventCategory;
197
- primitive?: KernelPrimitive;
198
161
  history_len: number;
162
+ } | {
163
+ kind: "entropy_sample";
164
+ turn: number;
165
+ score: number;
166
+ score_version: number;
167
+ rho: number;
168
+ repeat_pressure: number;
169
+ failure_rate: number;
170
+ rollbacks_in_window: number;
171
+ window_turns: number;
172
+ } | {
173
+ kind: "entropy_alert";
174
+ turn: number;
175
+ score: number;
176
+ threshold: number;
199
177
  } | {
200
178
  kind: "agent_process_changed";
201
179
  turn: number;
202
- category?: KernelEventCategory;
203
- primitive?: KernelPrimitive;
204
180
  agent_id: string;
205
181
  parent_session_id: string;
206
182
  role: string;
@@ -212,52 +188,52 @@ export type SessionEvent = {
212
188
  } | {
213
189
  kind: "memory_written";
214
190
  turn: number;
215
- category?: KernelEventCategory;
216
- primitive?: KernelPrimitive;
217
191
  memory_id: string;
218
192
  memory_kind: string;
219
193
  size_bytes: number;
220
194
  } | {
221
195
  kind: "memory_queried";
222
196
  turn: number;
223
- category?: KernelEventCategory;
224
- primitive?: KernelPrimitive;
225
197
  query_context: string;
226
198
  requested_k: number;
227
199
  requires_async_response: boolean;
228
200
  } | {
229
201
  kind: "memory_validation_failed";
230
202
  turn: number;
231
- category?: KernelEventCategory;
232
- primitive?: KernelPrimitive;
233
203
  memory_id: string;
234
204
  error: string;
235
205
  } | {
236
206
  kind: "workflow_node_completed";
237
207
  turn: number;
238
- category?: KernelEventCategory;
239
- primitive?: KernelPrimitive;
240
208
  agent_id: string;
241
209
  termination: string;
210
+ /** W-1: result-borne control signals, persisted so resume replays control flow faithfully —
211
+ * a classifier re-prunes its rejected branches, a recorded loop stop is honored. */
212
+ classify_branch?: string;
213
+ tournament_winner?: string;
214
+ loop_continue?: boolean;
215
+ /** W-1: the node's final output text — resume re-seeds the driver's outputs map from it so
216
+ * post-resume reduce/judge/dependent nodes still see their dependencies' outputs. */
217
+ output?: string;
242
218
  } | {
243
219
  kind: "workflow_nodes_submitted";
244
220
  turn: number;
245
- category?: KernelEventCategory;
246
- primitive?: KernelPrimitive;
247
221
  /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
248
222
  nodes: Record<string, unknown>[];
223
+ /** R3-1: graph base index the batch was appended at (from the kernel's
224
+ * WorkflowNodesSubmitted observation) — lets resume rebuild exact indices. */
225
+ base_index?: number;
226
+ /** W-N3: the submitting node's agent id (absent = host/bootstrap). Resume DROPS batches whose
227
+ * submitter re-runs — it will re-submit — instead of duplicating their nodes. */
228
+ submitter_agent_id?: string;
249
229
  } | {
250
230
  kind: "workflow_batch_spawned";
251
231
  turn: number;
252
- category?: KernelEventCategory;
253
- primitive?: KernelPrimitive;
254
232
  node_count: number;
255
233
  node_ids: string[];
256
234
  } | {
257
235
  kind: "workflow_completed";
258
236
  turn: number;
259
- category?: KernelEventCategory;
260
- primitive?: KernelPrimitive;
261
237
  completed: string[];
262
238
  failed: string[];
263
239
  total_nodes: number;
@@ -36,33 +36,56 @@ export declare function buildRunTerminalEvent(input: {
36
36
  }): Extract<SessionEvent, {
37
37
  kind: "run_terminal";
38
38
  }>;
39
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
40
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
39
41
  export declare function buildWorkflowNodeCompletedEvent(input: {
40
42
  turn: number;
41
43
  agentId: string;
42
44
  termination: string;
45
+ classifyBranch?: string;
46
+ tournamentWinner?: string;
47
+ loopContinue?: boolean;
48
+ output?: string;
43
49
  }): Extract<SessionEvent, {
44
50
  kind: "workflow_node_completed";
45
51
  }>;
52
+ /** One recovered node completion: the agent id plus its persisted control signals and output. */
53
+ export interface RecoveredNodeCompletion {
54
+ agentId: string;
55
+ classifyBranch?: string;
56
+ tournamentWinner?: string;
57
+ loopContinue?: boolean;
58
+ output?: string;
59
+ }
46
60
  /**
47
- * Recover completed workflow node agent_ids from a session event stream.
48
- * Scans for workflow_node_completed events and returns the agent_ids whose
49
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
61
+ * Recover completed workflow node records from a session event stream. Scans for
62
+ * workflow_node_completed events with termination "completed" and returns them WITH their
63
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
64
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
65
+ * driver's outputs map from the persisted output text.
50
66
  */
51
67
  export declare function recoverCompletedWorkflowNodes(events: Array<{
52
68
  seq: number;
53
69
  event: SessionEvent;
54
- }>): string[];
70
+ }>): RecoveredNodeCompletion[];
55
71
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
56
72
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
57
73
  export declare function buildWorkflowNodesSubmittedEvent(input: {
58
74
  turn: number;
59
75
  nodes: Record<string, unknown>[];
76
+ baseIndex?: number;
77
+ submitterAgentId?: string;
60
78
  }): Extract<SessionEvent, {
61
79
  kind: "workflow_nodes_submitted";
62
80
  }>;
63
81
  /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
64
- * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
82
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed.
83
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
65
84
  export declare function recoverSubmittedWorkflowNodes(events: Array<{
66
85
  seq: number;
67
86
  event: SessionEvent;
68
- }>): Record<string, unknown>[][];
87
+ }>): {
88
+ submissions: Record<string, unknown>[][];
89
+ bases: number[];
90
+ submitters: Array<string | undefined>;
91
+ };