@sema-agent/core 7.25.1 → 7.26.1

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 (83) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/core/ask-origin.d.ts +84 -0
  3. package/dist/core/ask-origin.js +31 -3
  4. package/dist/core/auto-compaction.d.ts +5 -0
  5. package/dist/core/auto-compaction.js +7 -0
  6. package/dist/core/auto-mode-directive.d.ts +45 -0
  7. package/dist/core/auto-mode-directive.js +47 -0
  8. package/dist/core/checkpoint-store.d.ts +10 -1
  9. package/dist/core/engine-notice.d.ts +10 -0
  10. package/dist/core/gate-lanes.js +18 -10
  11. package/dist/core/governance-codes.d.ts +1 -1
  12. package/dist/core/governance-codes.js +3 -0
  13. package/dist/core/hooks.d.ts +3 -0
  14. package/dist/core/mcp-probe.d.ts +122 -0
  15. package/dist/core/mcp-probe.js +132 -0
  16. package/dist/core/memory-engine/engine.d.ts +6 -1
  17. package/dist/core/memory-engine/engine.js +30 -2
  18. package/dist/core/memory-engine/index.d.ts +1 -0
  19. package/dist/core/memory-engine/index.js +1 -0
  20. package/dist/core/memory-engine/shadowing.d.ts +58 -0
  21. package/dist/core/memory-engine/shadowing.js +40 -0
  22. package/dist/core/memory-engine/tools.d.ts +33 -1
  23. package/dist/core/memory-engine/tools.js +136 -31
  24. package/dist/core/memory-engine/types.d.ts +10 -0
  25. package/dist/core/memory.d.ts +60 -0
  26. package/dist/core/memory.js +47 -2
  27. package/dist/core/permission-rule-provider.js +6 -1
  28. package/dist/core/permission-rule-store.d.ts +87 -8
  29. package/dist/core/permission-rule-store.js +223 -76
  30. package/dist/core/permission-rule-sync.js +14 -5
  31. package/dist/core/reminder-mint.d.ts +41 -0
  32. package/dist/core/reminder-mint.js +55 -0
  33. package/dist/core/runner/compaction-restate.d.ts +9 -0
  34. package/dist/core/runner/compaction-restate.js +20 -0
  35. package/dist/core/runner/contracts.d.ts +18 -1
  36. package/dist/core/runner/denial-limit-arms.js +1 -1
  37. package/dist/core/runner/permission-rule-lanes.d.ts +5 -17
  38. package/dist/core/runner/permission-rule-lanes.js +5 -23
  39. package/dist/core/runner/prepare-ask-lane.js +2 -0
  40. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  41. package/dist/core/runner/prepare-context-lane.js +7 -2
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -2
  43. package/dist/core/runner/prepare-inherited-gate.js +6 -3
  44. package/dist/core/runner/prepare-memory.js +47 -6
  45. package/dist/core/runner/prepare-park-ask.js +1 -0
  46. package/dist/core/runner/prepare-policy-chain.js +8 -4
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +3 -1
  49. package/dist/core/runner/prepare-task.js +7 -4
  50. package/dist/core/runner/run-leg.js +9 -1
  51. package/dist/core/runner/run-recovery-lanes.js +2 -2
  52. package/dist/core/runner/run-terminal-adoption.js +2 -2
  53. package/dist/core/runner/run-turn-boundary.js +2 -2
  54. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  55. package/dist/core/runner/session-rule-policy.js +6 -7
  56. package/dist/core/session-policy-store.d.ts +18 -0
  57. package/dist/core/session-policy-store.js +3 -0
  58. package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
  59. package/dist/core/store-contracts/permission-rule-write-contract.js +90 -0
  60. package/dist/core/store-contracts/read-back-shape-contract.d.ts +56 -0
  61. package/dist/core/store-contracts/read-back-shape-contract.js +78 -0
  62. package/dist/core/task-result.d.ts +11 -0
  63. package/dist/core/task-spec.d.ts +31 -0
  64. package/dist/core/tool-catalog-entries.js +3 -3
  65. package/dist/core/tool-policy.d.ts +38 -0
  66. package/dist/core/tool-policy.js +5 -1
  67. package/dist/core/types.d.ts +1 -1
  68. package/dist/index.d.ts +11 -2
  69. package/dist/index.js +10 -1
  70. package/dist/stores/file/adoption/adopt.js +4 -10
  71. package/dist/stores/file/permission-rule-store.d.ts +5 -1
  72. package/dist/stores/file/permission-rule-store.js +52 -31
  73. package/dist/stores/file/session-policy-store.d.ts +32 -0
  74. package/dist/stores/file/session-policy-store.js +72 -50
  75. package/dist/tools/fs/shell-read-check.d.ts +58 -1
  76. package/dist/tools/fs/shell-read-check.js +47 -33
  77. package/dist/tools/fs/shell-read-establish.js +19 -8
  78. package/dist/tools/fs/shell-read-judge.d.ts +3 -0
  79. package/dist/tools/fs/shell-read-judge.js +3 -1
  80. package/package.json +1 -1
  81. package/test/export-surface.snapshot.json +79 -1
  82. package/wire-changes/core@7.26.0.json +695 -0
  83. package/wire-changes/core@7.26.1.json +605 -0
@@ -35,6 +35,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
35
35
  "memory.consolidation_refused",
36
36
  "memory.consolidation_withheld",
37
37
  "memory.restore_withheld",
