@sema-agent/core 5.48.0 → 5.50.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 (61) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +1 -1
  3. package/dist/agents/agent-transcript-tool.js +1 -1
  4. package/dist/agents/roster-store.js +4 -1
  5. package/dist/agents/send-message-tool.d.ts +2 -2
  6. package/dist/agents/send-message-tool.js +2 -2
  7. package/dist/agents/subagent.d.ts +6 -0
  8. package/dist/agents/subagent.js +126 -1
  9. package/dist/agents/teacher.d.ts +25 -1
  10. package/dist/agents/teacher.js +89 -13
  11. package/dist/brain/anthropic.js +11 -20
  12. package/dist/brain/open-responses.js +6 -14
  13. package/dist/brain/openai.js +6 -18
  14. package/dist/brain/reasoning.d.ts +100 -8
  15. package/dist/brain/reasoning.js +39 -15
  16. package/dist/brain/request-params.d.ts +37 -1
  17. package/dist/brain/request-params.js +40 -2
  18. package/dist/core/background-agent-store.d.ts +1 -1
  19. package/dist/core/background-agent-store.js +5 -4
  20. package/dist/core/mcp.d.ts +7 -1
  21. package/dist/core/mcp.js +64 -8
  22. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  23. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  24. package/dist/core/memory-engine/dual-root.js +11 -0
  25. package/dist/core/memory-engine/engine.d.ts +36 -2
  26. package/dist/core/memory-engine/engine.js +354 -38
  27. package/dist/core/memory-engine/layout.d.ts +43 -0
  28. package/dist/core/memory-engine/layout.js +59 -0
  29. package/dist/core/memory-engine/memory-backend-contract.js +120 -0
  30. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  31. package/dist/core/memory-engine/origin-clearance.js +10 -0
  32. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  33. package/dist/core/memory-engine/provenance-wording.js +1 -0
  34. package/dist/core/memory-engine/tools.js +6 -4
  35. package/dist/core/memory-engine/types.d.ts +13 -1
  36. package/dist/core/runner/prepare-task.js +22 -8
  37. package/dist/core/runner/runtask.d.ts +26 -1
  38. package/dist/core/runner/runtask.js +18 -2
  39. package/dist/core/strategy-store.d.ts +180 -3
  40. package/dist/core/strategy-store.js +172 -23
  41. package/dist/core/task-registry-agent.js +6 -0
  42. package/dist/core/types.d.ts +24 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/orchestration/run-workflow-tool.d.ts +12 -0
  46. package/dist/orchestration/run-workflow-tool.js +1 -1
  47. package/dist/orchestration/workflow-governance.d.ts +27 -0
  48. package/dist/orchestration/workflow-governance.js +13 -0
  49. package/dist/orchestration/workflow-primitives.d.ts +8 -1
  50. package/dist/orchestration/workflow-primitives.js +11 -3
  51. package/dist/stores/file/file-snapshot-store.js +7 -1
  52. package/dist/stores/file/index.d.ts +8 -0
  53. package/dist/stores/file/index.js +12 -0
  54. package/dist/stores/file/session-policy-store.d.ts +0 -13
  55. package/dist/stores/file/session-policy-store.js +7 -1
  56. package/dist/stores/file/session-store.d.ts +4 -1
  57. package/dist/stores/file/session-store.js +7 -1
  58. package/dist/stores/file/strategy-store.d.ts +97 -0
  59. package/dist/stores/file/strategy-store.js +340 -0
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +8 -1
package/dist/core/mcp.js CHANGED
@@ -650,7 +650,30 @@ export function mcpToolSchemaProblem(schema) {
650
650
  }
651
651
  return undefined;
652
652
  }
