@sema-agent/core 7.25.0 → 7.26.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.
- package/CHANGELOG.md +81 -0
- package/dist/core/arg-summary.d.ts +0 -7
- package/dist/core/arg-summary.js +2 -1
- package/dist/core/ask-origin.d.ts +84 -0
- package/dist/core/ask-origin.js +31 -3
- package/dist/core/auto-compaction.d.ts +5 -0
- package/dist/core/auto-compaction.js +7 -0
- package/dist/core/auto-mode-directive.d.ts +45 -0
- package/dist/core/auto-mode-directive.js +47 -0
- package/dist/core/checkpoint-store.d.ts +10 -1
- package/dist/core/engine-notice.d.ts +10 -0
- package/dist/core/gate-lanes.js +18 -10
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/mcp-probe.d.ts +122 -0
- package/dist/core/mcp-probe.js +132 -0
- package/dist/core/memory-engine/engine.d.ts +6 -1
- package/dist/core/memory-engine/engine.js +30 -2
- package/dist/core/memory-engine/index.d.ts +1 -0
- package/dist/core/memory-engine/index.js +1 -0
- package/dist/core/memory-engine/shadowing.d.ts +58 -0
- package/dist/core/memory-engine/shadowing.js +40 -0
- package/dist/core/memory-engine/tools.d.ts +33 -1
- package/dist/core/memory-engine/tools.js +136 -31
- package/dist/core/memory-engine/types.d.ts +10 -0
- package/dist/core/memory.d.ts +60 -0
- package/dist/core/memory.js +47 -2
- package/dist/core/permission-rule-store.d.ts +51 -8
- package/dist/core/permission-rule-store.js +112 -65
- package/dist/core/reminder-mint.d.ts +41 -0
- package/dist/core/reminder-mint.js +55 -0
- package/dist/core/runner/compaction-restate.d.ts +9 -0
- package/dist/core/runner/compaction-restate.js +20 -0
- package/dist/core/runner/contracts.d.ts +18 -1
- package/dist/core/runner/denial-limit-arms.js +1 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +5 -17
- package/dist/core/runner/permission-rule-lanes.js +5 -23
- package/dist/core/runner/prepare-ask-lane.js +2 -0
- package/dist/core/runner/prepare-context-lane.d.ts +4 -0
- package/dist/core/runner/prepare-context-lane.js +7 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -1
- package/dist/core/runner/prepare-inherited-gate.js +4 -2
- package/dist/core/runner/prepare-memory.js +47 -6
- package/dist/core/runner/prepare-park-ask.js +1 -0
- package/dist/core/runner/prepare-policy-chain.js +8 -4
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -0
- package/dist/core/runner/prepare-suspend-saga.js +3 -1
- package/dist/core/runner/prepare-task.js +7 -4
- package/dist/core/runner/run-leg.js +9 -1
- package/dist/core/runner/run-recovery-lanes.js +2 -2
- package/dist/core/runner/run-terminal-adoption.js +2 -2
- package/dist/core/runner/run-turn-boundary.js +2 -2
- package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
- package/dist/core/store-contracts/permission-rule-write-contract.js +77 -0
- package/dist/core/task-result.d.ts +11 -0
- package/dist/core/task-spec.d.ts +31 -0
- package/dist/core/tool-catalog-entries.js +3 -3
- package/dist/core/tool-policy.d.ts +38 -0
- package/dist/core/tool-policy.js +35 -24
- package/dist/core/types.d.ts +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/stores/file/permission-rule-store.js +48 -29
- package/dist/tools/fs/bash-lexer.d.ts +13 -0
- package/dist/tools/fs/bash-lexer.js +61 -0
- package/dist/tools/fs/bash-program-position.d.ts +32 -3
- package/dist/tools/fs/bash-program-position.js +33 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +6 -4
- package/dist/tools/fs/bash-readonly-classifier.js +55 -18
- package/dist/tools/fs/shell-read-plan.js +9 -11
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +29 -1
- package/wire-changes/core@7.25.1.json +64 -0
- package/wire-changes/core@7.26.0.json +695 -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",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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
|