38
+ "memory.project_scope_absent",
38
39
  "memory.projection_failed",
39
40
  "memory.consolidation_disabled",
40
41
  "memory.consolidation_gated",
@@ -152,6 +153,7 @@ export const ENGINE_NOTICE_CODES = [
152
153
  "memory.consolidation_refused",
153
154
  "memory.consolidation_withheld",
154
155
  "memory.restore_withheld",
156
+ "memory.project_scope_absent",
155
157
  "route.fallback_to_primary",
156
158
  "route.classifier_unroutable",
157
159
  "route.base_url_changed_key_unchanged",
@@ -229,6 +231,7 @@ const NOTICE_AUDIENCE_TABLE = {
229
231
  "memory.consolidation_incomplete": "operator",
230
232
  "memory.consolidation_refused": "operator",
231
233
  "memory.restore_withheld": "operator",
234
+ "memory.project_scope_absent": "operator",
232
235
  "route.fallback_to_primary": "operator",
233
236
  "route.classifier_unroutable": "operator",
234
237
  "route.base_url_changed_key_unchanged": "operator",
@@ -1711,6 +1711,9 @@ export interface AskCarry {
1711
1711
  /** #688 C3: the mechanism behind an `origin: "rule_store_unavailable"` (`store` / `call`), persisted on the row
1712
1712
  * so a durable card says the same thing the synchronous card does. */
1713
1713
  readonly ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1714
+ /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
1715
+ * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1716
+ readonly mandated?: true;
1714
1717
  }
1715
1718
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1716
1719
  * the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
@@ -0,0 +1,122 @@
1
+ import { type McpFailureKind } from "./mcp-failure.js";
2
+ import type { McpServerSpec, OnElicit } from "./mcp-server-spec.js";
3
+ import type { WiringManifestMcpEntry } from "./wiring-manifest.js";
4
+ /** One server's probe reading, beside its manifest row. `connectMs` is wall time this probe measured
5
+ * for THIS server's dial — the spawn/handshake, the initial `tools/list` walk and the intake, ending
6
+ * at whichever verdict the server reached (connected, failed, or the clock below) — never a number the
7
+ * server reported. `tools` are the namespaced (`mcp__<server>__<tool>`) names that WOULD mount, i.e.
8
+ * exactly `McpServerStatus.toolNames` after the schema gate and the per-server allowlist, so
9
+ * `tools.length` equals the row's `toolCount`; a server that did not connect carries `[]`.
10
+ * `errorCode` is the row's failure class, repeated here so a caller reading only this array need not
11
+ * join back. */
12
+ export interface McpProbeServerReading {
13
+ name: string;
14
+ connectMs: number;
15
+ tools: string[];
16
+ errorCode?: McpFailureKind;
17
+ }
18
+ /**
19
+ * What {@link probeMcpServers} answers.
20
+ *
21
+ * @contract mcp.probe.index_aligned — `entries` and `servers` always have the LENGTH of the `specs`
22
+ * the caller passed and are ordered by it: `entries[i]` and `servers[i]` both describe `specs[i]`, for
23
+ * every i, on every resolving call. A server that failed, that timed out, or that was never dialed
24
+ * keeps its slot (the row carries its `status`/`errorCode`, the reading its measured `connectMs`), so a
25
+ * consumer pairs its own declaration list with either array by index and never by name matching.
26
+ */
27
+ export interface McpProbeResult {
28
+ /** The rows, verbatim as a prepared leg would publish them — same projection, same failure
29
+ * vocabulary, same `liveness` snapshot, same neutralized and bounded remote text. */
30
+ entries: WiringManifestMcpEntry[];
31
+ /** The probe-only half, index-aligned with `entries` (see the contract above). */
32
+ servers: McpProbeServerReading[];
33
+ }
34
+ /** The probe's knobs. Every one of them is optional; a value outside its range is REFUSED, never
35
+ * folded to the default (a bound the caller asked for and did not get is a bound nobody is keeping). */
36
+ export interface McpProbeOptions {
37
+ /** The end-user identity a per-task principal header carries, exactly as `TaskSpec.principal` does
38
+ * on a real leg — so a server that applies per-user rules answers the probe as it would answer the
39
+ * task. */
40
+ principal?: string;
41
+ /** Per-server ceiling on the VERDICT, in whole milliseconds within `[1, MCP_ENV_MS_MAX]` (the ceiling
42
+ * every env-supplied MCP bound keeps, because a larger delay is silently rewritten to 1 ms). When it
43
+ * elapses, THAT server's row reads `failed` with `errorCode: "timeout"` and the walk moves on; nothing
44
+ * throws. It does NOT end the dial — nothing here can — so the transport stays open until the dial
45
+ * lands on its own; see the residue note on {@link probeMcpServers}.
46
+ *
47
+ * Default: the engine's own startup-phase bound ({@link mcpStartupTimeoutMs}, env `MCP_TIMEOUT`) when a
48
+ * deployment set one; absent there, this module runs no clock and the answer for that server is
49
+ * whatever the dial's own request budgets produce — which bound the REQUESTS, not the dial. */
50
+ timeoutMs?: number;
51
+ /** Abort the whole probe. The call rejects with the signal's own reason (a bare `abort()` gives an
52
+ * `AbortError`) AFTER every server it had opened is closed — see the residue contract on
53
+ * {@link probeMcpServers}. An already-aborted signal dials nothing. */
54
+ signal?: AbortSignal;
55
+ /** The elicitation seat, for a caller that has a human behind it. Default: decline everything. */
56
+ onElicit?: OnElicit;
57
+ /** Abort reading, stated once: the call rejects with the signal's own reason when that reason is an
58
+ * Error (a bare `abort()` supplies a DOMException named `AbortError`) and with this module's own
59
+ * `AbortError` otherwise — a face that throws a string is a face no `catch` can read. A condition
60
+ * already held when the abort lands wins: the FIRST thing that ended the walk is what it rejects
61
+ * with, so a materialization's refusal is never overwritten by the abort that followed it. */
62
+ /** How many servers this walk PURSUES A VERDICT for at once, ≥ 1. Default 1 — one at a time, in
63
+ * declaration order. Raising it trades what isolation there is for wall time; the rows and the index
64
+ * alignment are unaffected.
65
+ *
66
+ * Even at 1, `connectMs` is not a contention-free measurement: a settled dial's CLOSE is started and
67
+ * not waited for, so the next dial begins while the previous transport is still shutting down.
68
+ *
69
+ * IT IS NOT A CEILING ON OPEN TRANSPORTS, and the difference is the whole point of `timeoutMs`: a
70
+ * dial the clock abandoned keeps running (nothing here can cancel it) while the walk moves on, so
71
+ * with a short `timeoutMs` against an unresponsive fleet the transports actually in flight can reach
72
+ * one per DECLARATION, not `concurrency`. A caller probing a large fleet through a tight clock is
73
+ * choosing that: the alternative — holding the slot until the abandoned dial lands — makes one stuck
74
+ * server the walk's pace, which is the thing the clock exists to prevent. */
75
+ concurrency?: number;
76
+ }
77
+ /**
78
+ * Connect to each declared MCP server, list its tools and close it again — no task, no run.
79
+ *
80
+ * The rows in `entries` are minted by the SAME projection a prepared leg uses, from the SAME
81
+ * materialization, so they are the rows that leg would publish: `connected` with the mounted tool
82
+ * count, `failed` with the connect-failure record as closed words (`errorCode` / `delivered` /
83
+ * `httpStatus`) beside the neutralized, bounded remote text, `skipped` for a declaration this probe
84
+ * produced no attempt record for. No word is added to the failure vocabulary
85
+ * ({@link import("./mcp-failure.js").MCP_FAILURE_KINDS}) and none is spelled twice.
86
+ *
87
+ * NOTHING IS REGISTERED AND NOTHING IS MOUNTED: no task registry entry, no session, no checkpoint, no
88
+ * store write, no `TaskEvent`. The materialized tools are dropped unread and never reach a harness, and
89
+ * the result carries tool NAMES, so a caller holds nothing it could invoke.
90
+ *
91
+ * FAILURE IS PER SERVER, REFUSAL IS NOT. A server that cannot be dialed, that answers a status, or that
92
+ * outlives `timeoutMs` takes its verdict in its own row and the walk continues. What DOES reject is the
93
+ * declaration set the materialization itself refuses — two names that mount under one namespace prefix
94
+ * are refused before any transport is touched, by that same function, so the probe answers a colliding
95
+ * configuration exactly as a task would rather than showing two healthy-looking rows for a pair the
96
+ * engine would never mount.
97
+ *
98
+ * @contract mcp.probe.zero_residue_bound — this call does not settle while a handle it opened is
99
+ * undisposed: every dial's handle is disposed, a dial abandoned by the clock is disposed WHEN IT LANDS,
100
+ * and all of them are awaited before the call resolves OR rejects. What that promises is that no
101
+ * connection is left owned by nobody — NOT that every child process has already exited at the instant of
102
+ * return, and NOT that the call itself is bounded. What bounds it, stated part by part rather than
103
+ * summarised into a number that would be wrong:
104
+ * · a dial has two REQUEST-BUDGET phases, each carrying the startup-phase bound ({@link
105
+ * mcpStartupTimeoutMs}, env `MCP_TIMEOUT`, else the SDK's 60 000 ms default): the handshake, one
106
+ * request; and the initial tool listing, a PAGINATED WALK whose whole budget is that same number
107
+ * (however many pages it takes — and no request at all when the peer advertises no tools). Two
108
+ * budgets, so twice that, not once;
109
+ * · the transport's own close ladder then reaps a stdio child that does not exit when its input closes:
110
+ * at most 2 000 ms to SIGTERM, 2 000 ms more to SIGKILL;
111
+ * · and ONE part is bounded by nothing here: a transport SEND carries no request deadline of its own
112
+ * (the post-handshake `notifications/initialized`, an HTTP body read), so a peer that answers the
113
+ * handshake and then stalls that send holds the dial — and therefore this call — open. The bound
114
+ * belongs to the dial, which owns no cancellation; see `docs/KNOWN-LIMITS.md`.
115
+ * So, PER DIAL and against a peer that either answers or fails its requests: that dial's residue is gone
116
+ * within `max(timeoutMs, the handshake budget + the listing budget) + 4 000 ms`, and a server that exits
117
+ * on its closed input (every well-behaved one) is gone at return. THE CALL is the walk over the
118
+ * declarations, so its own duration is those per-dial times summed over `ceil(n / concurrency)` rounds —
119
+ * this is not a whole-call number, and a caller budgeting a health check must do that arithmetic itself.
120
+ * Against a peer that stalls a send, neither the dial nor this call has a deadline at all.
121
+ */
122
+ export declare function probeMcpServers(specs: readonly McpServerSpec[], opts?: McpProbeOptions): Promise<McpProbeResult>;
@@ -0,0 +1,132 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { snapshotMcpEntries } from "./locked-config.js";
3
+ import { classifyMcpFailure, livenessOf } from "./mcp-failure.js";
4
+ import { MCP_ENV_MS_MAX, materializeMcpTools, mcpStartupTimeoutMs } from "./mcp.js";
5
+ import { findNamespacePrefixCollision } from "./protocol-naming.js";
6
+ import { MCP_NAMESPACE } from "./protocol-table.js";
7
+ import { mcpManifestEntries } from "./runner/prepare-wiring-manifest.js";
8
+ const DECLINE_EVERY_ELICITATION = async () => ({ action: "decline" });
9
+ function wholeInRange(name, value, min, max) {
10
+ if (value === undefined)
11
+ return undefined;
12
+ if (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max)
13
+ return value;
14
+ const shown = typeof value === "number" ? String(value) : `a ${typeof value}`;
15
+ const e = new Error(`probeMcpServers: \`${name}\` must be a whole number in [${min}, ${max}] — received ${shown}`);
16
+ e.code = "config.mcp_probe_option";
17
+ throw e;
18
+ }
19
+ function abortCause(signal) {
20
+ const reason = signal.reason;
21
+ if (reason instanceof Error)
22
+ return reason;
23
+ const e = new Error("the MCP probe was aborted by its caller's signal");
24
+ e.name = "AbortError";
25
+ return e;
26
+ }
27
+ function whenAborted(signal, until) {
28
+ if (signal.aborted)
29
+ return Promise.resolve();
30
+ return new Promise((resolve) => {
31
+ signal.addEventListener("abort", () => resolve(), { once: true, signal: until });
32
+ });
33
+ }
34
+ function afterMs(ms, until) {
35
+ return new Promise((resolve) => {
36
+ const timer = setTimeout(resolve, ms);
37
+ timer.unref();
38
+ until.addEventListener("abort", () => clearTimeout(timer), { once: true });
39
+ });
40
+ }
41
+ async function raceDial(dial, ms, signal) {
42
+ if (ms === undefined && signal === undefined)
43
+ return { kind: "settled", handle: await dial };
44
+ const until = new AbortController();
45
+ const legs = [dial.then((handle) => ({ kind: "settled", handle }))];
46
+ if (ms !== undefined)
47
+ legs.push(afterMs(ms, until.signal).then(() => ({ kind: "timeout", afterMs: ms })));
48
+ if (signal !== undefined)
49
+ legs.push(whenAborted(signal, until.signal).then(() => ({ kind: "aborted", signal })));
50
+ try {
51
+ return await Promise.race(legs);
52
+ }
53
+ finally {
54
+ until.abort();
55
+ }
56
+ }
57
+ function stoppedWaitingStatus(entry, ms) {
58
+ const condition = new McpError(ErrorCode.RequestTimeout, `the probe stopped waiting for this server's connect and initial tool listing after ${ms} ms`);
59
+ const { kind, delivered } = classifyMcpFailure(condition, { phase: "connect", transport: entry.transport.kind });
60
+ const liveness = livenessOf(kind, Date.now());
61
+ return { name: entry.name, status: "failed", errorCode: kind, delivered, error: condition.message, ...(liveness !== undefined ? { liveness } : {}) };
62
+ }
63
+ export async function probeMcpServers(specs, opts = {}) {
64
+ const declared = snapshotMcpEntries(specs);
65
+ const timeoutMs = wholeInRange("timeoutMs", opts.timeoutMs, 1, MCP_ENV_MS_MAX) ?? mcpStartupTimeoutMs();
66
+ const concurrency = wholeInRange("concurrency", opts.concurrency, 1, Number.MAX_SAFE_INTEGER) ?? 1;
67
+ const onElicit = opts.onElicit ?? DECLINE_EVERY_ELICITATION;
68
+ const signal = opts.signal;
69
+ if (findNamespacePrefixCollision(MCP_NAMESPACE, declared.map((e) => e.name)) !== undefined) {
70
+ await materializeMcpTools(declared, opts.principal, onElicit);
71
+ }
72
+ const statuses = [];
73
+ const servers = new Array(declared.length);
74
+ const teardowns = [];
75
+ let fatal;
76
+ const probeOne = async (i) => {
77
+ const entry = declared[i];
78
+ const started = performance.now();
79
+ const elapsed = () => Math.round(performance.now() - started);
80
+ const dial = materializeMcpTools([entry], opts.principal, onElicit);
81
+ const verdict = await raceDial(dial, timeoutMs, signal);
82
+ if (verdict.kind === "settled") {
83
+ const found = verdict.handle.statuses.find((s) => s.name === entry.name);
84
+ const status = found === undefined ? undefined : { ...found };
85
+ teardowns.push(verdict.handle.dispose());
86
+ if (status !== undefined)
87
+ statuses.push(status);
88
+ servers[i] = { name: entry.name, connectMs: elapsed(), tools: status?.toolNames ?? [], ...(status?.errorCode !== undefined ? { errorCode: status.errorCode } : {}) };
89
+ return;
90
+ }
91
+ teardowns.push(dial.then((h) => h.dispose(), () => undefined));
92
+ if (verdict.kind === "aborted") {
93
+ fatal ??= { cause: abortCause(verdict.signal) };
94
+ return;
95
+ }
96
+ const status = stoppedWaitingStatus(entry, verdict.afterMs);
97
+ statuses.push(status);
98
+ servers[i] = { name: entry.name, connectMs: elapsed(), tools: [], errorCode: status.errorCode };
99
+ };
100
+ if (signal?.aborted === true)
101
+ fatal = { cause: abortCause(signal) };
102
+ let next = 0;
103
+ const lane = async () => {
104
+ for (;;) {
105
+ if (signal?.aborted === true)
106
+ fatal ??= { cause: abortCause(signal) };
107
+ if (fatal !== undefined)
108
+ return;
109
+ const i = next++;
110
+ if (i >= declared.length)
111
+ return;
112
+ try {
113
+ await probeOne(i);
114
+ }
115
+ catch (e) {
116
+ fatal ??= { cause: e };
117
+ return;
118
+ }
119
+ }
120
+ };
121
+ try {
122
+ await Promise.all(Array.from({ length: Math.min(concurrency, declared.length) }, lane));
123
+ }
124
+ finally {
125
+ await Promise.allSettled(teardowns);
126
+ }
127
+ if (fatal === undefined && signal?.aborted === true)
128
+ fatal = { cause: abortCause(signal) };
129
+ if (fatal !== undefined)
130
+ throw fatal.cause;
131
+ return { entries: mcpManifestEntries(declared, statuses), servers };
132
+ }
@@ -5,7 +5,7 @@ import { type OriginClearanceRow } from "./origin-clearance.js";
5
5
  import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
6
6
  import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
7
7
  import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionMarkOutcome, type SessionPollutionRecord, type SessionCaptureOptOutMarkOutcome, type SessionCaptureOptOutRecord, type SessionCaptureRecordStore } from "./layout.js";
8
- import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntry, MemoryEntryOrigin, MemoryScopeEnumeration, MemorySessionHandle, ScanFinding, SessionCaptureResidueSweep } from "./types.js";
8
+ import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryEntryOrigin, MemoryScopeEnumeration, MemorySessionHandle, ScanFinding, SessionCaptureResidueSweep } from "./types.js";
9
9
  /**
10
10
  * The RECALL-FRAMING sentence shared by every arm of the `# Memory` prompt section — the one
11
11
  * sentence that tells the model what a recalled memory IS (background context, not an instruction)
@@ -1206,6 +1206,7 @@ export declare class MemoryEngine {
1206
1206
  materialize(scopes: readonly string[], writeScope: string | null, opts?: {
1207
1207
  adoptionRestricted?: boolean;
1208
1208
  sessionId?: string;
1209
+ shadowNames?: ReadonlyMap<string, string>;
1209
1210
  }): Promise<MemorySessionHandle>;
1210
1211
  inject(handle: MemorySessionHandle, opts?: {
1211
1212
  writeToolMounted?: boolean;
@@ -1527,6 +1528,10 @@ export declare class MemoryEngine {
1527
1528
  * retrieval view (copy-out File), else the backend itself (Pg/TiDB shapes — naturally committed
1528
1529
  * and side-effect-free per the getByIds contract clause). */