653
- export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure) {
653
+ export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
654
+ let revocationProbeFailed = false;
655
+ const isServerRevoked = (serverName) => {
656
+ if (mcpRevocations === undefined)
657
+ return false;
658
+ try {
659
+ const r = mcpRevocations.isRevoked(serverName);
660
+ if (typeof r !== "boolean") {
661
+ throw new Error(`isRevoked returned ${r instanceof Promise ? "a Promise — the ledger seat is SYNCHRONOUS by contract" : `a non-boolean (${typeof r})`}`);
662
+ }
663
+ return r;
664
+ }
665
+ catch (e) {
666
+ if (!revocationProbeFailed) {
667
+ revocationProbeFailed = true;
668
+ try {
669
+ mcpRevocations.onProbeFailure?.(e);
670
+ }
671
+ catch {
672
+ }
673
+ }
674
+ return false;
675
+ }
676
+ };
654
677
  const mcpDisclosure = reminderDisclosure?.reminderMark !== undefined
655
678
  ? { mark: reminderDisclosure.reminderMark, windows: new Map(), ...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}) }
656
679
  : undefined;
@@ -665,7 +688,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
665
688
  const droppedTools = [];
666
689
  let disposing = false;
667
690
  const serverHandles = [];
668
- const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure)));
691
+ const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
669
692
  try {
670
693
  for (let i = 0; i < specs.length; i++) {
671
694
  const r = settled[i];
@@ -701,7 +724,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
701
724
  statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
702
725
  }
703
726
  }
704
- const resourceTools = buildResourceTools(resourceServers);
727
+ const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
705
728
  tools.push(...resourceTools.tools);
706
729
  toolAxes.push(...resourceTools.axes);
707
730
  }
@@ -729,10 +752,14 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
729
752
  results.push({ server: h.name, prefix: prefixOf(h.name), status: "not_connected", toolCount: 0, added: [], removed: [] });
730
753
  continue;
731
754
  }
755
+ if (isServerRevoked(h.name)) {
756
+ results.push({ server: h.name, prefix: prefixOf(h.name), status: "revoked", toolCount: 0, added: [], removed: [], error: "server revoked by the operator — the refresh did not contact it" });
757
+ continue;
758
+ }
732
759
  try {
733
760
  const listed = await listToolsLenient(h.client);
734
761
  cacheMcpToolMetadata(h.client, listed.tools);
735
- const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure);
762
+ const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
736
763
  const newNames = serverTools.map((t) => t.name);
737
764
  const added = newNames.filter((n) => !h.toolNames.includes(n));
738
765
  const removed = h.toolNames.filter((n) => !newNames.includes(n));
@@ -882,7 +909,7 @@ function renderDirChildren(server, uri, children, flags) {
882
909
  terminate: false,
883
910
  };
884
911
  }
885
- function buildResourceTools(resourceServers) {
912
+ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
886
913
  const listable = resourceServers.filter((rs) => rs.listAllowed);
887
914
  const readable = resourceServers.filter((rs) => rs.readAllowed);
888
915
  if (listable.length === 0 && readable.length === 0)
@@ -910,6 +937,11 @@ function buildResourceTools(resourceServers) {
910
937
  const all = [];
911
938
  const errors = [];
912
939
  for (const rs of targets) {
940
+ if (isServerRevoked(rs.server)) {
941
+ errors.push({ server: rs.server, error: "server revoked by the operator mid-session (request not sent)" });
942
+ sections.push(`[${rs.server}] Error: server revoked by the operator — its resources are unavailable this turn.`);
943
+ continue;
944
+ }
913
945
  if (rs.health.dead) {
914
946
  errors.push({ server: rs.server, error: "server disconnected (transport closed earlier in this task)" });
915
947
  sections.push(`[${rs.server}] Error: server disconnected — its resources are unavailable.`);
@@ -970,6 +1002,14 @@ function buildResourceTools(resourceServers) {
970
1002
  if (!rs)
971
1003
  return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with readable resources. Available: ${serverNames(readable)}.` }], details: undefined, terminate: false };
972
1004
  const what = `The read of resource ${inlineUntrusted(uri)}`;
1005
+ if (isServerRevoked(server)) {
1006
+ return {
1007
+ content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
1008
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
1009
+ terminate: false,
1010
+ isError: true,
1011
+ };
1012
+ }
973
1013
  if (rs.health.dead)
974
1014
  throwDeadServer(server, what);
975
1015
  const timeoutMs = mcpToolTimeoutMs();
@@ -1021,6 +1061,14 @@ function buildResourceTools(resourceServers) {
1021
1061
  if (!rs)
1022
1062
  return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(listable)}.` }], details: undefined, terminate: false };
1023
1063
  const what = `The directory listing of ${inlineUntrusted(uri)}`;
1064
+ if (isServerRevoked(server)) {
1065
+ return {
1066
+ content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
1067
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
1068
+ terminate: false,
1069
+ isError: true,
1070
+ };
1071
+ }
1024
1072
  if (rs.health.dead)
1025
1073
  throwDeadServer(server, what);
1026
1074
  const timeoutMs = mcpToolTimeoutMs();
@@ -1123,7 +1171,7 @@ const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient
1123
1171
  async function listToolsLenient(client, options) {
1124
1172
  return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
1125
1173
  }
1126
- async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure) {
1174
+ async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked) {
1127
1175
  const elicitOn = spec.elicitation === true && onElicit !== undefined;
1128
1176
  const health = { dead: false, pendingElicitations: 0, lastElicitationClosedAt: 0 };
1129
1177
  const client = new Client({ name: `sema-core/${spec.name}`, version: "0.1.0" }, { capabilities: elicitOn ? { elicitation: { form: {} } } : {} });
@@ -1159,7 +1207,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1159
1207
  };
1160
1208
  const listed = await listToolsLenient(client, startupOpts);
1161
1209
  cacheMcpToolMetadata(client, listed.tools);
1162
- const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure);
1210
+ const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
1163
1211
  const caps = client.getServerCapabilities();
