@sema-agent/core 5.60.1 → 5.61.0

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 (32) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +4 -2
  3. package/dist/agents/subagent.js +9 -9
  4. package/dist/core/governance-codes.d.ts +1 -1
  5. package/dist/core/governance-codes.js +2 -2
  6. package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
  7. package/dist/core/memory-engine/consolidation-driver.js +75 -3
  8. package/dist/core/memory-engine/consolidation.d.ts +52 -5
  9. package/dist/core/memory-engine/consolidation.js +3 -1
  10. package/dist/core/memory-engine/distiller.d.ts +89 -1
  11. package/dist/core/memory-engine/distiller.js +94 -5
  12. package/dist/core/memory-engine/engine.d.ts +8 -0
  13. package/dist/core/memory-engine/engine.js +51 -8
  14. package/dist/core/memory-engine/index.d.ts +1 -1
  15. package/dist/core/memory-engine/index.js +1 -1
  16. package/dist/core/runner/prepare-task.d.ts +6 -3
  17. package/dist/core/runner/runtask.js +57 -26
  18. package/dist/core/task-notification.d.ts +50 -23
  19. package/dist/core/task-notification.js +20 -4
  20. package/dist/core/types.d.ts +76 -19
  21. package/dist/engine/harness/agent-harness.d.ts +58 -2
  22. package/dist/engine/harness/agent-harness.js +115 -5
  23. package/dist/engine/loop/agent-loop.js +153 -15
  24. package/dist/engine/loop/types.d.ts +32 -0
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/orchestration/run-workflow-tool.d.ts +7 -2
  28. package/dist/orchestration/run-workflow-tool.js +1 -1
  29. package/dist/tools/monitor.d.ts +3 -3
  30. package/dist/tools/monitor.js +1 -1
  31. package/package.json +1 -1
  32. package/test/export-surface.snapshot.json +7 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.61.0 — 2026-08-25
4
+
5
+ ### Added
6
+ - **Injection-priority ladder is LIVE (design/373 S1+S2)**: `now`/`next`/`later` now carry three
7
+ distinct delivery semantics (CC 2.1.223 form). `next` = the running turn's next boundary (the
8
+ previous behavior of every frame — byte-identical for `next`); `later` = the run's
9
+ would-otherwise-stop seat (its own closing turn; non-natural endings re-pend per session, the next
10
+ run's turn-open delivers); `now` = `next`'s guarantee plus a manufactured boundary. A
11
+ caller-provenance `TaskStream.steer(text, { priority: "now" })` CUTS the in-flight turn at a
12
+ reconciled boundary (finished tools keep real results; never-started ones settle as paired
13
+ `[INTERRUPTED]` results with `details.errorKind: "interrupted_never_started"`; the CC-verbatim
14
+ interruption marker lands) and the run CONTINUES; every real cut emits the new
15
+ `task.turn_interrupted` notice (audience `user`) and counts toward `maxTurns`. Interrupt authority
16
+ is the steer face's alone.
17
+ - `TaskStream.steer` gains `options.priority` (default `"next"`); the replay identity (`inputId`)
18
+ gains the normalized tier as an axis (same id+text at a different tier ⇒ `steering.duplicate_input_id`).
19
+ - `task_notification` wire events gain an additive `priority` field (live + turn-open lanes; absent =
20
+ pre-373 producer or tier-unknown park — never read absence as `later`).
21
+ - `isTerminalTaskNotification` package-root export; engine-note backlog cap gains terminal preference
22
+ (#445): at a full cap an INTERNAL-lane terminal frame displaces the oldest event batch (which parks
23
+ losslessly) instead of parking behind it — external frames of every status stay ordinary
24
+ displaceable notes (trust cut at the marking site).
25
+ - Engine-layer seams: per-turn AbortController + `interruptTurn(frame)` (consumption guard; run
26
+ signal wins), pre-request immediate re-check, final commit point (a frame arriving during the
27
+ stop-gate await window is served by the same run), `AgentHarness.steer` returns the minted frame.
28
+ - **Product-grade origin folding behind a host-code attestation gate (design/376-C1)**:
29
+ `freezeConsolidationPlan` grows the attested arm — `proposal.mintExposure: "partitioned"` (closed
30
+ single-member set; non-members refuse structurally at the commit door) switches the fold from the
31
+ run-level single value to a per-product committed-exposure re-read over declared inputs (any marked
32
+ input ⇒ external/derived, all-clean ⇒ no origin); every UN-attested plan — every pre-C1 caller and
33
+ every foreign minter — keeps the run-level blanket byte-identical. `mintExposurePartitionedPlan`
34
+ + `LlmConsolidationPlanArm` exported: the exposure-partitioned mint bisects candidates on the
35
+ engine-minted marked flag (clean arm byte-identical to the historical chain; marked arm mints only
36
+ at markedCount ≥ 2; a lone marked row stands as its handle row, withheld from every call); the
37
+ attestation threads verbatim distiller → driver → commit. D-1b two-layer world-movement guard:
38
+ the engine re-checks every served candidate on four axes (rev moved / left listing / challenge-latch
39
+ / turned marked) and refuses the whole plan `_stale_snapshot`; the driver's cross-cycle roster guard
40
+ discards a stale mint cache and settles under the NEW stop reason `mint_invalidated`
41
+ (closed-set add; committed cycles stand add-only). Plan rows gain `markedProducts` (additive);
42
+ snapshot gate rows record `markedIds` (additive); the instruction-form gate grows the D-5 three-arm
43
+ form (attested all-clean products follow C-3's clean arm — default refusal + the
44
+ `allowInstructionProducts` hatch, a named WIDENING vs the pre-C1 marked-set blanket; a marked-input
45
+ product keeps the unconditional hard arm).
46
+
47
+ ### BREAKING
48
+ - **B1** — `notify()` frames with `priority: "later"` (INCLUDING the parameterless default) moved
49
+ from next-boundary delivery to the run's closing seat / the next run's turn-open. Honestly noted:
50
+ in the flat era `later` was silently inert (no runtime signal said so) — the engine version is the
51
+ only discriminator. Callers needing boundary delivery must pass `priority: "next"` explicitly.
52
+ - **B2** — `notify(…, { priority: "now" })` is now a typed refusal (`notify.invalid_priority`);
53
+ previously it delivered with a one-per-run disclosure notice.
54
+ - **B3** — the `task.injection_priority_unimplemented` notice code is RETIRED (catalog + audience row
55
+ + word table). Consumers must judge ladder support by version, never by the code's absence.
56
+ - Closed-set adds, named: `ENGINE_NOTICE_CODES` +`task.turn_interrupted`; notify error family
57
+ +`notify.invalid_priority`; consolidation run stop reasons +`mint_invalidated`. Internal producer
58
+ tiers re-seated: subagent terminal frames ×9 and monitor (both shapes) `later`→`next` (#445
59
+ ALIGNED), workflow terminals now explicit `next`.
60
+
3
61
  ## 5.60.1 — 2026-08-25
4
62
 
5
63
  ### Fixed
@@ -511,8 +511,10 @@ export interface SubagentToolOptions {
511
511
  /**
512
512
  * design/115 P3 — background sub-agents (CC `run_in_background`). When set, the tool exposes the
513
513
  * `run_in_background` parameter: the call returns an `a*` task_id immediately, the child runs
514
- * asynchronously, and completion fires ONE task-notification ("later" priority — CC posture: an agent
515
- * completion doesn't derail active work). Poll/stop via TaskOutput/TaskStop. Absent synchronous only.
514
+ * asynchronously, and completion fires ONE task-notification ("next" priority — design/373 §3.7 census
515
+ * re-seat, upstream-anchored: every background-task terminal is explicitly "next" there; the earlier
516
+ * "later doesn't derail active work" reading was the flat era's — a completion must reach a busy model
517
+ * at the boundary, not at the run's closing seat). Poll/stop via TaskOutput/TaskStop. Absent ⇒ synchronous only.
516
518
  */
517
519
  background?: {
518
520
  registry: import("../core/task-registry.js").TaskRegistry;
@@ -775,7 +775,7 @@ export function createSubagentResume(deps) {
775
775
  }
776
776
  else if (ownsTerminalFacesRevive) {
777
777
  try {
778
- deps.notify?.(resumeFrame, { priority: "later" });
778
+ deps.notify?.(resumeFrame, { priority: "next" });
779
779
  }
780
780
  catch {
781
781
  }
@@ -829,7 +829,7 @@ export function createSubagentResume(deps) {
829
829
  ...(abort.signal.aborted ? {} : { error: msgRevive.slice(0, REPORT_FIELD_MAX) }),
830
830
  ...(!abort.signal.aborted && errCodeReviveReject !== undefined ? { errorCode: errCodeReviveReject } : {}),
831
831
  ...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
832
- }, { priority: "later" });
832
+ }, { priority: "next" });
833
833
  }
