@sema-agent/core 5.35.0 → 5.37.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 (51) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/subagent.d.ts +10 -0
  3. package/dist/agents/subagent.js +29 -2
  4. package/dist/core/auto-compaction.d.ts +23 -0
  5. package/dist/core/auto-compaction.js +8 -0
  6. package/dist/core/checkpoint-store.d.ts +16 -0
  7. package/dist/core/context-guard.d.ts +41 -0
  8. package/dist/core/context-guard.js +76 -0
  9. package/dist/core/governance-codes.js +4 -0
  10. package/dist/core/memory-engine/engine.d.ts +142 -0
  11. package/dist/core/memory-engine/engine.js +265 -3
  12. package/dist/core/memory-engine/file-backend.d.ts +490 -16
  13. package/dist/core/memory-engine/file-backend.js +1099 -36
  14. package/dist/core/memory-engine/index.d.ts +2 -2
  15. package/dist/core/memory-engine/index.js +1 -1
  16. package/dist/core/memory-engine/layout.d.ts +42 -2
  17. package/dist/core/memory-engine/layout.js +76 -12
  18. package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
  19. package/dist/core/memory-engine/memory-backend-contract.js +89 -0
  20. package/dist/core/park-selfcheck.d.ts +5 -0
  21. package/dist/core/protocol-table.d.ts +4 -4
  22. package/dist/core/runner/assemble-result.d.ts +8 -0
  23. package/dist/core/runner/assemble-result.js +4 -1
  24. package/dist/core/runner/git-status-frame.d.ts +219 -0
  25. package/dist/core/runner/git-status-frame.js +212 -0
  26. package/dist/core/runner/prepare-memory.d.ts +11 -1
  27. package/dist/core/runner/prepare-memory.js +48 -2
  28. package/dist/core/runner/prepare-task.d.ts +21 -0
  29. package/dist/core/runner/prepare-task.js +28 -35
  30. package/dist/core/runner/runtask.js +270 -5
  31. package/dist/core/task-registry-agent.d.ts +15 -0
  32. package/dist/core/task-registry-agent.js +9 -0
  33. package/dist/core/task-registry.d.ts +3 -0
  34. package/dist/core/task-registry.js +4 -1
  35. package/dist/core/types.d.ts +122 -7
  36. package/dist/engine/harness/types.d.ts +65 -1
  37. package/dist/engine/harness/types.js +20 -0
  38. package/dist/engine/session/import-validate.js +10 -1
  39. package/dist/engine/session/session.d.ts +37 -1
  40. package/dist/engine/session/session.js +56 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +1 -1
  43. package/dist/internal/harness-types.d.ts +1 -0
  44. package/dist/internal/harness.d.ts +2 -0
  45. package/dist/internal/harness.js +2 -0
  46. package/dist/prompt-assembly/epoch.js +1 -1
  47. package/dist/prompt-assembly/event-registry.js +1 -0
  48. package/dist/prompts/default.d.ts +20 -7
  49. package/dist/prompts/default.js +2 -7
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +13 -1
@@ -1,7 +1,7 @@
1
- export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
1
+ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, } from "./engine.js";
2
2
  export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
3
3
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
4
- export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
4
+ export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, } from "./file-backend.js";
5
5
  export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
6
6
  export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
7
7
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
@@ -1,7 +1,7 @@
1
1
  export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
2
2
  export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
3
3
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
4
- export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
4
+ export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, erasureSelectHash, } from "./file-backend.js";
5
5
  export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
6
6
  export { readV2HeaderHints } from "./header-hints.js";
7
7
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
@@ -380,15 +380,55 @@ export declare function reconcileLineage(controlDir: string, now: () => number):
380
380
  * refuse — "the account is not settled, the content does not go on the table". Throws on a
381
381
  * corrupt ledger (integrity unknown ≠ no latch). */
382
382
  export declare function lineageLatchedIds(controlDir: string): Set<string>;