1164
1212
  const resourceInfo = caps?.resources
1165
1213
  ? {
@@ -1192,7 +1240,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1192
1240
  throw err;
1193
1241
  }
1194
1242
  }
1195
- function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure) {
1243
+ function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
1196
1244
  const serverTools = [];
1197
1245
  const serverAxes = [];
1198
1246
  const dropped = [];
@@ -1235,6 +1283,14 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1235
1283
  ...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
1236
1284
  execute: async (_toolCallId, params, signal) => {
1237
1285
  const what = `The call to tool ${inlineUntrusted(remoteName)}`;
1286
+ if (isServerRevoked?.(spec.name) === true) {
1287
+ return {
1288
+ content: [{ type: "text", text: `The call to MCP server "${spec.name}" was refused: the server was revoked by the operator mid-session. The call was NOT sent, so the server did not execute it. The tool list updates at the next turn.` }],
1289
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server: spec.name },
1290
+ terminate: false,
1291
+ isError: true,
1292
+ };
1293
+ }
1238
1294
  if (health.dead)
1239
1295
  throwDeadServer(spec.name, what);
1240
1296
  const timeoutMs = mcpToolTimeoutMs();
@@ -221,6 +221,11 @@ export interface HoldRow {
221
221
  };
222
222
  /** Host valve verdict awaiting the next harvest ("release" commits with cause "static"). */
223
223
  resolved?: "release" | "discard";
224
+ /** The verdict's GENERATION (strictly increases on every re-resolution): the settle leg's
225
+ * consumed-verdict exit clears only the exact generation it read, so a host re-resolution
226
+ * racing a failing release attempt is never erased by the stale attempt's cleanup. Absent on
227
+ * rows resolved before the member existed (a generation-less clear matches those). */
228
+ resolvedAt?: number;
224
229
  }
225
230
  export declare function readHolds(controlDir: string): HoldRow[];