834
834
  catch {
835
835
  }
@@ -862,7 +862,7 @@ function parkCompletionNotify(deps) {
862
862
  ...payload,
863
863
  summary: `${payload.summary} (delivered after ${pendingCount} background ${pendingCount === 1 ? "child" : "children"} settled)`,
864
864
  }
865
- : payload, { priority: "later" });
865
+ : payload, { priority: "next" });
866
866
  }
867
867
  catch {
868
868
  }
@@ -2387,7 +2387,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2387
2387
  sessionId: forkedId,
2388
2388
  resumable: false,
2389
2389
  ...(completionIdReap !== undefined ? { completionId: completionIdReap } : {}),
2390
- }, { priority: "later" }), "subagent.reapTerminalNotify");
2390
+ }, { priority: "next" }), "subagent.reapTerminalNotify");
2391
2391
  });
2392
2392
  if (agentName !== undefined) {
2393
2393
  recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
@@ -2588,7 +2588,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2588
2588
  resumable: resumableFork,
2589
2589
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
2590
2590
  ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
2591
- }, { priority: "later" });
2591
+ }, { priority: "next" });
2592
2592
  }
2593
2593
  catch {
2594
2594
  }
@@ -2639,7 +2639,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2639
2639
  ...(settledBg === "failed" ? { error: msgFork.slice(0, REPORT_FIELD_MAX) } : {}),
2640
2640
  ...(settledBg === "failed" && errCodeForkReject !== undefined ? { errorCode: errCodeForkReject } : {}),
2641
2641
  ...(completionIdForkReject !== undefined ? { completionId: completionIdForkReject } : {}),
2642
- }, { priority: "later" });
2642
+ }, { priority: "next" });
2643
2643
  }
2644
2644
  catch {
2645
2645
  }
@@ -2967,7 +2967,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2967
2967
  sessionId: bgChildSessionId,
2968
2968
  resumable: false,