1529
1530
  private committedAuditFace;
1531
+ /** The COMMITTED header listing of `scopes` through the face above: the name source of the
1532
+ * write-home rule, and the reason an in-session edit of a `name:` field retires nothing before
1533
+ * the harvest boundary admits it. Side-effect-free (`audit: false`). */
1534
+ listCommittedHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
1530
1535
  private static originClearRefusal;
1531
1536
  /** #468② — the most recent COMPLETED clearance of `entryId` in `rows`, or `undefined` when there is none.
1532
1537
  * Pure over rows the caller ALREADY read (this opens no second read of the account — the not-marked
@@ -10,6 +10,7 @@ import { committedDistilledOf, committedOriginOf, computeEntryRev, consolidation
10
10
  import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
11
11
  import { isInstructionEntry } from "./header-hints.js";
12
12
  import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, sessionsSinceExcludingCurrent, recordIntentCredentials, scanRefusalCeiling, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
13
+ import { deriveShadowedSet } from "./shadowing.js";
13
14
  import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
14
15
  import { noReplaceRestore, retireStagingToCustody, stagingPathBeside } from "./delegation-settlement.js";
15
16
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
@@ -1330,6 +1331,7 @@ export class MemoryEngine {
1330
1331
  repoIndexTargets: new Map(),
1331
1332
  indexBaselineLines: 0,
1332
1333
  indexText: "",
1334
+ ...(opts?.shadowNames !== undefined ? { shadowNames: opts.shadowNames } : {}),
1333
1335
  ...(restricted ? { adoptionRestricted: true } : {}),
1334
1336
  ...(sessionAccountFailed ? { sessionAccountFailed: true } : {}),
1335
1337
  };
@@ -4047,6 +4049,11 @@ export class MemoryEngine {
4047
4049
  const b = this.backend;
4048
4050
  return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
4049
4051
  }
4052
+ async listCommittedHeaders(scopes) {
4053
+ if (scopes.length === 0)
4054
+ return [];
4055
+ return this.committedAuditFace().listHeaders(scopes);
4056
+ }
4050
4057
  static originClearRefusal(code, message, detail) {
4051
4058
  const e = new Error(message);
4052
4059
  e.code = code;
@@ -4341,6 +4348,17 @@ export class MemoryEngine {
4341
4348
  const exposedProjectionTargets = new Set();
4342
4349
  const supersededIds = deriveSupersededSet(headers, { excluded: (id) => excludedIds.has(id) });
4343
4350
  const supersededTargets = new Set();
4351
+ const shadowedIds = deriveShadowedSet(headers, handle.writeScope, {
4352
+ excluded: (id) => excludedIds.has(id) || supersededIds.has(id),
4353
+ ...(handle.shadowNames !== undefined ? { shadowNames: handle.shadowNames } : {}),
4354
+ });
4355
+ const shadowedCounts = new Map();
4356
+ for (const h of headers) {
4357
+ if (excludedIds.has(h.id) || !shadowedIds.has(h.id))
4358
+ continue;
4359
+ shadowedCounts.set(h.scope, (shadowedCounts.get(h.scope) ?? 0) + 1);
4360
+ }
4361
+ const shadowedTargets = new Set();
4344
4362
  for (const h of headers) {
4345
4363
  const repoTarget = handle.repoIndexTargets?.get(h.id);
4346
4364
  const target = repoTarget ?? relative(indexBase, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
@@ -4349,8 +4367,9 @@ export class MemoryEngine {
4349
4367
  continue;
4350
4368
  }
4351
4369
  const superseded = supersededIds.has(h.id);
4370
+ const shadowed = shadowedIds.has(h.id);
4352
4371
  if (carryIndex && h.exposure === "external") {
4353
- if (!superseded)
4372
+ if (!superseded && !shadowed)
4354
4373
  exposedById.set(h.id, h);
4355
4374
  exposedProjectionTargets.add(target);
4356
4375
  continue;
@@ -4359,8 +4378,13 @@ export class MemoryEngine {
4359
4378
  supersededTargets.add(target);
4360
4379
  continue;
4361
4380
  }
4381
+ if (shadowed) {
4382
+ shadowedTargets.add(target);
4383
+ continue;
4384
+ }
4362
4385
  expected.set(target, h);
4363
4386
  }
4387
+ handle.shadowedByScope = shadowedCounts;
4364
4388
  const covered = new Set();
4365
4389
  const handleCovered = new Set();
4366
4390
  const keptByTarget = new Map();
@@ -4376,7 +4400,7 @@ export class MemoryEngine {
4376
4400
  return prev;
4377
4401
  };
4378
4402
  const foldTarget = (t) => foldDots(t.toLowerCase());
4379
- const knownTargetsFolded = new Set([...expected.keys(), ...excludedTargets, ...exposedProjectionTargets, ...supersededTargets].map(foldTarget));
4403
+ const knownTargetsFolded = new Set([...expected.keys(), ...excludedTargets, ...exposedProjectionTargets, ...supersededTargets, ...shadowedTargets].map(foldTarget));
4380
4404
  const pathTokenAt = (folded, idx, len) => {
4381
4405
  const isPathChar = (ch) => /[a-z0-9._\-/\\]/.test(ch);
4382
4406
  let s = idx;
@@ -4428,6 +4452,10 @@ export class MemoryEngine {
4428
4452
  warnings?.push(`MEMORY.md index line for ${target} removed: a newer consolidated entry supersedes it (the entry stays readable via memory_get)`);
4429
4453
  continue;
4430
4454
  }
4455
+ if (shadowedTargets.has(target)) {
4456
+ warnings?.push(`MEMORY.md index line for ${target} removed: a same-named entry in this session's write home shadows it (the entry stays readable via memory_get)`);
4457
+ continue;
4458
+ }
4431
4459
  if (covered.has(target)) {
4432
4460
  if (line === keptByTarget.get(target))
4433
4461
  continue;
@@ -9,6 +9,7 @@ export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./hea
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
10
10
  export { consolidationExposedFrontmatter } from "./frontmatter.js";
11
11
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
12
+ export { deriveShadowedSet } from "./shadowing.js";
12
13
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
13
14
  export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
14
15
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
@@ -9,6 +9,7 @@ export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation } from "./frontmatter.js";
10
10
  export { consolidationExposedFrontmatter } from "./frontmatter.js";
11
11
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
12
+ export { deriveShadowedSet } from "./shadowing.js";
12
13
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, CONSOLIDATION_RUN_STOP_REASONS, } from "./consolidation.js";
13
14
  export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
14
15
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, } from "./consolidation-driver.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The WRITE-HOME NAME rule: one predicate, four existing read seats (index rows, search, listing,
3
+ * bare-slug resolution). When the session's write home already carries an entry by a given NAME,
4
+ * the read-only layers' same-named entries leave the DEFAULT read face. Nothing is written, moved
5
+ * or deleted — the shadowed entry stays fully readable as evidence by id and through the
6
+ * `includeShadowed` faces, the write home's own entries are never affected, read-only layers never
7
+ * shadow each other, and a session with no write home derives the empty set (byte-identical
8
+ * behaviour to a tree without this module).
9
+ *
10
+ * It lands at the seats the supersession derivation in `consolidation.ts` lands at, in the same
11
+ * shape: a second answer to "what is on the default face" would be a drift seam. The two are
12
+ * orthogonal and compose — supersession is a same-scope data fact carried by consolidation edges,
13
+ * shadowing a cross-layer session fact that depends on where this session writes — so an entry can
14
+ * be both, and each is separately disclosed.
15
+ *
16
+ * TWO LAWS AUTHORIZE A SHADOW. (1) The NAME SOURCE is a committed header face: an in-session
17
+ * `name:` edit retires nothing before the harvest boundary admits it. (2) The CARRIER must itself
18
+ * STAND ON THE SEAT'S OWN DEFAULT FACE — withheld, superseded and undeliverable are not three
19
+ * conditions but three ways of not standing there, which is how the caller passes them (one
20
+ * `excluded` predicate, plus a delivery probe for the one part a header listing cannot answer).
21
+ * Stating it as three independent conditions is what a counterexample punched through: a
22
+ * write-home entry replaced by a consolidation product is committed, unchallenged and deliverable,
23
+ * yet it is not on the default face, and letting it shadow took BOTH same-named answers off every
24
+ * enumeration at once. The write home's answer and the shadowed one never vanish together.
25
+ */
26
+ import type { MemoryEntryHeader } from "./types.js";
27
+ /** The name an entry is known by on the read faces: its frontmatter `name`, else the last segment
28
+ * of its slug — the expression the index row's own title is built from, so "same name" means what
29
+ * a reader of the injected index would call the same name. Exact equality: no case folding (the
30
+ * frontmatter parse already trimmed), no type or scope participation. */
31
+ export declare function shadowKeyOf(h: Pick<MemoryEntryHeader, "name" | "slug">): string;
32
+ /** The NAMES the session's write home carries, each mapped to the id carrying it (lowest id wins
33
+ * when the home holds several under one name — deterministic, never a scan order). A read-only
34
+ * layering (`writeScope: null`) carries nothing. */
35
+ export declare function shadowNamesOf(headers: readonly MemoryEntryHeader[], writeScope: string | null, opts?: {
36
+ excluded?: (id: string) => boolean;
37
+ }): Map<string, string>;
38
+ /**
39
+ * Derive the SHADOWED set from a header listing: shadowed entry id → the write-home entry id whose
40
+ * name retires it from the default face.
41
+ *
42
+ * `opts.shadowNames`, when given, IS the name map (the caller computed it — across planes, or from
43
+ * a listing this one does not contain); absent, the map is derived from these same headers through
44
+ * {@link shadowNamesOf}. One rule either way.
45
+ *
46
+ * @contract memory.read_face.write_home_name_wins — a read-only layer's entry whose name the
47
+ * session's write home already carries leaves the default read face (index row, search hit,
48
+ * listing row, bare-slug resolution) and stays fully readable as evidence by id; nothing is
49
+ * written, moved or deleted, and the write home's own entries are never affected.
50
+ */
51
+ export declare function deriveShadowedSet(headers: readonly MemoryEntryHeader[], writeScope: string | null, opts?: {
52
+ excluded?: (id: string) => boolean;
53
+ shadowNames?: ReadonlyMap<string, string>;
54
+ }): Map<string, string>;
55
+ /** memory_get head note for a shadowed entry (delivery proceeds — disclosure, never refusal). */
56
+ export declare function memoryShadowedNote(shadowingId: string): string;
57
+ /** The tag suffixed to a search hit / listing row when a shadowed entry is retrieved explicitly. */
58
+ export declare const MEMORY_SEARCH_SHADOWED_TAG = "[shadowed]";
@@ -0,0 +1,40 @@
1
+ export function shadowKeyOf(h) {
2
+ return h.name ?? h.slug.split("/").pop() ?? h.slug;
3
+ }
4
+ export function shadowNamesOf(headers, writeScope, opts = {}) {
5
+ const out = new Map();
6
+ if (writeScope === null)
7
+ return out;
8
+ for (const h of headers) {
9
+ if (h.scope !== writeScope)
10
+ continue;
11
+ if (opts.excluded?.(h.id) === true)
12
+ continue;
13
+ const key = shadowKeyOf(h);
14
+ if (key === "")
15
+ continue;
16
+ const prior = out.get(key);
17
+ if (prior === undefined || h.id < prior)
18
+ out.set(key, h.id);
19
+ }
20
+ return out;
21
+ }
22
+ export function deriveShadowedSet(headers, writeScope, opts = {}) {
23
+ const names = opts.shadowNames ?? shadowNamesOf(headers, writeScope, opts);
24
+ const out = new Map();
25
+ if (names.size === 0)
26
+ return out;
27
+ for (const h of headers) {
28
+ if (h.scope === writeScope)
29
+ continue;
30
+ const carrier = names.get(shadowKeyOf(h));
31
+ if (carrier === undefined || carrier === h.id)
32
+ continue;
33
+ out.set(h.id, carrier);
34
+ }
35
+ return out;
36
+ }
37
+ export function memoryShadowedNote(shadowingId) {
38
+ return `note: a same-named entry in this session's write home shadows this one — memory entry ${shadowingId} carries the form this session reads by default; this entry remains readable as evidence.`;
39
+ }
40
+ export const MEMORY_SEARCH_SHADOWED_TAG = "[shadowed]";
@@ -32,7 +32,7 @@
32
32
  * so the engine's abort path stays the abort path.