226
231
  /** ATOMIC no-replace restore of a staged file onto a plane path (adversarial round 2: an
@@ -308,6 +313,28 @@ export declare function resolveHoldRecord(controlDir: string, input: {
308
313
  requestId: string;
309
314
  now: () => number;
310
315
  }): void;
316
+ /**
317
+ * Consume a standing valve verdict WITHOUT any other transition — the settle leg's exit for a
318
+ * valve release that FAILED on an already-disposed row (an expired hold whose release hit a
319
+ * judgment refusal: lost custody, re-scan refusal, CAS conflict). `disposeHold` is a no-op there
320
+ * (the row is already terminal), so leaving `resolved` standing would re-run the identical failing
321
+ * release — and re-announce a disposal that never happened — on every subsequent harvest, forever.
322
+ * Clearing it returns the row to its plain disposed/expired state: the failure is disclosed by the
323
+ * caller, custody/quarantine bytes stay addressable, and `resolveHold(holdId, "release")` remains
324
+ * admissible after the host reconciles. GENERATION-KEYED: only the exact verdict generation the
325
+ * caller read is cleared — a host re-resolution that landed while the failing attempt was in
326
+ * flight is a NEWER generation and stands (the stale attempt's cleanup must not erase a live host
327
+ * action). A generation-less verdict (a row written before the member existed) is consumable
328
+ * against a generation-less read — every CURRENT writer mints a generation, so the only verdict
329
+ * such a clear can erase is one written by a PRE-generation writer racing on the same control
330
+ * plane: the standing mixed-version-writer class the upgrade-order duty prices, kept consumable
331
+ * on purpose (refusing it would revive the forever-re-firing valve loop for every verdict minted
332
+ * before the upgrade). Idempotent; unknown/verdict-less/other-generation rows are a no-op.
333
+ */
334
+ export declare function clearHoldResolution(controlDir: string, input: {
335
+ holdId: string;
336
+ ifResolvedAt: number | undefined;
337
+ }): void;
311
338
  /** Read one hold's custody bytes (release leg + expired-release valve). Returns undefined when
312
339
  * the custody file is gone or fails its digest — the caller records capture_lost, never commits
313
340
  * unverified bytes. For an expired-released row the bytes may already sit in quarantine
@@ -346,6 +346,7 @@ function coerceHolds(raw) {
346
346
  !HOLD_STATUSES.has(row.status) ||
347
347
  (row.disposition !== undefined && (typeof row.disposition !== "object" || row.disposition === null || !HOLD_TERMINALS.has(row.disposition.terminal) || (row.disposition.quarantineName !== undefined && !safeBasename(row.disposition.quarantineName)))) ||
348
348
  (row.resolved !== undefined && row.resolved !== "release" && row.resolved !== "discard") ||
349
+ (row.resolvedAt !== undefined && (typeof row.resolvedAt !== "number" || !Number.isSafeInteger(row.resolvedAt) || row.resolvedAt < 0 || row.resolvedAt >= 2 ** 50 || row.resolved === undefined)) ||
349
350
  (row.entryId !== undefined && typeof row.entryId !== "string") ||
350
351
  (row.baseRev !== undefined && typeof row.baseRev !== "string") ||
351
352
  (row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
@@ -401,6 +402,14 @@ export function openInstructionHold(controlDir, input) {
401
402
  const digest = sha256(buf);
402
403
  const captureName = `${holdId}.md`;
403
404
  const holdDir = join(controlDir, HOLD_DIR);
405
+ const refuse = (reason, terminal) => {
406
+ try {
407
+ disposeHold(controlDir, { holdId, terminal, now: input.now });
408
+ }
409
+ catch {
410
+ }
411
+ return { ok: false, holdId, reason };
412
+ };
404
413
  try {
405
414
  lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
406
415
  if (rec.rows.some((r) => r.holdId === holdId))
@@ -431,10 +440,10 @@ export function openInstructionHold(controlDir, input) {
431
440
  writeFileSync(join(holdDir, captureName), buf, { flag: "wx" });
432
441
  const back = readFileSync(join(holdDir, captureName));
433
442
  if (sha256(back) !== digest)
434
- return { ok: false, holdId, reason: "hold capture verification failed (written bytes do not hash to the captured digest)" };
443
+ return refuse("hold capture verification failed (written bytes do not hash to the captured digest)", "capture_lost");
435
444
  }
436
445
  catch (err) {
437
- return { ok: false, holdId, reason: `hold capture failed: ${err instanceof Error ? err.message : String(err)}` };
446
+ return refuse(`hold capture failed: ${err instanceof Error ? err.message : String(err)}`, "capture_lost");
438
447
  }
439
448
  const stagingName = uniqueStagingName(holdId, input.now);
440
449
  const stagingPath = join(holdDir, stagingName);
@@ -447,7 +456,7 @@ export function openInstructionHold(controlDir, input) {
447
456
  if (code === "ENOENT") {
448
457
  }
449
458
  else {
450
- return { ok: false, holdId, reason: `hold removal failed (${code ?? "rename error"}): the plane file was not moved` };
459
+ return refuse(`hold removal failed (${code ?? "rename error"}): the plane file was not moved`, "discarded");
451
460
  }
452
461
  }
453
462
  if (existsSync(stagingPath)) {
@@ -475,7 +484,7 @@ export function openInstructionHold(controlDir, input) {
475
484
  });
476
485
  }
477
486
  catch (err) {
478
- return { ok: false, holdId, reason: `hold ledger flip refused: ${err instanceof Error ? err.message : String(err)}` };
487
+ return refuse(`hold ledger flip refused: ${err instanceof Error ? err.message : String(err)}`, "discarded");
479
488
  }
480
489
  return { ok: true, holdId, ...(thirdWriterStranded !== undefined ? { thirdWriterStranded } : {}) };
481
490
  }
@@ -604,6 +613,7 @@ export function markHoldReleased(controlDir, input) {
604
613
  return { result: undefined };
605
614
  r.status = "released";
606
615
  delete r.resolved;
616
+ delete r.resolvedAt;
607
617
  return { next: rec, result: r.captureName };
608
618
  });
609
619
  if (captureName === undefined)
@@ -638,6 +648,23 @@ export function resolveHoldRecord(controlDir, input) {
638
648
  throw e;
639
649
  }
640
650
  r.resolved = input.action;
651
+ const nextGeneration = Math.max(input.now(), 0, (r.resolvedAt ?? 0) + 1);
652
+ if (!Number.isSafeInteger(nextGeneration) || nextGeneration < 0 || nextGeneration >= 2 ** 50) {
653
+ const e = new Error(`resolveHold: hold ${JSON.stringify(input.holdId)} cannot mint a fresh verdict generation (${String(nextGeneration)} — an exhausted window, a deranged clock, or a corrupt ledger row) — refused, never written.`);
654
+ e.code = "memory.hold_resolve_invalid_state";
655
+ throw e;
656
+ }
657
+ r.resolvedAt = nextGeneration;
658
+ return { next: rec, result: undefined };
659
+ });
660
+ }
661
+ export function clearHoldResolution(controlDir, input) {
662
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
663
+ const r = rec.rows.find((x) => x.holdId === input.holdId);
664
+ if (r === undefined || r.resolved === undefined || r.resolvedAt !== input.ifResolvedAt)
665
+ return { result: undefined };
666
+ delete r.resolved;
667
+ delete r.resolvedAt;
641
668
  return { next: rec, result: undefined };
642
669
  });
643
670
  }
@@ -75,5 +75,16 @@ export function mergeHarvestReports(a, b) {
75
75
  const qf = [...(a.quarantineFailures ?? []), ...(b.quarantineFailures ?? [])];
76
76
  if (qf.length > 0)
77
77
  merged.quarantineFailures = qf;
78
+ const ac = a.containment;
79
+ const bc = b.containment;
80
+ if (ac !== undefined || bc !== undefined) {
81
+ merged.containment = {
82
+ indexRolledBack: ac?.indexRolledBack === true || bc?.indexRolledBack === true,
83
+ quarantinedInstruction: [...(ac?.quarantinedInstruction ?? []), ...(bc?.quarantinedInstruction ?? [])],
84
+ heldInstruction: [...(ac?.heldInstruction ?? []), ...(bc?.heldInstruction ?? [])],
85
+ releasedHolds: [...(ac?.releasedHolds ?? []), ...(bc?.releasedHolds ?? [])],
86
+ disposedHolds: [...(ac?.disposedHolds ?? []), ...(bc?.disposedHolds ?? [])],
87
+ };
88
+ }
78
89
  return merged;
79
90
  }
@@ -803,7 +803,12 @@ export declare class MemoryEngine {
803
803
  * and a re-call RESUMES it idempotently. Refusals (#123 coded, caller-shape):
804
804
  * unattributed/invalid input, unknown/unmarked entry, and a CHALLENGED entry — the clear valve
805
805
  * is not a challenge exit (adjudicate the challenge first; clearing a challenged entry would
806
- * launder the exclusion through the weaker credential).
806
+ * launder the exclusion through the weaker credential); the challenged refusal re-judges on the
807
+ * RESUME path too while the marked entry stands. Two effect-half refusals (terminal, both
808
+ * states untouched): an on-disk projection diverging from the committed state (the clear never
809
+ * overwrites unadopted plane bytes — adopt/reconcile first), and a resumed row whose entry is
810
+ * ABSENT without a recorded committed tombstone (that absence is an independent deletion the
811
+ * clear never resurrects; the custody bytes stay disclosed on the terminal row).
807
812
  */