383
- /** Every committed contribution of `sessionId` (the pollution-retroaction sweep input). Throws on
384
- * a corrupt ledger. */
383
+ /** Every committed contribution of `sessionId` (the pollution-retroaction sweep input, and the
384
+ * erasure lane's sessionId-selector resolver). Throws on a corrupt ledger. Lookups are
385
+ * OWN-property (the twin law {@link lineageAccountOfEntry} states): a prototype-member sessionId
386
+ * such as "constructor" indexes truthy on EVERY plain object, so a bare lookup would attribute
387
+ * the WHOLE committed set to it — behind the erasure selector that phantom is a whole-store
388
+ * delete, the exact shape the selector type forbids. Belt over the null-proto parse rebuild:
389
+ * this guard stays honest even against a map minted elsewhere. */
385
390
  export declare function lineageContributionsOfSession(controlDir: string, sessionId: string): Array<{
386
391
  entryId: string;
387
392
  lastRev: string;
388
393
  }>;
394
+ /** design/178 v2-a §1.3 — the BY-ENTRY lineage account, from ONE parse of the ledger (the
395
+ * by-session read's transpose; the committed set is keyed by entryId, so the index is O(1) after
396
+ * the load — the load itself still parses and validates the whole account). BOTH halves come from
397
+ * the same read on purpose (adversarial r3): contributors (committed rows; pending rows are NOT
398
+ * contributions — the account is unsettled) and the dirty-latch fact (any pending row naming this
399
+ * id). Deriving them from two separate reads let a promotion land between them and assemble a
400
+ * scene no instant exhibited — empty ancestry AND no latch. Unknown entry ⇒ empty/false. Throws
401
+ * {@link ControlPlaneCorruptError} on a corrupt ledger — an audit answer is never assembled over
402
+ * an account whose integrity is unknown. Lookups are own-property (a prototype-member entryId
403
+ * such as "constructor" must answer the empty account, never a phantom). */
404
+ export declare function lineageAccountOfEntry(controlDir: string, entryId: string): {
405
+ contributors: Array<{
406
+ sessionId: string;
407
+ lastRev: string;
408
+ lastAt: number;
409
+ }>;
410
+ pendingLatched: boolean;
411
+ };
389
412
  /** Drop an entry's lineage rows once the entry is DELETED (an applied tombstone) — space hygiene
390
413
  * only, taken at the harvest that saw the applied delete (design/180 §10). */
391
414
  export declare function clearLineageForEntries(controlDir: string, entryIds: readonly string[]): void;
415
+ /**
416
+ * design/178 v2-b §7.2-3 — capture-and-clear an erased id set's lineage in ONE strict-lock write:
417
+ * - the COMMITTED contributors of each id are captured (the attestation's testimony) and the rows
418
+ * dropped (the organic tombstone's space hygiene, same terminal state);
419
+ * - every PENDING txn's staged rows naming these ids are STRUCK in the same write — a promotion
420
+ * landing after this lock releases iterates rows the strike already removed, so a late harvest
421
+ * can never write a deleted id's lineage row back (the read and the clear being two separate
422
+ * lock spans was exactly the interleave that both missed a landing contributor and then erased
423
+ * the record of having missed it).
424
+ * Struck pending rows contribute their txn's sessionId to the returned set — EXCEPT rows carrying
425
+ * the structural `kind: "latch-only"` seat (an import protocol's synthetic latch is not a
426
+ * contributing session; the row is still struck). Returns id → contributing session ids
427
+ * (committed ∪ struck). NON-durable by design: a crash between the caller's journal close and this
428
+ * call loses the return set — the caller's replay re-runs it idempotently and its attestation
429
+ * carries the chain's pre-captured set (honest downgrade, never a fabricated rebuild).
430
+ */
431
+ export declare function captureAndClearLineageForEntries(controlDir: string, entryIds: readonly string[]): Map<string, string[]>;
392
432
  /** Full-ledger read (tests / host observability). Throws on corruption. */