33
33
  */
34
34
  import type { ToolSpec } from "../types.js";
35
- import type { MemoryBackend } from "./types.js";
35
+ import type { MemoryBackend, MemoryEntryHeader } from "./types.js";
36
36
  export declare const MEMORY_SEARCH_TOOL_NAME = "memory_search";
37
37
  export declare const MEMORY_GET_TOOL_NAME = "memory_get";
38
38
  /** #493 件① — the enumeration face. NOT `memory_list`: that wire name (and the `core.memory_list@1`
@@ -108,6 +108,15 @@ export interface MemoryEnginePlane {
108
108
  * hosts and tests) and nothing is asserted: the factory's behaviour is byte-identical to before.
109
109
  */
110
110
  enginePlaneProvenance?: "off" | "carry";
111
+ /**
112
+ * The plane's COMMITTED header listing — the name source of the write-home rule
113
+ * ({@link import("./shadowing.js").deriveShadowedSet}). Wired to
114
+ * `MemoryEngine.listCommittedHeaders`, it is what makes an in-session edit of a `name:` field
115
+ * retire nothing before the harvest boundary admits it. ABSENT ⇒ this plane contributes no
116
+ * write-home names, so nothing on any plane is shadowed and every face below is byte-identical
117
+ * to a deployment without the rule (engine-direct hosts and tests).
118
+ */
119
+ committedHeaders?: () => Promise<MemoryEntryHeader[]>;
111
120
  }