808
813
  clearEntryOrigin(entryId: string, input: {
809
814
  requestId: string;
@@ -844,6 +849,11 @@ export declare class MemoryEngine {
844
849
  * the deleted disk file WAS the backend's storage). Zero-copy skips getByIds: its read-side scan
845
850
  * cannot see a deleted file, and calling it mid-harvest would sync-adopt in-session edits. */
846
851
  private committedContentFor;
852
+ /** {@link committedContentFor} with the FAULT axis preserved: `fault: true` means the backend
853
+ * read THREW and no shadow answered — "could not read" — which consumers that make destructive
854
+ * decisions on absence (the projection-debt settlement clears a row on "entry gone") must
855
+ * distinguish from a clean miss. The 8 non-destructive read sites keep the folded face. */
856
+ private committedContentOrFault;
847
857
  /**
848
858
  * design/336 §2.2 (r4-9) — the COMMITTED frontmatter an origin carry-forward is computed against.
849
859
  * Deliberately NOT {@link committedContentFor}'s non-zero-copy leg: that one calls the backend's
@@ -905,8 +915,32 @@ export declare class MemoryEngine {
905
915
  /** Persist the canonical projection back to the session file (id minting / frontmatter completion).
906
916
  * C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
907
917
  * commit point. Skips the write when the disk already holds the canonical bytes (zero-copy: the
908
- * journal's own execute step wrote them, making this an idempotent no-op). */
918
+ * journal's own execute step wrote them, making this an idempotent no-op). Returns whether the
919
+ * disk now holds the canonical bytes — the ordinary completion family stays best-effort on a
920
+ * `false` (the next materialize re-projects), but the id-COMPLETION family's caller reads it to
921
+ * keep the #366 projection-debt row standing (an id-less plane file must re-bind to its
922
+ * committed id at the next harvest, never re-admit as a duplicate). */
909
923
  private writeBackProjection;
924
+ /**
925
+ * #366 — repair an id-less-but-committed seat IN PLACE (the failed write-back's retry): write the
926
+ * committed canonical bytes over the seat and settle the debt row on success. Callers have
927
+ * already proven the seat carries no unadmitted edit (rev equality against its baseline), so the
928
+ * overwrite is content-preserving — it restores the id line, the completed frontmatter and any
929
+ * carried marker, nothing else. A refused read/write leaves the row standing (retried next pass;
930
+ * the next materialize's projection loop repairs and settles it too).
931
+ */
932
+ private repairProjectionSeat;
933
+ /**
934
+ * #366 — validate a projection-debt row against the COMMITTED state (side-effect-free read: the
935
+ * retrievalView face in copy-out, never the File backend's adopting `getByIds`). Three-valued on
936
+ * purpose: `valid` binds, `stale` clears the row (the seat claim dissolved — entry gone, moved or
937
+ * renamed), and `unknown` (committed state unreadable) makes the caller DEFER the file fail-closed
938
+ * — treating a transient read fault as "stale" would clear the row and mint the very duplicate
939
+ * the account exists to close. Zero-copy answers `stale` by construction: there the backend's own
940
+ * commit writes the id into the plane file, so any standing row is a leftover, and the plain
941
+ * `getByIds` there would sync-adopt mid-harvest.
942
+ */
943
+ private debtCommittedProjection;
910
944
  /** Sibling scope subdir names under `dir` (excluded from a scope-tree walk when `dir` is the root —
911
945
  * a root-owning layer's chmod/restore must never touch another scope's home). */
912
946
  private siblingScopeDirNames;