@sema-agent/core 5.36.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.
@@ -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) {
@@ -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
@@ -162,5 +162,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
162
162
  void _internalCompaction;
163
163
  if (flags.unpricedSpend)
164
164
  delete publicStats.costMicroUsd;
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 } : {}), 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 };
166
166
  }
@@ -1,4 +1,4 @@
1
- import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
1
+ import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
2
  import type { Prepared } from "./prepare-task.js";
3
3
  export interface PrepareMemoryInput {
4
4
  spec: TaskSpec;
@@ -106,5 +106,15 @@ export interface PrepareMemoryResult {
106
106
  /** The session's own frozen verdict to persist at suspend — present iff this leg ADJUDICATED an
107
107
  * org plane (including adjudicated-empty); `undefined` for an org-less task. */
108
108
  ownOrgVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
109
+ /**
110
+ * design/178 v2 §2.3 (件①) — the memory-visibility OBSERVATION this phase minted for
111
+ * `TaskResult.effectiveMemoryScopes` (see {@link EffectiveMemoryScopes} for the state law).
112
+ * ALWAYS present when the phase returns: the memory-less outcomes are their own values
113
+ * (`none` / `memoryless`), never an absent seat — absence on the RESULT means only "prepare
114
+ * never completed" (this phase threw). Minted AFTER the materialize outcome (the fail-open arm
115
+ * sits between the admission verdict and the mount — an earlier stamp would report a mount that
116
+ * never happened); the deliberate-refusal `config.memory_*` throws produce no value at all.
117
+ */
118
+ effectiveMemoryScopes: EffectiveMemoryScopes;
109
119
  }
110
120
  export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;
@@ -12,12 +12,16 @@ export async function prepareMemory(input) {
12
12
  const memorySpec = normalizeMemorySpec(spec.memory);
13
13
  let admittedOrgScopes = [];
14
14
  let ownOrgVerdict;
15
+ const deploymentScopeSet = new Set(deps.deploymentMemoryScopes ?? []);
16
+ const scopeOriginsSnapshot = memorySpec?.scopeOrigins !== undefined ? { ...memorySpec.scopeOrigins } : undefined;
17
+ if (memorySpec && scopeOriginsSnapshot !== undefined)
18
+ memorySpec.scopeOrigins = scopeOriginsSnapshot;
15
19
  if (memorySpec && memorySpec.enabled && deps.memoryBackend) {
16
20
  const outcome = await admitMemoryScopes({
17
21
  memorySpec,
18
22
  principal: spec.principal,
19
23
  admission: deps.memoryScopeAdmission,
20
- deploymentScopes: new Set(deps.deploymentMemoryScopes ?? []),
24
+ deploymentScopes: deploymentScopeSet,
21
25
  orgMemoryDenied: admissionCtx.orgMemoryDenied,
22
26
  complianceDegraded: admissionCtx.complianceDegraded,
23
27
  parentAdmittedOrgScopes: admissionCtx.parentAdmittedOrgScopes,
@@ -38,6 +42,15 @@ export async function prepareMemory(input) {
38
42
  const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
39
43
  let memoryEngineSession;
40
44
  let memoryTools;
45
+ let effectiveMemoryScopes;
46
+ const originOf = (scope) => scopeOriginsSnapshot !== undefined
47
+ ? scopeOriginsSnapshot[scope] === "deployment"
48
+ ? "deployment"
49
+ : "request"
50
+ : deploymentScopeSet.has(scope)
51
+ ? "deployment"
52
+ : "request";
53
+ const materializedResidue = [];
41
54
  if (useMemoryEngine && memorySpec) {
42
55
  try {
43
56
  const backend = deps.memoryBackend;
@@ -133,6 +146,12 @@ export async function prepareMemory(input) {
133
146
  b)
134
147
  : (b.retrievalView?.() ?? b);
135
148
  const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
149
+ const mountedScopeRows = (dual
150
+ ? [
151
+ ...planeScopes(planes.project, planes.writePlane === "project" ? memorySpec.writeScope : null),
152
+ ...planeScopes(planes.personal, planes.writePlane === "personal" ? memorySpec.writeScope : null),
153
+ ]
154
+ : planeScopes(memorySpec.scopes, memorySpec.writeScope)).map((scope) => ({ scope, origin: originOf(scope) }));
136
155
  const pollutedOpts = (engine) => {
137
156
  const rec = engine.sessionPollution(sessionId);
138
157
  return rec !== undefined ? { polluted: { reason: rec.reason } } : {};
@@ -179,7 +198,9 @@ export async function prepareMemory(input) {
179
198
  const personalEngine = personal.engine;
180
199
  const p = planes;
181
200
  const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
201
+ materializedResidue.push(...planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null));
182
202
  const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
203
+ materializedResidue.push(...planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null));
183
204
  const writeIsPersonal = p.writePlane === "personal";
184
205
  writeEngine = writeIsPersonal ? personalEngine : projectEngine;
185
206
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
@@ -222,6 +243,7 @@ export async function prepareMemory(input) {
222
243
  const personal = createPersonalEngine(choosePersonalBackend());
223
244
  const personalEngine = personal.engine;
224
245
  const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
246
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
225
247
  writeEngine = personalEngine;
226
248
  writeHandle = handle;
227
249
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -243,6 +265,7 @@ export async function prepareMemory(input) {
243
265
  onIncident: onEngineIncident,
244
266
  });
245
267
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
268
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
246
269
  writeEngine = engine;
247
270
  writeHandle = handle;
248
271
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -311,6 +334,12 @@ export async function prepareMemory(input) {
311
334
  execIsExternalContent: memorySpec.execIsExternalContent === true,
312
335
  },
313
336
  };
337
+ effectiveMemoryScopes = {
338
+ state: "mounted",
339
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
340
+ scopes: mountedScopeRows,
341
+ writeScope: memorySpec.writeScope,
342
+ };
314
343
  if (input.memorySearchToolsPlanned)
315
344
  memoryTools = createMemoryEngineTools({ planes: toolPlanes });
316
345
  }
@@ -322,6 +351,16 @@ export async function prepareMemory(input) {
322
351
  throw err;
323
352
  }
324
353
  deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
354
+ if (effectiveMemoryScopes === undefined) {
355
+ effectiveMemoryScopes = {
356
+ state: "memoryless",
357
+ reason: "mount-failed",
358
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
359
+ scopes: [],
360
+ writeScope: null,
361
+ ...(materializedResidue.length > 0 ? { materializedResidue: [...new Set(materializedResidue)] } : {}),
362
+ };
363
+ }
325
364
  }
326
365
  }
327
366
  let memoryBlock;
@@ -359,5 +398,12 @@ export async function prepareMemory(input) {
359
398
  if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
360
399
  seedFiles = [injection.indexSeed];
361
400
  }
362
- return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
401
+ if (effectiveMemoryScopes === undefined) {
402
+ effectiveMemoryScopes = memorySpec
403
+ ? memorySpec.enabled
404
+ ? { state: "memoryless", reason: "no-backend", contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy", scopes: [], writeScope: null }
405
+ : { state: "none", reason: "disabled", scopes: [], writeScope: null }
406
+ : { state: "none", reason: "no-spec", scopes: [], writeScope: null };
407
+ }
408
+ return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, effectiveMemoryScopes, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
363
409
  }
@@ -205,6 +205,11 @@ export interface Prepared {
205
205
  * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
206
206
  * copy (the wide-scope working array stays the engine's own). */
207
207
  effectiveReadDenyPatterns?: readonly import("../../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
208
+ /** design/178 v2 §2.3 (件①) — the memory-visibility observation prepareMemory minted (echoed on
209
+ * `TaskResult.effectiveMemoryScopes`). Always present on a completed prepare (the memory-less
210
+ * states are their own values); the seat is optional only so a Prepared shape without the phase
211
+ * cannot fabricate one. */
212
+ effectiveMemoryScopes?: import("../types.js").EffectiveMemoryScopes;
208
213
  /** design/99 §E13 — the per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
209
214
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
210
215
  cwdRef?: CwdRef;
@@ -1555,7 +1555,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1555
1555
  const memoryPairExcludedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => (toolFaceSnapshot.exclude ?? []).includes(n));
1556
1556
  const memoryPairOccupiedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => memoryPairNameDomain.has(n));
1557
1557
  const memorySearchToolsPlanned = memoryPairExcludedName === undefined && memoryPairOccupiedName === undefined;
1558
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, } = await prepareMemory({
1558
+ const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
1559
1559
  spec,
1560
1560
  deps,
1561
1561
  sessionId,
@@ -4586,7 +4586,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4586
4586
  const effectiveReadFaceObserved = carrierReadFace();
4587
4587
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4588
4588
  const preparedHolder = {};
4589
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4589
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4590
4590
  const prepared = buildPrepared();
4591
4591
  preparedHolder.current = prepared;
4592
4592
  return prepared;
@@ -1659,6 +1659,7 @@ export class Runner {
1659
1659
  errorMessage: err instanceof Error ? err.message : String(err),
1660
1660
  errorCode: code,
1661
1661
  ...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
1662
+ ...(taskIdRef.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: taskIdRef.effectiveMemoryScopes } : {}),
1662
1663
  ...(() => {
1663
1664
  const hinted = err.retryAfterMs;
1664
1665
  return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
@@ -2029,6 +2030,8 @@ export class Runner {
2029
2030
  }, this);
2030
2031
  notificationHarness = prepared.harness;
2031
2032
  notificationSessionId = prepared.sessionId;
2033
+ if (taskIdRef)
2034
+ taskIdRef.effectiveMemoryScopes = prepared.effectiveMemoryScopes;
2032
2035
  const runSourceTaskId = spec.taskId ?? prepared.sessionId;
2033
2036
  const parentToolCallId = internals?.parentToolCallId;
2034
2037
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
@@ -3426,6 +3429,7 @@ export class Runner {
3426
3429
  remoteEnvFailures: prepared.remoteEnvFailures,
3427
3430
  effectiveReadFace: prepared.effectiveReadFace,
3428
3431
  effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
3432
+ effectiveMemoryScopes: prepared.effectiveMemoryScopes,
3429
3433
  retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
3430
3434
  abortedForTimeout: timeout.fired,
3431
3435
  abortedForTurns: rs.limits.turnsExceeded,