112
121
  export interface MemoryEngineToolsOptions {
113
122
  planes: ReadonlyArray<MemoryEnginePlane>;
@@ -135,6 +144,14 @@ export interface MemoryEngineToolsOptions {
135
144
  * even over a store that carries marked entries (the knob's whole meaning).
136
145
  */
137
146
  provenance?: "off" | "carry";
147
+ /**
148
+ * The session's WRITE HOME scope — the layer every write of this session routes to. The
149
+ * write-home name rule's one input beside the headers: a read-only layer's entry whose name this
150
+ * scope already carries leaves the DEFAULT face of all three tools (retrievable through
151
+ * `includeShadowed` and by id). ABSENT or `null` ⇒ no write home is declared to these faces and
152
+ * nothing is ever shadowed — the pre-rule bytes.
153
+ */
154
+ writeScope?: string | null;
138
155
  }
139
156
  /**
140
157
  * An unmarked search hit — the pre-336 hit shape, `slug` required as before. `exposure` is a
@@ -154,6 +171,9 @@ export interface CleanMemorySearchHit {
154
171
  /** design/339 §3.4 — present ⇔ this hit is SUPERSEDED (retrieved via `includeSuperseded`; the
155
172
  * default face filters such hits out) — the value is the standing carrier entry's id. */
156
173
  supersededBy?: string;
174
+ /** Present ⇔ this hit is SHADOWED (retrieved via `includeShadowed`; the default face filters
175
+ * such hits out) — the value is the write-home entry id whose name retires it. */
176
+ shadowedBy?: string;
157
177
  }
158
178
  /**
159
179
  * design/336 §5.2 — a MARKED hit is an OPAQUE HANDLE: id/scope/score/age/size plus the exposure
@@ -175,6 +195,8 @@ export interface ExposedMemorySearchHit {
175
195
  description?: never;
176
196
  /** design/339 §3.4 — see {@link CleanMemorySearchHit.supersededBy} (same seat on the handle shape). */
177
197
  supersededBy?: string;
198
+ /** See {@link CleanMemorySearchHit.shadowedBy} (same seat on the handle shape). */
199
+ shadowedBy?: string;
178
200
  }