2969
2969
  ...(completionIdReap !== undefined ? { completionId: completionIdReap } : {}),
2970
- }, { priority: "later" }), "subagent.reapTerminalNotify");
2970
+ }, { priority: "next" }), "subagent.reapTerminalNotify");
2971
2971
  });
2972
2972
  const bgDurableProbe = bg.registry.durableAgentRowProbe(taskId);
2973
2973
  const bgRowConfirmed = async () => (bgDurableProbe ? await bgDurableProbe().catch(() => false) : false);
@@ -3438,7 +3438,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3438
3438
  }
3439
3439
  else if (ownsTerminalFaces) {
3440
3440
  try {
3441
- notify?.(completionFrame, { priority: "later" });
3441
+ notify?.(completionFrame, { priority: "next" });
3442
3442
  }
3443
3443
  catch {
3444
3444
  }
@@ -3508,7 +3508,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3508
3508
  ...(settled === "failed" ? { error: msg.slice(0, REPORT_FIELD_MAX) } : {}),
3509
3509
  ...(settled === "failed" && errCodeBgReject !== undefined ? { errorCode: errCodeBgReject } : {}),
3510
3510
  ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
3511
- }, { priority: "later" });
3511
+ }, { priority: "next" });
3512
3512
  }
3513
3513
  catch {
3514
3514
  }
@@ -93,7 +93,7 @@ export type NoticeAudience = "user" | "operator";
93
93
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
94
94
  * no longer minted.
95
95
  */
96
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
96
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.turn_interrupted", "tool_result.offload_put_failed"];
97
97
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
98
98
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
99
99
  * sink is a supported shape, and narrowing that field would break it. */
@@ -113,7 +113,7 @@ export const ENGINE_NOTICE_CODES = [
113
113
  "route.base_url_changed_key_unchanged",
114
114
  "task.user_steer_undrained",
115
115
  "task.user_followup_undrained",
116
- "task.injection_priority_unimplemented",
116
+ "task.turn_interrupted",
117
117
  "tool_result.offload_put_failed",
118
118
  ];