393
433
  export declare function readLineageRecord(controlDir: string): {
394
434
  committed: Record<string, Record<string, LineageContribution>>;
@@ -375,7 +375,8 @@ export function claimRootScope(controlDir, scope) {
375
375
  }
376
376
  export function registerScope(memoryDir, controlDir, scope, opts) {
377
377
  return lockedScopesUpdate(controlDir, (rec) => {
378
- const dirName = rec.rootScope === scope ? "" : (rec.scopes?.[scope] ?? scopeDirName(scope));
378
+ const registered = rec.scopes !== undefined && Object.prototype.hasOwnProperty.call(rec.scopes, scope) ? rec.scopes[scope] : undefined;
379
+ const dirName = rec.rootScope === scope ? "" : (registered ?? scopeDirName(scope));
379
380
  for (const [other, otherDir] of Object.entries(rec.scopes ?? {})) {
380
381
  if (other === scope || otherDir === "")
381
382
  continue;
@@ -390,7 +391,7 @@ export function registerScope(memoryDir, controlDir, scope, opts) {
390
391
  }
391
392
  }
392
393
  }
393
- if (rec.scopes?.[scope] === undefined) {
394
+ if (registered === undefined) {
394
395
  return { next: { ...rec, scopes: { ...rec.scopes, [scope]: dirName } }, result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
395
396
  }
396
397
  return { result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
@@ -403,7 +404,7 @@ export function scopeDirFor(memoryDir, controlDir, scope) {
403
404
  const rec = readScopesRecord(controlDir);
404
405
  if (rec.rootScope === scope)
405
406
  return memoryDir;
406
- const registered = rec.scopes?.[scope];
407
+ const registered = rec.scopes !== undefined && Object.prototype.hasOwnProperty.call(rec.scopes, scope) ? rec.scopes[scope] : undefined;
407
408
  if (registered !== undefined)
408
409
  return registered === "" ? memoryDir : join(memoryDir, registered);
409
410
  return join(memoryDir, scopeDirName(scope));
@@ -769,8 +770,8 @@ export const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
769
770
  export const USAGE_RETRIEVED_MAX_IDS = 4096;
770
771
  function coerceRetrievedAccount(raw) {
771
772
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
772
- return {};
773
- const out = {};
773
+ return Object.create(null);
774
+ const out = Object.create(null);
774
775
  for (const [k, v] of Object.entries(raw)) {
775
776
  const row = v;
776
777
  if (row && typeof row === "object" && typeof row.count === "number" && Number.isFinite(row.count) && row.count > 0 && typeof row.lastAt === "number" && Number.isFinite(row.lastAt)) {
@@ -903,8 +904,9 @@ function readStrictSidecar(controlDir, fileName, what) {
903
904
  return readStrictSidecarRaw(file, what);
904
905
  }
905
906
  function coerceLineage(raw) {
906
- if (raw === undefined)
907
- return { version: 1, committed: {}, pending: {} };
907
+ if (raw === undefined) {
908
+ return { version: 1, committed: Object.create(null), pending: Object.create(null) };
909
+ }
908
910
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
909
911
  throw new ControlPlaneCorruptError("memory lineage ledger has the wrong shape");
910
912
  const r = raw;
@@ -942,7 +944,17 @@ function coerceLineage(raw) {
942
944
  }
943
945
  }
944
946
  }
945
- return raw;
947
+ const committed = Object.create(null);
948
+ for (const [entryId, sessions] of Object.entries(r.committed)) {
949
+ const s = Object.create(null);
950
+ for (const [sid, c] of Object.entries(sessions))
951
+ s[sid] = c;
952
+ committed[entryId] = s;
953
+ }
954
+ const pending = Object.create(null);
955
+ for (const [txnId, t] of Object.entries(r.pending))
956
+ pending[txnId] = t;
957
+ return { version: 1, committed, pending };
946
958
  }
947
959
  export function stageLineagePending(controlDir, txnId, sessionId, rows, now) {
948
960
  lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
@@ -981,7 +993,7 @@ export function promoteLineagePending(controlDir, txnId, now) {
981
993
  for (const row of txn.rows) {
982
994
  if (!applied.has(row.entryId))
983
995
  continue;
984
- const sessions = (rec.committed[row.entryId] ??= {});
996
+ const sessions = (rec.committed[row.entryId] ??= Object.create(null));
985
997
  sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
986
998
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
987
999
  }
@@ -1004,7 +1016,7 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
1004
1016
  for (const row of txn.rows) {
1005
1017
  if (applied !== undefined && !applied.has(row.entryId))
1006
1018
  continue;
1007
- const sessions = (rec.committed[row.entryId] ??= {});
1019
+ const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1008
1020
  sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
1009
1021
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
1010
1022
  }
@@ -1028,7 +1040,7 @@ export function reconcileLineage(controlDir, now) {
1028
1040
  if (!applied.has(row.entryId))
1029
1041
  continue;
1030
1042
  at ??= now();
1031
- const sessions = (rec.committed[row.entryId] ??= {});
1043
+ const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1032
1044
  sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
1033
1045
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
1034
1046
  }
@@ -1050,12 +1062,25 @@ export function lineageContributionsOfSession(controlDir, sessionId) {
1050
1062
  const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
1051
1063
  const out = [];
1052
1064
  for (const [entryId, sessions] of Object.entries(rec.committed)) {
1053
- const c = sessions[sessionId];
1065
+ const c = Object.prototype.hasOwnProperty.call(sessions, sessionId) ? sessions[sessionId] : undefined;
1054
1066
  if (c !== undefined)
1055
1067
  out.push({ entryId, lastRev: c.lastRev });
1056
1068
  }
1057
1069
  return out;
1058
1070
  }
1071
+ export function lineageAccountOfEntry(controlDir, entryId) {
1072
+ const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
1073
+ const sessions = Object.prototype.hasOwnProperty.call(rec.committed, entryId) ? rec.committed[entryId] : undefined;
1074
+ const contributors = sessions === undefined ? [] : Object.entries(sessions).map(([sessionId, c]) => ({ sessionId, lastRev: c.lastRev, lastAt: c.lastAt }));
1075
+ let pendingLatched = false;
1076
+ for (const txn of Object.values(rec.pending)) {
1077
+ if (txn.rows.some((r) => r.entryId === entryId)) {
1078
+ pendingLatched = true;
1079
+ break;
1080
+ }
1081
+ }
1082
+ return { contributors, pendingLatched };
1083
+ }
1059
1084
  export function clearLineageForEntries(controlDir, entryIds) {
1060
1085
  if (entryIds.length === 0)
1061
1086
  return;
@@ -1070,6 +1095,45 @@ export function clearLineageForEntries(controlDir, entryIds) {
1070
1095
  return { ...(changed ? { next: rec } : {}), result: undefined };
1071
1096
  });
1072
1097
  }
1098
+ export function captureAndClearLineageForEntries(controlDir, entryIds) {
1099
+ if (entryIds.length === 0)
1100
+ return new Map();
1101
+ return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
1102
+ const want = new Set(entryIds);
1103
+ const captured = new Map();
1104
+ const add = (id, sessionId) => {
1105
+ const set = captured.get(id) ?? new Set();
1106
+ set.add(sessionId);
1107
+ captured.set(id, set);
1108
+ };
1109
+ let changed = false;
1110
+ for (const id of entryIds) {
1111
+ if (!Object.prototype.hasOwnProperty.call(rec.committed, id))
1112
+ continue;
1113
+ for (const sessionId of Object.keys(rec.committed[id] ?? {}))
1114
+ add(id, sessionId);
1115
+ delete rec.committed[id];
1116
+ changed = true;
1117
+ }
1118
+ for (const txn of Object.values(rec.pending)) {
1119
+ const keep = [];
1120
+ for (const row of txn.rows) {
1121
+ if (!want.has(row.entryId)) {
1122
+ keep.push(row);
1123
+ continue;
1124
+ }
1125
+ changed = true;
1126
+ if (row.kind !== "latch-only")
1127
+ add(row.entryId, txn.sessionId);
1128
+ }
1129
+ txn.rows = keep;
1130
+ }
1131
+ const result = new Map();
1132
+ for (const [id, sessions] of captured)
1133
+ result.set(id, [...sessions].sort());
1134
+ return { ...(changed ? { next: rec } : {}), result };
1135
+ });
1136
+ }
1073
1137
  export function readLineageRecord(controlDir) {
1074
1138
  const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
1075
1139
  return { committed: rec.committed, pending: rec.pending };
@@ -12,6 +12,19 @@ export interface MemoryBackendContractHooks {
12
12
  skipCrossInstanceCas?: string;
13
13
  /** Wrap each case (vitest `it`, sequential SQL chaining, …). Default: run sequentially. */
14
14
  runAssertion?: (name: string, fn: () => Promise<void>) => void | Promise<void>;
15
+ /**
16
+ * design/178 v2-a §1.3 — the OPTIONAL audit-face clauses' report seat (#196
17
+ * `onOptionalMember` posture: absence is REPORTED, never a silent green — a run without this
18
+ * hook still passes on an absent face, but a certifying harness that wires it can tell
19
+ * "verified" from "the store never offered the face"). The three faces are capability probes on
20
+ * the concrete class (`committedSnapshotOf` / `committedSnapshotsOfScopes` / `custodyOf`); the
21
+ * `MemoryBackend` interface itself is untouched, so absence is legal — the engine's provenance
22
+ * assembly answers it as capability-absent.
23
+ */
24
+ onOptionalMember?: (report: {
25
+ member: "committedSnapshotOf" | "committedSnapshotsOfScopes" | "custodyOf" | "eraseWithEvidence";
26
+ status: "verified" | "absent";
27
+ }) => void;
15
28
  }
16
29
  /** Run the full conformance suite. Throws (assert) on the first violated contract clause. */
17
30
  export declare function memoryBackendContract(hooks: MemoryBackendContractHooks): Promise<void>;
@@ -295,6 +295,95 @@ export async function memoryBackendContract(hooks) {
295
295
  assert.strictEqual(await b.getConsolidationCursor("s1"), "c-2");
296
296
  assert.strictEqual(await b.getConsolidationCursor("s2"), "c-other");
297
297
  });
298
+ defer("design/178 v2-a optional audit faces: committed snapshot / scope enumeration / custody (absent ⇒ reported)", async () => {
299
+ const b = await hooks.make();
300
+ const seats = [
301
+ ["committedSnapshotOf", b.committedSnapshotOf],
302
+ ["committedSnapshotsOfScopes", b.committedSnapshotsOfScopes],
303
+ ["custodyOf", b.custodyOf],
304
+ ];
305
+ for (const [member, seat] of seats) {
306
+ assert.ok(seat === undefined || typeof seat === "function", `${member} must be undefined (absent) or a function — a non-function occupant is a defect, not absence`);
307
+ }
308
+ const snapshotOf = typeof seats[0]?.[1] === "function" ? seats[0][1] : undefined;
309
+ const snapshotsOfScopes = typeof seats[1]?.[1] === "function" ? seats[1][1] : undefined;
310
+ const custodyOf = typeof seats[2]?.[1] === "function" ? seats[2][1] : undefined;
311
+ const e = entry("id-audit-0001", "s1", "audited", "audited body", { name: "Audited" });
312
+ await b.applyPatches([{ op: "add", id: e.id, entry: e }]);
313
+ if (typeof snapshotOf !== "function") {
314
+ hooks.onOptionalMember?.({ member: "committedSnapshotOf", status: "absent" });
315
+ }
316
+ else {
317
+ const ghost = await snapshotOf.call(b, "id-ghost-9999");
318
+ assert.strictEqual(ghost.state, "absent", "an unknown id answers state 'absent' — never a throw, never a guess");
319
+ const snap = await snapshotOf.call(b, e.id);
320
+ assert.strictEqual(snap.state, "row");
321
+ if (snap.state === "row") {
322
+ assert.strictEqual(snap.rev, e.rev, "the snapshot's rev is the committed rev");
323
+ assert.strictEqual(snap.binding.state, "bound", "a committed add answers a bound row");
324
+ if (snap.binding.state === "bound") {
325
+ assert.strictEqual(snap.binding.scope, "s1");
326
+ assert.strictEqual(snap.binding.slug, "audited");
327
+ }
328
+ assert.strictEqual(snap.content.state, "present", "a committed, in-place entry's content is servable");
329
+ if (snap.content.state === "present")
330
+ assert.strictEqual(snap.content.entry.rev, e.rev);
331
+ }
332
+ hooks.onOptionalMember?.({ member: "committedSnapshotOf", status: "verified" });
333
+ }
334
+ if (typeof snapshotsOfScopes !== "function") {
335
+ hooks.onOptionalMember?.({ member: "committedSnapshotsOfScopes", status: "absent" });
336
+ }
337
+ else {
338
+ const enumerated = await snapshotsOfScopes.call(b, ["s1"]);
339
+ if (!enumerated.complete)
340
+ assert.fail("a quiet store enumerates complete");
341
+ assert.deepStrictEqual(enumerated.rows.map((r) => r.id), [e.id], "the account-driven enumeration answers the committed row");
342
+ const foreign = await snapshotsOfScopes.call(b, ["s-elsewhere"]);
343
+ if (!foreign.complete)
344
+ assert.fail("a quiet store enumerates complete (foreign scope)");
345
+ assert.deepStrictEqual(foreign.rows, [], "a foreign scope enumerates empty, not the whole store");
346
+ hooks.onOptionalMember?.({ member: "committedSnapshotsOfScopes", status: "verified" });
347
+ }
348
+ if (typeof custodyOf !== "function") {
349
+ hooks.onOptionalMember?.({ member: "custodyOf", status: "absent" });
350
+ }
351
+ else {
352
+ const custody = await custodyOf.call(b, e.id);
353
+ assert.ok(custody.state === "complete" || custody.state === "damaged", "custody state is the two-valued store answer");
354
+ assert.ok(Array.isArray(custody.events));
355
+ for (const ev of custody.events) {
356
+ assert.strictEqual(typeof ev.ev, "string");
357
+ assert.strictEqual(typeof ev.channel, "string");
358
+ assert.strictEqual(typeof ev.at, "number");
359
+ }
360
+ hooks.onOptionalMember?.({ member: "custodyOf", status: "verified" });
361
+ }
362
+ });
363
+ defer("design/178 v2-b optional erasure face: evidenced delete / replay pinning / selector-swap refusal (absent ⇒ reported)", async () => {
364
+ const b = await hooks.make();
365
+ const seat = b.eraseWithEvidence;
366
+ assert.ok(seat === undefined || typeof seat === "function", "eraseWithEvidence must be undefined (absent) or a function — a non-function occupant is a defect, not absence");
367
+ if (typeof seat !== "function") {
368
+ hooks.onOptionalMember?.({ member: "eraseWithEvidence", status: "absent" });
369
+ return;
370
+ }
371
+ const erase = seat;
372
+ const e = entry("id-erase-0001", "s1", "erasable", "erasable body", { name: "Erasable" });
373
+ await b.applyPatches([{ op: "add", id: e.id, entry: e }]);
374
+ const att = await erase.call(b, { requestId: "req-contract-1", select: { ids: [e.id, "id-ghost-8888"] } });
375
+ assert.strictEqual(att.v, 1, "the attestation rides the v:1 envelope");
376
+ assert.strictEqual(att.evidenceCapability, "journal", "a face-claiming backend answers the evidenced capability");
377
+ assert.deepStrictEqual(att.erased.map((r) => r.id), [e.id], "the existing id is erased");
378
+ assert.ok(typeof att.erased[0]?.evidenceEv === "string" && att.erased[0].evidenceEv.length > 0, "an evidenced erase names its evidence row");
379
+ assert.deepStrictEqual(att.notFound.map((r) => r.id), ["id-ghost-8888"], "the unknown id answers notFound — never a throw, never a silent drop");
380
+ assert.deepStrictEqual(await b.getByIds([e.id]), [], "an erased id is gone from the read faces");
381
+ const replay = await erase.call(b, { requestId: "req-contract-1", select: { ids: [e.id, "id-ghost-8888"] } });
382
+ assert.deepStrictEqual(replay.resolvedIds, att.resolvedIds, "a replay answers the FIRST resolution's pinned set");
383
+ assert.deepStrictEqual((replay.erasedPreviously ?? []).map((r) => r.id), [e.id], "a replay claims the prior erase as erasedPreviously, never a second erase");
384
+ await assert.rejects(async () => erase.call(b, { requestId: "req-contract-1", select: { ids: ["id-other-7777"] } }), (err) => err.code === "memory.erasure_selector_mismatch", "a reused requestId with a different selector refuses loudly");
385
+ hooks.onOptionalMember?.({ member: "eraseWithEvidence", status: "verified" });
386
+ });
298
387
  if (!hooks.makeSibling) {
299
388
  const reason = hooks.skipCrossInstanceCas?.trim();
300
389
  if (reason) {
@@ -152,5 +152,10 @@ export declare function durableParkGapFor(deps: Pick<RunnerDeps, "checkpointStor
152
152
  * deployment's, deliberately — this module reports, loudly and in a closed vocabulary, and does not
153
153
  * legislate. What it will NOT do is report a pass it did not observe: `"round_trip_ok"` is returned only
154
154
  * with an empty findings list, and the invariant is asserted below rather than left to reading.
155
+ *
156
+ * This is a DIAGNOSTIC probe, not a health-poll body: every run files (and then fences) one real
157
+ * synthetic row under the reserved scope prefix. Mounted on a high-frequency health endpoint it
158
+ * becomes write amplification against the checkpoint store — run it at startup, on demand, or on a
159
+ * coarse operator cadence, not per request.
155
160
  */
156
161
  export declare function probeParkRoundTrip(deps: Pick<RunnerDeps, "checkpointStore" | "sessionStore">, spec?: Pick<TaskSpec, "checkpointStore">): Promise<ParkSelfCheckResult>;
@@ -66,10 +66,10 @@ export declare const MCP_NAMESPACE: ProtocolNamespace<typeof MCP_PREFIX_NAME>;
66
66
  /**
67
67
  * The A2A tool namespace (roadmap 2026-08-03, appended for the A2A client/server work).
68
68
  *
69
- * NAME LAYER ONLY: this entry decides how an A2A tool name is SPELLED and how it decomposes. Nothing
70
- * materializes A2A tools yet until the client leg lands, no name in this namespace can exist at runtime,
71
- * so appending the entry changes no live behavior. It exists first because every table-driven site
72
- * (`protocolOf`, the grouped display key, the `ToolOrigin` union) then needs no change when it does.
69
+ * NAME LAYER ONLY: this entry decides how an A2A tool name is SPELLED and how it decomposes. The
70
+ * client leg (`a2a.ts`, `materializeA2aTools`) mints live names in this namespace since v5.3.0; the
71
+ * table entry predates it so that every table-driven site (`protocolOf`, the grouped display key, the
72
+ * `ToolOrigin` union) needed no change when the leg landed and needs none for the server-side leg.
73
73
  */
74
74
  export declare const A2A_NAMESPACE: ProtocolNamespace<typeof A2A_PREFIX_NAME>;
75
75
  /** Every protocol namespace the engine knows. APPEND-ONLY: adding a protocol must not change how any
@@ -128,6 +128,11 @@ export interface ResultFlags {
128
128
  * spawner folds it stricter-wins into follow-on legs; every other consumer may ignore it. */
129
129
  effectiveReadFace?: TaskResult["effectiveReadFace"];
130
130
  effectiveReadDenyPatterns?: TaskResult["effectiveReadDenyPatterns"];
131
+ /** design/178 v2 §2.3 (件①) — the memory-visibility observation, echoed on
132
+ * `TaskResult.effectiveMemoryScopes`. Pure pass-through on every terminal, same law as the
133
+ * read-posture seats above: present iff prepare completed (the memory-less states are their own
134
+ * values — absence means only "prepare never ran to completion"). */
135
+ effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
131
136
  /** ruled 2026-08-04 — the usage-governance wait hint carried by the platform terminal the run adopted
132
137
  * (`undefined` for every other cause: an expiring environment has no return time to give, and a store
133
138
  * failure is not a window). Passed as data rather than read back off `threw` so the seat has a typed
@@ -149,6 +154,9 @@ export interface ResultFlags {
149
154
  budgetAxis?: BudgetAxis;
150
155
  blockedReason?: string;
151
156
  conflict?: boolean;
157
+ /** env-tail migration F5: the request-build guard threw the irreducible-core terminal this run —
158
+ * the loop folded it into a text-only failure message, so the typed code rides this flag. */
159
+ gitCoreOverBudget?: boolean;
152
160
  outputInvalid?: boolean;
153
161
  /** design/72 §2.2 (B): a re-suspend was refused because the task already suspended `maxSuspends` times
154
162
  * (a resume/restart loop). It aborted the run (no `threw`) but must read as `failed`/`suspend.loop`,
@@ -141,6 +141,9 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
141
141
  errorCode = lifted;
142
142
  errorMessage = stripErrorCodePrefix(errorMessage);
143
143
  }
144
+ else if (flags.gitCoreOverBudget) {
145
+ errorCode = "irreducible_core_over_budget";
146
+ }
144
147
  else if (flags.conflict) {
145
148
  errorCode = "conflict";
146
149
  }
@@ -159,5 +162,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
159
162
  void _internalCompaction;
160
163
  if (flags.unpricedSpend)
161
164
  delete publicStats.costMicroUsd;
162
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), stats: publicStats };
165
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
163
166
  }