179
201
  /**
180
202
  * The search hit union (design/336 B5 — a DISCRIMINATED union on `exposure`, deliberately not an
@@ -220,6 +242,10 @@ export interface MemoryGetDetails {
220
242
  * the standing carrier entry's id (the disclosure note's structured half). Delivery proceeds —
221
243
  * supersession is disclosure/ordering, never withholding. */
222
244
  supersededBy?: string;
245
+ /** Present on an `ok` delivery of a SHADOWED entry: the write-home entry id whose name retires
246
+ * it from this session's default face (the head note's structured half). Delivery proceeds —
247
+ * the rule is disclosure/ordering, never withholding. */
248
+ shadowedBy?: string;
223
249
  }
224
250
  /**
225
251
  * #493 件① — one row of the memory_index enumeration. The same discriminated shape law as the
@@ -239,6 +265,9 @@ export interface CleanMemoryIndexRow {
239
265
  /** Present ⇔ this row is SUPERSEDED (listed via `includeSuperseded`; the default face filters
240
266
  * such rows out) — the value is the standing carrier entry's id. */
241
267
  supersededBy?: string;
268
+ /** Present ⇔ this row is SHADOWED (listed via `includeShadowed`; the default face filters such
269
+ * rows out) — the value is the write-home entry id whose name retires it. */
270
+ shadowedBy?: string;
242
271
  }
243
272
  /** The opaque marked-row handle (carry mode) — same posture as {@link ExposedMemorySearchHit}. */
244
273
  export interface ExposedMemoryIndexRow {
@@ -251,6 +280,7 @@ export interface ExposedMemoryIndexRow {
251
280
  name?: never;
252
281
  description?: never;
253
282
  supersededBy?: string;
283
+ shadowedBy?: string;
254
284
  }
255
285
  export type MemoryIndexRow = CleanMemoryIndexRow | ExposedMemoryIndexRow;
256
286
  export interface MemoryIndexDetails {
@@ -265,6 +295,8 @@ export interface MemoryIndexDetails {
265
295
  total?: number;
266
296
  /** Superseded rows the default face hid (absent when zero or when `includeSuperseded` was passed). */
267
297
  supersededHidden?: number;
298
+ /** Shadowed rows the default face hid (absent when zero or when `includeShadowed` was passed). */
299
+ shadowedHidden?: number;
268
300
  /** Present ⇔ more rows follow — pass it as the next call's `offset`. */
269
301
  nextOffset?: number;
270
302
  /** The listing revision this page was served against (a stateless digest of the ordered id