119
119
  const NOTICE_AUDIENCE_TABLE = {
@@ -125,6 +125,7 @@ const NOTICE_AUDIENCE_TABLE = {
125
125
  "memory.hold_disposed": "user",
126
126
  "task.user_steer_undrained": "user",
127
127
  "task.user_followup_undrained": "user",
128
+ "task.turn_interrupted": "user",
128
129
  "config.autocompact_window_clamped": "operator",
129
130
  "config.env_timeout_discarded": "operator",
130
131
  "config.materialize_env_discarded": "operator",
@@ -144,7 +145,6 @@ const NOTICE_AUDIENCE_TABLE = {
144
145
  "memory.consolidation_refused": "operator",
145
146
  "route.fallback_to_primary": "operator",
146
147
  "route.base_url_changed_key_unchanged": "operator",
147
- "task.injection_priority_unimplemented": "operator",
148
148
  "tool_result.offload_put_failed": "operator",
149
149
  };
150
150
  export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
@@ -56,11 +56,24 @@ export interface ConsolidationDriverRunRow {
56
56
  outcome?: ConsolidationRunStopReason;
57
57
  settledAt?: number;
58
58
  stopDetail?: string;
59
- /** The minted plan's replayable half (mint ONCE per run; each cycle filters, never re-mints). */
59
+ /** The minted plan's replayable half (mint ONCE per run; each cycle filters, never re-mints).
60
+ * design/376-C1: `mintExposure` + `servedCandidates` ride the cache — the attestation threads
61
+ * onto every cycle's proposal, the roster feeds the cross-cycle invalidation predicate. Rows
62
+ * cached before the fields existed replay UN-attested with no roster check (run-level fold —
63
+ * exactly the treatment their whole-library prompts earned). */
60
64
  planCache?: {
61
65
  maxInputsPerProduct: number;
62
66
  products: LlmConsolidationPlanProduct[];
67
+ mintExposure?: "partitioned";
68
+ servedCandidates?: Array<{
69
+ id: string;
70
+ rev: string;
71
+ marked: boolean;
72
+ }>;
63
73
  };
74
+ /** design/376-C1 A-5 disclosure — marked candidates withheld from the clean arm's prompts at
75
+ * mint time (present ⇔ the partition engaged over a marked library). */
76
+ withheldFromCleanArm?: number;
64
77
  /** The archived mint transcript's file name under {@link CONSOLIDATION_DRIVER_PLANS_DIR}. */
65
78
  planArchive?: string;
66
79
  /** COMMITTED cycles only (the store moved): a refused/fuse-blocked/zero-delta cycle row does
@@ -181,6 +194,11 @@ export interface ConsolidationRunReceipt {
181
194
  name: string;
182
195
  targets: number;
183
196
  }>;
197
+ /** design/376-C1 A-5 disclosure — marked candidates withheld from the clean arm's prompts
198
+ * (present ⇔ the partitioned mint engaged over a marked library). A host reading a clean
199
+ * product's silence about some fact key should know the current value may sit in a withheld
200
+ * (marked, handle-faced) row. */
201
+ withheldFromCleanArm?: number;
184
202
  planArchive?: string;
185
203
  /** Advisory notices minted for this run (the `_incomplete` disclosure when not converged). */
186
204
  notices: EngineNotice[];
@@ -4,7 +4,7 @@ import { atomicWriteFileSync, ensureDirExists, lockedStrictUpdate, readStrictSid
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
5
  import { CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, screenConsolidationOptions, supersessionFuseCeiling, } from "./consolidation.js";
6
6
  import { memoryConsolidationIncompleteNotice } from "./engine.js";
7
- import { CONSOLIDATION_DRIVE_ABORT, CONSOLIDATION_SNAPSHOT_PARKED, MEMORY_DISTILLER_CONTRACT_V1, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintLlmConsolidationPlan, } from "./distiller.js";
7
+ import { CONSOLIDATION_DRIVE_ABORT, CONSOLIDATION_SNAPSHOT_PARKED, MEMORY_DISTILLER_CONTRACT_V1, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, normalizedCandidateRev, } from "./distiller.js";
8
8
  export const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json";
9
9
  export const CONSOLIDATION_DRIVER_PLANS_DIR = "distiller-plans";
10
10
  function coerceRunsFile(raw) {
@@ -71,6 +71,15 @@ class DriverMintFailure extends Error {
71
71
  this.detail = detail;
72
72
  }
73
73
  }
74
+ class MintInvalidated extends Error {
75
+ detail;
76
+ residue;
77
+ constructor(detail, residue = []) {
78
+ super(detail);
79
+ this.detail = detail;
80
+ this.residue = residue;
81
+ }
82
+ }
74
83
  const codeOf = (err) => {
75
84
  const c = err?.code;
76
85
  return typeof c === "string" ? c : undefined;
@@ -173,6 +182,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
173
182
  repairs: { ...run.repairs },
174
183
  writeFailures: [...run.writeFailures],
175
184
  residue: run.residue ?? [],
185
+ ...(run.withheldFromCleanArm !== undefined ? { withheldFromCleanArm: run.withheldFromCleanArm } : {}),
176
186
  ...(run.planArchive !== undefined ? { planArchive: run.planArchive } : {}),
177
187
  notices,
178
188
  ...(announceFailures.length > 0 ? { announceFailures: [...announceFailures] } : {}),
@@ -187,7 +197,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
187
197
  if (run.planCache === undefined) {
188
198
  let minted;
189
199
  try {
190
- minted = await mintLlmConsolidationPlan({
200
+ minted = await mintExposurePartitionedPlan({
191
201
  candidates,
192
202
  chat: opts.chat,
193
203
  model: opts.model,
@@ -230,10 +240,50 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
230
240
  throw new DriverMintFailure(`the mint needed structural repairs on ${dropped} of ${total} member claim(s) (${(ratio * 100).toFixed(1)}% > mintRepairBudget ${(mintRepairBudget * 100).toFixed(1)}%) — a plan repaired this much does not represent the model's intent, and committing it would fold entries the harness chose, not the model (hallucinated ${plan.minting.repairs.hallucinatedMembers}, duplicate ${plan.minting.repairs.duplicateMembers}, nonInteger ${plan.minting.repairs.nonIntegerMembers}; emptied groups ${plan.minting.repairs.emptyGroups}, oversize ${plan.minting.repairs.oversizeGroups.length})`);
231
241
  }
232
242
  run.planArchive = distillerPlanArchiveName(run.runId, run.attempt);
233
- run.planCache = { maxInputsPerProduct: plan.maxInputsPerProduct, products: plan.products };
243
+ run.planCache = {
244
+ maxInputsPerProduct: plan.maxInputsPerProduct,
245
+ products: plan.products,
246
+ ...(plan.mintExposure !== undefined ? { mintExposure: plan.mintExposure } : {}),
247
+ ...(plan.servedCandidates !== undefined ? { servedCandidates: plan.servedCandidates } : {}),
248
+ };
249
+ if (plan.withheldFromCleanArm !== undefined)
250
+ run.withheldFromCleanArm = plan.withheldFromCleanArm;
234
251
  writeDriverRun(controlDir, run);
235
252
  archiveDistillerPlan(controlDir, run.runId, plan, run.attempt);
236
253
  }
254
+ const roster = run.planCache?.servedCandidates;
255
+ if (roster !== undefined && roster.length > 0) {
256
+ const offeredNow = new Map();
257
+ for (const c of candidates) {
258
+ if (typeof c?.entry?.id === "string")
259
+ offeredNow.set(c.entry.id, { rev: normalizedCandidateRev(c.entry.rev), marked: c.marked === true });
260
+ }
261
+ let ownFold;
262
+ const violations = [];
263
+ for (const rowR of roster) {
264
+ const off = offeredNow.get(rowR.id);
265
+ if (off !== undefined) {
266
+ if (off.rev !== rowR.rev)
267
+ violations.push(`${rowR.id}: rev moved since the mint`);
268
+ else if (off.marked !== rowR.marked)
269
+ violations.push(`${rowR.id}: marked flag flipped since the mint (${String(rowR.marked)} -> ${String(off.marked)})`);
270
+ continue;
271
+ }
272
+ ownFold ??= collectRunFoldEvidence(engine, scope, run.requestId);
273
+ if (ownFold.unreadable !== undefined)
274
+ continue;
275
+ if (!ownFold.targets.has(rowR.id))
276
+ violations.push(`${rowR.id}: left the candidate set without a fold this run's plans attest (deleted, excluded, or folded by another writer)`);
277
+ }
278
+ if (violations.length > 0) {
279
+ const residue = (run.planCache?.products ?? []).flatMap((p) => {
280
+ const live = (p.inputIds ?? []).filter((id) => offeredNow.has(id));
281
+ return live.length > 0 ? [{ name: p.name, targets: live.length }] : [];
282
+ });
283
+ delete run.planCache;
284
+ throw new MintInvalidated(`the mint's served-world roster failed the cross-cycle re-check (${violations.length} row(s)): ${violations.slice(0, 5).join("; ")}${violations.length > 5 ? "; …" : ""} — the cached plan is discarded; committed cycles stand and a fresh run re-mints over the current library`, residue);
285
+ }
286
+ }
237
287
  const offered = new Map();
238
288
  for (const c of candidates)
239
289
  if (typeof c?.entry?.id === "string")
@@ -304,6 +354,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
304
354
  }
305
355
  }
306
356
  };
357
+ const cyclesSeen = [];
307
358
  let drive;
308
359
  try {
309
360
  drive = await driveConsolidationToFixpoint(engine, scope, {
@@ -315,6 +366,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
315
366
  maxCycles: opts.maxCycles ?? 64,
316
367
  takeSnapshot,
317
368
  onCycle: (row) => {
369
+ cyclesSeen.push(row);
318
370
  if (row.committed.products > 0 || row.committed.superseded > 0)
319
371
  run.cyclesDone += 1;
320
372
  run.entriesSuperseded = (run.entriesSuperseded ?? 0) + row.committed.superseded;
@@ -330,6 +382,17 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
330
382
  announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} FAILED before any write: ${inlineUntrusted(err.detail, 300)}`]);
331
383
  return settle("driver_failed", undefined, err.detail);
332
384
  }
385
+ if (err instanceof MintInvalidated) {
386
+ announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} MINT INVALIDATED: ${inlineUntrusted(err.detail, 300)}`]);
387
+ if (run.residue === undefined && err.residue.length > 0)
388
+ run.residue = err.residue;
389
+ const partial = {
390
+ cycles: cyclesSeen,
391
+ productsCommitted: cyclesSeen.reduce((a, c) => a + c.committed.products, 0),
392
+ entriesSuperseded: cyclesSeen.reduce((a, c) => a + c.committed.superseded, 0),
393
+ };
394
+ return settle("mint_invalidated", partial, err.detail);
395
+ }
333
396
  throw err;
334
397
  }
335
398
  const writeResidue = run.writeFailures.map((w) => ({ name: w.key, targets: w.members }));
@@ -372,6 +435,15 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
372
435
  }
373
436
  if (driverResidue.length > 0)
374
437
  run.residue = [...(drive.stop?.fuseBlocked ?? []), ...driverResidue];
438
+ const plannedAtStop = (run.planCache?.products ?? []).some((p) => (p.inputIds?.length ?? 0) > 0);
439
+ if (plannedAtStop) {
440
+ const evidence = collectRunFoldEvidence(engine, scope, run.requestId);
441
+ if (evidence.unreadable !== undefined) {
442
+ announce([
443
+ `memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)}: plan ${evidence.unreadable} — one of THIS run's own cycles — answered ${evidence.unreadableState} (control-plane damage on run-owned evidence, not a transient race); the run stops non-converged with the damage disclosed, and a fresh run re-mints over the current bytes`,
444
+ ]);
445
+ }
446
+ }
375
447
  const stop = drive.stop;
376
448
  if (stop?.fuseBlocked !== undefined)
377
449
  return settle("fuse_residue", drive, stop.reason);
@@ -247,7 +247,13 @@ export interface ConsolidationGateRow {
247
247
  * cycle token; a newer snapshot replaces it, and commit refuses a token that no longer
248
248
  * matches). `candidates`: the served (incremental) id → rev set — the edge-target legality
249
249
  * domain; `eligible`: the FULL eligible id → rev map at snapshot time (the next fingerprint on
250
- * completion); `marked`: any served candidate carried a committed external-origin marker. */
250
+ * completion); `marked`: any served candidate carried a committed external-origin marker;
251
+ * `markedIds` (additive, design/376-C1 D-1b): WHICH served candidates carried it — the
252
+ * attested freeze's world-movement baseline (a candidate marked NOW but absent here turned
253
+ * marked after the snapshot ⇒ the mint's clean-arm premise is stale). Absent on rows written
254
+ * before the field existed: an attested freeze over such a row takes the conservative arm
255
+ * (ANY currently-marked served candidate refuses stale — no baseline means no proof the mark
256
+ * predates the mint, and refusing to re-mint beats guessing). */
251
257
  snapshot?: {
252
258
  token: string;
253
259
  at: number;
@@ -255,6 +261,7 @@ export interface ConsolidationGateRow {
255
261
  candidates: Record<string, string>;
256
262
  eligible: Record<string, string>;
257
263
  marked: boolean;
264
+ markedIds?: string[];
258
265
  };
259
266
  }
260
267
  interface ConsolidationGateFile {
@@ -326,7 +333,10 @@ export type ConsolidationIntent = {
326
333
  type?: string;
327
334
  };
328
335
  /** §1.1 — one proposed product (driver output = DATA, zero carriage authority: origin/distilled
329
- * in a proposal do not exist as fields — the engine computes both at freeze). */
336
+ * in a proposal do not exist as fields — the engine computes both at freeze). The design/376-C1
337
+ * attestation does not breach this invariant: `mintExposure` on the PROPOSAL declares how the
338
+ * prompts were composed, it carries no origin value and can never set one — the engine still
339
+ * computes every origin itself (per product under attestation, run-level otherwise). */
330
340
  export interface ConsolidationProductProposal {
331
341
  name?: string;
332
342
  description?: string;
@@ -342,6 +352,25 @@ export interface ConsolidationProposal {
342
352
  scope: string;
343
353
  products: ConsolidationProductProposal[];
344
354
  intents?: ConsolidationIntent[];
355
+ /**
356
+ * design/376-C1 D-2 — the HOST-CODE attestation about prompt composition: `"partitioned"`
357
+ * declares that the proposal was minted under exposure-partitioned calls, i.e. every all-clean
358
+ * declared product's authoring chain was served NO byte of any committed-origin (marked) entry
359
+ * (the clean arm's grouping/writing calls saw the clean subset only — mechanical isolation,
360
+ * auditable from the archived transcript). The freeze folds origin PER PRODUCT only under this
361
+ * attestation; absent (or any other spelling — refused structurally, closed set), the freeze
362
+ * keeps the run-level single-value fold: any marked candidate in the served set marks EVERY
363
+ * product (today's safe default, and the permanent default for foreign minters).
364
+ *
365
+ * Trust plane, stated in full: this is a host-code attestation, NOT driver/model output — the
366
+ * drive re-assembles the proposal it commits (`{ scope, products, mintExposure }`), so model
367
+ * output can never populate it. Attesting falsely is a HOST defect outside the threat model,
368
+ * the same plane as a mis-declared `ToolSpec.contentOrigin` — and unlike most host mistakes it
369
+ * stays auditable after the fact: the archived clean-arm grouping prompt must contain zero
370
+ * marked-entry bytes, a mechanical check any reader can re-run against any plan that claims
371
+ * this attestation.
372
+ */
373
+ mintExposure?: "partitioned";
345
374
  }
346
375
  export type ConsolidationDirectedState = "pending" | "applied" | "satisfied" | "conflict";
347
376
  export interface ConsolidationDirectedPatch {
@@ -379,7 +408,15 @@ export interface ConsolidationPlanFile {
379
408
  /** r3-1 — the ENGINE-VISIBLE-SET fold fact this run froze under (authority = what the engine
380
409
  * served, never the proposal's declaration). */
381
410
  visibleMarked: boolean;
411
+ /** The RUN-LEVEL single-value fold, when it applied (design/376-C1 D-6): present ⇔ this plan
412
+ * froze UN-attested over a marked served set, and this one value rides every product. An
413
+ * attested (per-product) plan omits it — each product's origin lives in its own frozen bytes
414
+ * below, and a single plan-level value would misdescribe a mixed plan. */
382
415
  foldedOrigin?: MemoryEntryOrigin;
416
+ /** design/376-C1 D-6 — the proposal's accepted attestation, echoed for audit (informational:
417
+ * apply is byte-faithful to `products` and never re-reads this; a reader that predates the
418
+ * field ignores it and replays correctly, which is why the plan version does NOT bump). */
419
+ mintExposure?: "partitioned";
383
420
  /** Frozen products — FULL entry bytes (id/frontmatter incl. distilled + origin/body/rev). */
384
421
  products: MemoryEntry[];
385
422
  productStates: Record<string, "pending" | "applied" | "conflict">;
@@ -479,16 +516,26 @@ export declare function productAddPatch(product: MemoryEntry): NotePatch;
479
516
  * inputs edited mid-run) settles here TOO — committed cycles stand
480
517
  * (add-only, no rollback), the residue is named, and a fresh run re-mints
481
518
  * over the current bytes. The clean `converged` label never covers a
482
- * driver-side residue.
519
+ * driver-side residue;
520
+ * - `mint_invalidated` — design/376-C1 D-1b (driver layer): the mint's served-world ROSTER
521
+ * failed the cross-cycle re-check — a served candidate moved rev, flipped
522
+ * its marked flag, or left the candidate set without a fold this run's own
523
+ * plans attest. The cached plan is discarded (its prompts were composed
524
+ * over a world that no longer stands — replaying it could publish clean
525
+ * products whose authoring chain silently absorbed the moved row's
526
+ * influence); committed cycles stand (add-only, no rollback), and the host
527
+ * re-runs to re-mint over the current bytes (immediately or next window —
528
+ * host policy).
483
529
  * Extending this set is a consumer-visible closed-set expansion (release-note duty: every
484
- * downstream that pinned the enumeration becomes a named party).
530
+ * downstream that pinned the enumeration becomes a named party — `mint_invalidated` was the
531
+ * design/376-C1 add, named on its release).
485
532
  *
486
533
  * The type DERIVES from the array (never `satisfies` beside it): the array exists precisely for
487
534
  * downstream enumeration, and two independent declarations would let a new union arm skip the
488
535
  * array with no compile error — the derived form makes an edit to either side a type error at
489
536
  * the other.
490
537
  */
491
- export declare const CONSOLIDATION_RUN_STOP_REASONS: readonly ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed"];
538
+ export declare const CONSOLIDATION_RUN_STOP_REASONS: readonly ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated"];
492
539
  /** See the doc on {@link CONSOLIDATION_RUN_STOP_REASONS} — the single source this derives from. */
493
540
  export type ConsolidationRunStopReason = (typeof CONSOLIDATION_RUN_STOP_REASONS)[number];
494
541
  export {};
@@ -265,6 +265,8 @@ function planShapeInvalid(raw, planId) {
265
265
  if (freezeRefused !== undefined && (!Array.isArray(freezeRefused) || !freezeRefused.every((x) => typeof x === "string"))) {
266
266
  return "malformed freezeRefusedInputIds";
267
267
  }
268
+ if (p["mintExposure"] !== undefined && p["mintExposure"] !== "partitioned")
269
+ return "malformed mintExposure echo";
268
270
  if ((p["v"] === 2) !== (freezeRefused !== undefined)) {
269
271
  return p["v"] === 2 ? "v2 plan without its freezeRefusedInputIds member" : "freezeRefusedInputIds on a v1 plan";
270
272
  }
@@ -360,4 +362,4 @@ export class ConsolidationRefusedError extends Error {
360
362
  export function productAddPatch(product) {
361
363
  return { op: "add", id: product.id, entry: product, guard: "absent" };
362
364
  }
363
- export const CONSOLIDATION_RUN_STOP_REASONS = ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed"];
365
+ export const CONSOLIDATION_RUN_STOP_REASONS = ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated"];
@@ -64,6 +64,17 @@ export declare const LLM_DISTILLER_CONTRACTS: Record<string, LlmDistillerContrac
64
64
  * archived). The shipping rule this alias exists to make auditable: a change to the product prompt
65
65
  * is a NEW `version` (pd-2, …) and must produce rig readings BEFORE it ships — the contract-hash
66
66
  * pin in the test suite reds on any silent byte drift under an unchanged version id.
67
+ *
68
+ * STANDING OBLIGATION on pd-2 (design/376-C1, ruled — mandatory, not optional): the next contract
69
+ * version MUST carry, as its FIRST A/B item, the root-elimination arm for the shared-grouping
70
+ * influence channel — the clean arm's writing pass without the grouping-authored `intent` (the
71
+ * template's own "(none stated)" arm) and without the `key` name fallback, which empties the one
72
+ * byte path an unattributed candidate has into a clean product and retires most of the D-1b guard
73
+ * machinery. The A/B must publish an EXPLICIT continuation verdict, one of three states — switch
74
+ * (root elimination ships) / hold (guard form stays, with readings) / redirect — it may not be
75
+ * left hanging. The exposure-partitioned topology itself deliberately did NOT bump this version
76
+ * (the candidate-set composition is topology, not wording; the wording axis stays single-variable
77
+ * for exactly this A/B).
67
78
  */
68
79
  export declare const MEMORY_DISTILLER_CONTRACT_V1: LlmDistillerContract;
69
80
  /**
@@ -223,6 +234,20 @@ export interface LlmConsolidationPlanProduct {
223
234
  * alone, exactly as they always did (the archived-readings compatibility arm). */
224
235
  inputRevs?: Record<string, string>;
225
236
  }
237
+ /** design/376-C1 §2.2 — one arm's mint accounting inside an exposure-partitioned plan (usage
238
+ * "两臂合计+分列": the merged plan's top-level `minting` carries the totals, these rows carry the
239
+ * per-arm split PLUS each arm's verbatim grouping prompt — the clean arm's prompt bytes are the
240
+ * C1-G5 audit face: any reader can mechanically verify zero marked-entry bytes reached it). */
241
+ export interface LlmConsolidationPlanArm {
242
+ arm: "clean" | "marked";
243
+ candidateCount: number;
244
+ calls: number;
245
+ promptTokens: number;
246
+ completionTokens: number;
247
+ retries: number;
248
+ truncations: number;
249
+ groupingPrompt: string;
250
+ }
226
251
  export interface LlmConsolidationPlan {
227
252
  kind: "llm-consolidation-plan";
228
253
  contractVersion: string;
@@ -233,6 +258,27 @@ export interface LlmConsolidationPlan {
233
258
  maxInputsPerProduct: number;
234
259
  candidateCount: number;
235
260
  products: LlmConsolidationPlanProduct[];
261
+ /** design/376-C1 D-2 — present ⇔ this plan was minted by {@link mintExposurePartitionedPlan}
262
+ * (clean/marked candidates partitioned into physically separate call chains). The DRIVE threads
263
+ * this onto the committed proposal, where it is the engine's per-product-fold attestation.
264
+ * Absent on every plan minted before the field existed — those replay UN-attested (run-level
265
+ * fold, the safe default), which is exactly right for prompts composed over the whole library. */
266
+ mintExposure?: "partitioned";
267
+ /** design/376-C1 D-1b — the mint's served-world roster: every candidate actually RENDERED into
268
+ * some arm's grouping call, with the rev and marked flag it was served at. The driver's
269
+ * cross-cycle invalidation predicate compares each row against the fresh snapshot
270
+ * (offered ∧ same rev ∧ same marked, or departed-with-own-fold-evidence) — any other state
271
+ * means the world the prompts were composed over no longer stands. A single withheld marked
272
+ * row (markedCount = 1, no marked arm) is deliberately NOT in the roster: no call ever saw it. */
273
+ servedCandidates?: Array<{
274
+ id: string;
275
+ rev: string;
276
+ marked: boolean;
277
+ }>;
278
+ /** design/376-C1 A-5 disclosure — how many marked candidates were withheld from the CLEAN arm's
279
+ * prompts (= the marked candidate count when the partition engaged). Present ⇔ partitioned
280
+ * mint over a library with at least one marked row. */
281
+ withheldFromCleanArm?: number;
236
282
  minting: {
237
283
  calls: number;
238
284
  groupingAttempts: number;
@@ -264,6 +310,10 @@ export interface LlmConsolidationPlan {
264
310
  model: string | null;
265
311
  answer: string;
266
312
  }>;
313
+ /** Per-arm split of a partitioned mint (see {@link LlmConsolidationPlanArm}); absent on
314
+ * single-call-chain plans, INCLUDING the zero-marked degenerate of the partitioned mint
315
+ * (whose bytes must stay identical to the plain mint's, modulo the attestation echo). */
316
+ arms?: LlmConsolidationPlanArm[];
267
317
  };
268
318
  }
269
319
  export type MintLlmConsolidationPlanResult = {
@@ -280,7 +330,7 @@ export type MintLlmConsolidationPlanResult = {
280
330
  * it answered is returned inside the plan so the run can be re-read, re-scored and disputed from
281
331
  * the archive alone.
282
332
  */
283
- export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, }: {
333
+ export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, armLabel, }: {
284
334
  candidates: readonly DistillerCandidate[];
285
335
  chat: DistillerChatFn;
286
336
  /** The requested model id, recorded into the plan (audit anchor). Alias spellings refuse. */
@@ -298,7 +348,44 @@ export declare function mintLlmConsolidationPlan({ candidates, chat, model, base
298
348
  done: number;
299
349
  of: number;
300
350
  }) => void) | null;
351
+ /** design/376-C1 — the partitioned mint's TRANSCRIPT arm tag (`grouping.clean#1` /
352
+ * `writing.marked#1:key#1` label forms). Prompt bytes are untouched — this names calls in the
353
+ * archive only, and stays compatible with `planParseRepairs`' `startsWith("grouping")` phase
354
+ * split and its `#N` retry grammar. Absent ⇒ the historical label forms, byte-identical. */
355
+ armLabel?: "clean" | "marked";
301
356
  }): Promise<MintLlmConsolidationPlanResult>;
357
+ /** ONE rev sentinel for every face that RECORDS or RE-CHECKS a candidate's rev (the mint roster
358
+ * below, the driver's cross-cycle roster re-check, the replay's offered map). The snapshot
359
+ * interface is structural — a host-supplied engine can hand entries whose rev is not a string at
360
+ * runtime — and the recording and re-checking sides MUST agree on the fallback, or a rev-less
361
+ * candidate compares unequal to its own roster row and self-invalidates the mint it was just
362
+ * served into (a paid mint discarded on its own cycle, re-minted and re-discarded forever). */
363
+ export declare const normalizedCandidateRev: (rev: unknown) => string;
364
+ /**
365
+ * design/376-C1 §2.2 — mint an EXPOSURE-PARTITIONED fold plan: candidates are bisected on the
366
+ * snapshot's per-candidate `marked` flag and each subset is minted through its own physically
367
+ * separate call chain, so no clean product's authoring chain (grouping intent included) is ever
368
+ * served a marked entry's bytes. This is what makes the engine's per-product fold SOUND — the
369
+ * attestation the plan carries (`mintExposure: "partitioned"`) is the freeze's gate, and this
370
+ * function is the single source the product driver and every bench adapter share.
371
+ *
372
+ * - CLEAN arm: `mintLlmConsolidationPlan` over the clean subset — contract, templates, retries,
373
+ * sampling all byte-identical to the plain mint (dl-3 恒等; only the candidate-set composition
374
+ * moves, which is topology, not wording — no prompt-contract version bump, D-10/A-10).
375
+ * - MARKED arm: the same contract over the marked subset, minted only at `markedCount ≥ 2`
376
+ * (design C1-B): a single marked row has no group to fold and simply stays where it is — an
377
+ * index handle row loses no discoverability by standing still. Withheld rows are disclosed
378
+ * (`withheldFromCleanArm`) and kept OUT of the served roster (no call saw them).
379
+ * - ZERO-marked degenerate: exactly ONE arm over exactly the full set with the HISTORICAL
380
+ * transcript labels — byte-identical to the plain mint modulo the attestation echo and the
381
+ * roster (C1-G1's identity claim).
382
+ * - Either arm failing fails the WHOLE mint (one mint, one verdict; both arms' calls ride the
383
+ * failure transcript). This widens nothing: under the run-level mint a hostile marked row
384
+ * already sat inside the single grouping call and could fail it whole.
385
+ * - Cross-arm group structure is impossible by construction: each arm's `sanitizeLlmGroups`
386
+ * valid map is built over that arm's own 1..N numbering.
387
+ */
388
+ export declare function mintExposurePartitionedPlan(args: Parameters<typeof mintLlmConsolidationPlan>[0]): Promise<MintLlmConsolidationPlanResult>;
302
389
  /** The distiller signature the drive consumes: `(candidates, snapshot, scope) -> proposal`.
303
390
  * At runtime only `entry.id` is load-bearing for the replay filter — test/bench stubs that hand
304
391
  * partial entry shapes are tolerated by the reads, the type states the engine's real snapshot.
@@ -321,6 +408,7 @@ export declare function llmPlanDistiller(plan: {
321
408
  products?: ReadonlyArray<Pick<LlmConsolidationPlanProduct, "name" | "description" | "type" | "body" | "inputIds"> & {
322
409
  inputRevs?: Record<string, string>;
323
410
  }>;
411
+ mintExposure?: "partitioned";
324
412
  } | undefined | null): ConsolidationDistillFn;
325
413
  export interface FuseSchedule<P> {
326
414
  schedule: "next-fit-decreasing";