@sema-agent/core 5.27.0 → 5.29.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 +97 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/observer.d.ts +3 -3
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/teacher.d.ts +1 -1
- package/dist/agents/verify.d.ts +6 -6
- package/dist/bench/metrics.d.ts +1 -1
- package/dist/brain/retry.d.ts +1 -1
- package/dist/core/arg-summary.d.ts +1 -1
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +14 -1
- package/dist/core/background-shell.d.ts +5 -5
- package/dist/core/checkpoint-store.d.ts +14 -14
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/hooks.d.ts +25 -3
- package/dist/core/hooks.js +22 -3
- package/dist/core/human-input-projection.d.ts +2 -2
- package/dist/core/memory-admission.d.ts +2 -2
- package/dist/core/memory-engine/engine.d.ts +1 -1
- package/dist/core/memory-engine/engine.js +2 -4
- package/dist/core/memory-engine/file-backend.d.ts +68 -9
- package/dist/core/memory-engine/file-backend.js +69 -27
- package/dist/core/memory-engine/header-hints.d.ts +1 -1
- package/dist/core/memory-engine/layout.d.ts +32 -3
- package/dist/core/memory-engine/layout.js +132 -8
- package/dist/core/memory-engine/types.d.ts +6 -5
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/oracle-isolation.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +83 -9
- package/dist/core/permission-rule-consent.js +92 -1
- package/dist/core/permission-rule-model.d.ts +18 -2
- package/dist/core/permission-rule-model.js +21 -0
- package/dist/core/permission-rule-org.d.ts +23 -4
- package/dist/core/permission-rule-org.js +67 -20
- package/dist/core/permission-rule-store.d.ts +1 -1
- package/dist/core/permission-rule-store.js +2 -2
- package/dist/core/permission-rule-sync.d.ts +15 -1
- package/dist/core/permission-rule-sync.js +89 -47
- package/dist/core/runner/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-task.d.ts +8 -3
- package/dist/core/runner/prepare-task.js +22 -7
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/runtask.js +13 -6
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -1
- package/dist/core/scheduler.d.ts +4 -4
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/shared-memory/normalize.d.ts +1 -1
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/task-notification.d.ts +2 -2
- package/dist/core/task-registry-agent.d.ts +18 -9
- package/dist/core/task-registry-agent.js +51 -21
- package/dist/core/task-registry-monitor.js +1 -1
- package/dist/core/task-registry-shared.d.ts +13 -4
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +78 -12
- package/dist/core/tool-policy.js +74 -7
- package/dist/core/tool-result-store.d.ts +109 -8
- package/dist/core/tool-result-store.js +95 -15
- package/dist/core/trace.d.ts +1 -1
- package/dist/core/types.d.ts +92 -22
- package/dist/core/types.js +30 -1
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/compaction/compaction.d.ts +1 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +2 -2
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/types.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +5 -4
- package/dist/orchestration/workflow.js +11 -3
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompts/default.d.ts +1 -1
- package/dist/prompts/default.js +1 -1
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/stores/file/tool-result-store.d.ts +43 -3
- package/dist/stores/file/tool-result-store.js +107 -19
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/fs-bash.d.ts +8 -1
- package/dist/tools/fs/fs-search-tools.d.ts +1 -1
- package/dist/tools/fs/fs-shared.d.ts +6 -1
- package/dist/tools/fs/fs-shared.js +11 -7
- package/dist/tools/fs/index.d.ts +6 -0
- package/dist/tools/fs/index.js +2 -0
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/task-list.d.ts +1 -1
- package/dist/tools/web.d.ts +3 -3
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ export declare const MEMORY_RECALL_DISCIPLINE = "Before answering questions abou
|
|
|
37
37
|
* CC-verbatim `# Memory` instruction stays byte-identical to its sha256-pinned capture. Appended
|
|
38
38
|
* only when a WRITE channel exists (prepare-memory gates it) — the lines it teaches are frontmatter
|
|
39
39
|
* the MODEL writes; the engine has no write arm for them (writing usage evidence for the model
|
|
40
|
-
* would
|
|
40
|
+
* would self-declare the very account the three-tier discipline forbids forging).
|
|
41
41
|
*/
|
|
42
42
|
export declare const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines \u2014 write them yourself; nothing fills them in for you.";
|
|
43
43
|
/**
|
|
@@ -328,7 +328,7 @@ export class MemoryEngine {
|
|
|
328
328
|
ensureDirExists(this.controlDir);
|
|
329
329
|
const restricted = opts?.adoptionRestricted === true;
|
|
330
330
|
const readBackend = restricted
|
|
331
|
-
? (this.backend.restrictedAdoptionView?.({ audit: true }) ?? this.backend)
|
|
331
|
+
? (this.backend.restrictedAdoptionView?.({ audit: true, writeScope }) ?? this.backend)
|
|
332
332
|
: this.backend;
|
|
333
333
|
if (writeScope !== null)
|
|
334
334
|
claimRootScope(this.controlDir, writeScope);
|
|
@@ -536,9 +536,7 @@ export class MemoryEngine {
|
|
|
536
536
|
const writeScope = handle.writeScope;
|
|
537
537
|
if (writeScope === null || opts?.admitNothing !== undefined) {
|
|
538
538
|
try {
|
|
539
|
-
this.backend.auditRestrictedDivergence?.([
|
|
540
|
-
...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])]),
|
|
541
|
-
]);
|
|
539
|
+
this.backend.auditRestrictedDivergence?.([...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])])], handle.adoptionRestricted === true && writeScope !== null ? { idlessWriteScope: writeScope } : undefined);
|
|
542
540
|
}
|
|
543
541
|
catch (err) {
|
|
544
542
|
report.warnings.push(`restricted-session divergence audit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -147,13 +147,48 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
147
147
|
* `sync` (read-side callers) adopts a committed file's CURRENT disk rev into the committed-rev
|
|
148
148
|
* ledger ONLY through the inbound gate (B2: out-of-session edits, e.g. git-pull, are scanned;
|
|
149
149
|
* a reject is moved to control-plane quarantine and the committed shadow content is restored to
|
|
150
|
-
* disk). The applyPatches lookup passes false (the ledger must keep the baseline for CAS).
|
|
150
|
+
* disk). The applyPatches lookup passes false (the ledger must keep the baseline for CAS).
|
|
151
|
+
*
|
|
152
|
+
* Ledger-write discipline: the sync=true adoption path WRITES `revs.json`, and that write is
|
|
153
|
+
* wholesale (atomic replace) — unserialized against applyPatches' transaction it is a
|
|
154
|
+
* lost update (the later writer drops every row the earlier one added). sync=true is therefore
|
|
155
|
+
* reachable ONLY through {@link readScopeAdopting}: `probeOnly` runs the scan with NO adoption
|
|
156
|
+
* side effects and returns undefined at the FIRST divergent entry (adoption required — the
|
|
157
|
+
* caller re-runs the scan under the txn mutex against a freshly reloaded ledger); a completed
|
|
158
|
+
* probe pass returns the entries (nothing to adopt, no lock ever taken). */
|
|
151
159
|
private readScope;
|
|
160
|
+
/**
|
|
161
|
+
* The adopting read: {@link readScope} sync=true with its ledger write serialized behind the
|
|
162
|
+
* SAME cross-process txn mutex applyPatches holds (acquisition/steal/timeout semantics shared,
|
|
163
|
+
* never duplicated). Without it, an adoption landing beside a sibling process's commit is a
|
|
164
|
+
* lost update: both read the ledger, both write it wholesale, the later atomic write drops the
|
|
165
|
+
* earlier writer's rows (the entry file stays on disk but the ledger no longer knows it).
|
|
166
|
+
* - PROBE first (no lock): disk agreeing with the ledger is the common case, and that pass writes
|
|
167
|
+
* no LEDGER row — the read stays lock-free and never contends with a writer over the ledger.
|
|
168
|
+
* (Precision, merged-code scan: the non-divergent branch may still BACKFILL a missing shadow
|
|
169
|
+
* file for a committed entry, an unserialized control-plane write with a narrow pre-existing
|
|
170
|
+
* last-writer window against a sibling's commit — see the shadow-backfill site below; that
|
|
171
|
+
* window predates this probe and is the same one the v2 single-lock design closes.)
|
|
172
|
+
* - Divergence ⇒ acquire the mutex, finish any interrupted transaction (the steal-safety
|
|
173
|
+
* contract: a stolen stale lock is safe BECAUSE recovery runs inside the lock), RELOAD the
|
|
174
|
+
* ledger from disk (the probe's cached view is exactly the stale baseline that loses
|
|
175
|
+
* updates), then re-run the scan with adoption and save inside the lock. The save re-verifies
|
|
176
|
+
* lock OWNERSHIP first (a holder stalled past the stale deadline was stolen from — its
|
|
177
|
+
* snapshot is behind the stealer's commit and must abort, exactly like the applyPatches
|
|
178
|
+
* journal write).
|
|
179
|
+
* - A lock that cannot be acquired throws (same loud arm as the write path): an adoption that
|
|
180
|
+
* cannot be serialized must surface, never silently skip the gate or serve stale state as
|
|
181
|
+
* adopted.
|
|
182
|
+
* No self-deadlock: nothing under the lock re-enters acquireTxnLock — the scan's quarantine /
|
|
183
|
+
* shadow / announcement writes use their own per-file sidecar locks (whose holders never take
|
|
184
|
+
* the txn mutex), and applyPatchesLocked only issues sync=false reads. The two UNLOCKED
|
|
185
|
+
* recovery observers (constructor / checkControlPlane) are unaffected: they already stand down
|
|
186
|
+
* while a live holder exists ({@link txnInFlight}), which now covers adoption holds too.
|
|
187
|
+
*/
|
|
188
|
+
private readScopeAdopting;
|
|
152
189
|
listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
|
|
153
|
-
private listHeadersWith;
|
|
154
190
|
private listHeadersFrom;
|
|
155
191
|
getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
|
|
156
|
-
private getByIdsWith;
|
|
157
192
|
private getByIdsFrom;
|
|
158
193
|
/**
|
|
159
194
|
* design/178 §3 (ruled 2026-08-08) — a SIDE-EFFECT-FREE projection of this backend for the session's
|
|
@@ -181,6 +216,19 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
181
216
|
* notice); a divergence whose bytes CHANGE mints a new key and is reported again. */
|
|
182
217
|
private restrictedFindingKeys;
|
|
183
218
|
private recordRestrictedFinding;
|
|
219
|
+
/** Id-less audit state for an adoption-restricted session's WRITE scope. Armed only by the
|
|
220
|
+
* audited mount, which carries the caller's explicit session verdict together with the session's
|
|
221
|
+
* write scope — never inferred from plane structure (a writeScope-null layering has no write
|
|
222
|
+
* scope and is never given this surface: id-less files there are ordinary working files or
|
|
223
|
+
* external drops). The baseline is the set of id-less rel paths already under the scope at the
|
|
224
|
+
* first audited read after the mount (= materialize time): a pre-existing id-less file cannot be
|
|
225
|
+
* attributed to this session and stays unflagged, while a session under the restriction verdict
|
|
226
|
+
* has no sanctioned write channel into the store — so an id-less file that APPEARS after the
|
|
227
|
+
* baseline arrived out of band by construction and is worth naming. Per-process like the finding
|
|
228
|
+
* keys; when restricted sessions interleave on one instance the later mount re-baselines, which
|
|
229
|
+
* degrades toward silence, never toward a false alarm. */
|
|
230
|
+
private restrictedIdlessScope?;
|
|
231
|
+
private restrictedIdlessBaseline?;
|
|
184
232
|
/**
|
|
185
233
|
* The ADOPTION-RESTRICTED read of one scope: serve the COMMITTED account (ledger + control-plane
|
|
186
234
|
* shadow) and never advance it. This is the tamper boundary for a session with no write intent:
|
|
@@ -223,13 +271,15 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
223
271
|
* view; the backend never guesses a session's intent from its own state.
|
|
224
272
|
* - `audit: true` (the engine's materialize/harvest mount): divergence findings are recorded for
|
|
225
273
|
* the harvest report + announcement queue, and gate-FAILING divergence is contained by the
|
|
226
|
-
* existing quarantine door;
|
|
274
|
+
* existing quarantine door; `writeScope` (when the session has one) additionally arms the
|
|
275
|
+
* id-less baseline for that scope — see {@link auditRestrictedDivergence};
|
|
227
276
|
* - `audit: false` (the retrieval-tool mount): a pure committed-view read — no findings, no
|
|
228
|
-
* containment, no writes.
|
|
277
|
+
* containment, no writes, no baseline.
|
|
229
278
|
* WRITES ARE REFUSED loudly — same posture as {@link retrievalView}.
|
|
230
279
|
*/
|
|
231
280
|
restrictedAdoptionView(opts?: {
|
|
232
281
|
audit?: boolean;
|
|
282
|
+
writeScope?: string | null;
|
|
233
283
|
}): MemoryBackend;
|
|
234
284
|
/**
|
|
235
285
|
* The harvest-boundary divergence sweep for an adoption-restricted session (the engine's
|
|
@@ -240,13 +290,22 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
240
290
|
* is divergence too). Ledger rows carry no scope, so presence is judged across every registered
|
|
241
291
|
* scope, the same resolution `getByIds` uses; a rename or cross-scope move therefore never reads
|
|
242
292
|
* as missing. Non-destructive throughout: nothing is restored, forgotten, or adopted here.
|
|
293
|
+
*
|
|
294
|
+
* `opts.idlessWriteScope` is the CALLER's per-call session verdict (same posture as the mount:
|
|
295
|
+
* the backend never guesses): when the audited session HAS a write scope, id-less files under it
|
|
296
|
+
* that were not in the mount-time baseline are named too — the session has no sanctioned write
|
|
297
|
+
* channel into the store, so a new arrival is out of band by construction. Left out (a
|
|
298
|
+
* writeScope-null layering, or a caller that never mounted the audit), the sweep is exactly the
|
|
299
|
+
* id-bearing audit above; a scope mismatch with the armed mount likewise degrades to silence
|
|
300
|
+
* rather than judging against a foreign baseline.
|
|
243
301
|
*/
|
|
244
|
-
auditRestrictedDivergence(scopes: readonly string[]
|
|
302
|
+
auditRestrictedDivergence(scopes: readonly string[], opts?: {
|
|
303
|
+
idlessWriteScope?: string;
|
|
304
|
+
}): void;
|
|
245
305
|
/** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
|
|
246
306
|
search(query: string, scopes: readonly string[], opts?: {
|
|
247
307
|
limit?: number;
|
|
248
308
|
}): Promise<ScoredMemoryEntry[]>;
|
|
249
|
-
private searchWith;
|
|
250
309
|
private searchFrom;
|
|
251
310
|
/**
|
|
252
311
|
* H4 — the entry transaction, staged and journaled:
|
|
@@ -264,7 +323,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
264
323
|
applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
|
|
265
324
|
private txnLockDir;
|
|
266
325
|
/**
|
|
267
|
-
*
|
|
326
|
+
* 证伪式复审 L1/L2/L3 — the mkdir mutex, hardened (same shape as layout.ts' acquireSidecarLock, but
|
|
268
327
|
* fail-closed timings and an async wait):
|
|
269
328
|
* - L2: every acquisition writes an OWNER TOKEN (`<lock>/owner`, crypto-random — never a
|
|
270
329
|
* timestamp) immediately after mkdir. Release only removes the lock while the token is still
|
|
@@ -279,7 +338,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
279
338
|
* crashed with `remaining stale > waitMs` no longer strands every waiter in an all-throw window.
|
|
280
339
|
*/
|
|
281
340
|
private acquireTxnLock;
|
|
282
|
-
/**
|
|
341
|
+
/** 证伪式复审 L2 — 提交点归属复验: called immediately before the JOURNAL write (the persistent commit
|
|
283
342
|
* point). A holder that stalled past the stale deadline (slow disk / GC pause / long txn) and was
|
|
284
343
|
* stolen from must ABORT — its staged tmps carry no committed state and the stealer's journal
|
|
285
344
|
* recovery is now authoritative; committing anyway would overwrite the stealer's transaction. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync } from "node:fs";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join, relative } from "node:path";
|
|
4
4
|
import { jaccardDistance, termSet } from "../memory-vector.js";
|
|
5
5
|
import { MAX_MEMORY_BYTES } from "../memory.js";
|
|
@@ -263,7 +263,7 @@ export class FileMemoryEngineBackend {
|
|
|
263
263
|
}
|
|
264
264
|
return undefined;
|
|
265
265
|
}
|
|
266
|
-
readScope(scope, sync
|
|
266
|
+
readScope(scope, sync, probeOnly, lockToken) {
|
|
267
267
|
if (!sync && this.batchScan !== undefined) {
|
|
268
268
|
const memo = this.batchScan.get(scope);
|
|
269
269
|
if (memo !== undefined)
|
|
@@ -293,6 +293,10 @@ export class FileMemoryEngineBackend {
|
|
|
293
293
|
if (ledger) {
|
|
294
294
|
const committed = ledger[entry.id];
|
|
295
295
|
if (committed !== entry.rev) {
|
|
296
|
+
if (probeOnly)
|
|
297
|
+
return undefined;
|
|
298
|
+
if (lockToken !== undefined)
|
|
299
|
+
this.assertTxnLockOwnership(lockToken);
|
|
296
300
|
const relPath = relative(this.directoryRoot, f.path);
|
|
297
301
|
let finding = this.inboundGate(relPath, text);
|
|
298
302
|
if (finding === undefined && committed !== undefined) {
|
|
@@ -343,8 +347,11 @@ export class FileMemoryEngineBackend {
|
|
|
343
347
|
}
|
|
344
348
|
entries.push(entry);
|
|
345
349
|
}
|
|
346
|
-
if (ledgerChanged)
|
|
350
|
+
if (ledgerChanged) {
|
|
351
|
+
if (lockToken !== undefined)
|
|
352
|
+
this.assertTxnLockOwnership(lockToken);
|
|
347
353
|
this.saveLedger();
|
|
354
|
+
}
|
|
348
355
|
if (adoptedExternal.length > 0) {
|
|
349
356
|
try {
|
|
350
357
|
enqueueMemoryAnnouncement(this.controlPlaneRoot, {
|
|
@@ -377,33 +384,46 @@ export class FileMemoryEngineBackend {
|
|
|
377
384
|
this.batchScan.set(scope, entries);
|
|
378
385
|
return entries;
|
|
379
386
|
}
|
|
380
|
-
async
|
|
381
|
-
|
|
387
|
+
async readScopeAdopting(scope, timings) {
|
|
388
|
+
const probed = this.readScope(scope, true, true);
|
|
389
|
+
if (probed !== undefined)
|
|
390
|
+
return probed;
|
|
391
|
+
const lock = await this.acquireTxnLock(timings);
|
|
392
|
+
try {
|
|
393
|
+
this.assertTxnLockOwnership(lock.token);
|
|
394
|
+
this.ledger = undefined;
|
|
395
|
+
this.recoverJournal();
|
|
396
|
+
return this.readScope(scope, true, false, lock.token);
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
this.ledger = undefined;
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
lock.release();
|
|
404
|
+
}
|
|
382
405
|
}
|
|
383
|
-
|
|
384
|
-
return this.listHeadersFrom(scopes, (s) => this.
|
|
406
|
+
async listHeaders(scopes) {
|
|
407
|
+
return this.listHeadersFrom(scopes, (s) => this.readScopeAdopting(s));
|
|
385
408
|
}
|
|
386
|
-
listHeadersFrom(scopes, read) {
|
|
409
|
+
async listHeadersFrom(scopes, read) {
|
|
387
410
|
const out = [];
|
|
388
411
|
for (const scope of scopes) {
|
|
389
412
|
const dir = this.scopeDir(scope);
|
|
390
|
-
for (const e of read(scope)) {
|
|
413
|
+
for (const e of await read(scope)) {
|
|
391
414
|
out.push(headerOf(e, join(dir, `${e.slug}.md`)));
|
|
392
415
|
}
|
|
393
416
|
}
|
|
394
417
|
return out;
|
|
395
418
|
}
|
|
396
419
|
async getByIds(ids) {
|
|
397
|
-
return this.
|
|
398
|
-
}
|
|
399
|
-
getByIdsWith(ids, adopt) {
|
|
400
|
-
return this.getByIdsFrom(ids, (s) => this.readScope(s, adopt));
|
|
420
|
+
return this.getByIdsFrom(ids, (s) => this.readScopeAdopting(s));
|
|
401
421
|
}
|
|
402
|
-
getByIdsFrom(ids, read) {
|
|
422
|
+
async getByIdsFrom(ids, read) {
|
|
403
423
|
const want = new Set(ids);
|
|
404
424
|
const out = [];
|
|
405
425
|
for (const scope of Object.keys(registeredScopes(this.controlPlaneRoot))) {
|
|
406
|
-
for (const e of read(scope)) {
|
|
426
|
+
for (const e of await read(scope)) {
|
|
407
427
|
if (want.has(e.id))
|
|
408
428
|
out.push(e);
|
|
409
429
|
}
|
|
@@ -415,9 +435,9 @@ export class FileMemoryEngineBackend {
|
|
|
415
435
|
throw new Error(`memory retrieval view is read-only — ${op} must go through the backend itself`);
|
|
416
436
|
};
|
|
417
437
|
return {
|
|
418
|
-
listHeaders: async (scopes) => this.
|
|
419
|
-
getByIds: async (ids) => this.
|
|
420
|
-
search: async (query, scopes, opts) => this.
|
|
438
|
+
listHeaders: async (scopes) => this.listHeadersFrom(scopes, (s) => this.readScope(s, false)),
|
|
439
|
+
getByIds: async (ids) => this.getByIdsFrom(ids, (s) => this.readScope(s, false)),
|
|
440
|
+
search: async (query, scopes, opts) => this.searchFrom(query, scopes, opts, (s) => this.readScope(s, false)),
|
|
421
441
|
applyPatches: async () => refuse("applyPatches"),
|
|
422
442
|
getConsolidationCursor: async (scope) => this.getConsolidationCursor(scope),
|
|
423
443
|
setConsolidationCursor: async () => refuse("setConsolidationCursor"),
|
|
@@ -430,6 +450,8 @@ export class FileMemoryEngineBackend {
|
|
|
430
450
|
this.restrictedFindingKeys.add(key);
|
|
431
451
|
this.inboundFindings.push(finding);
|
|
432
452
|
}
|
|
453
|
+
restrictedIdlessScope;
|
|
454
|
+
restrictedIdlessBaseline;
|
|
433
455
|
readScopeCommitted(scope, audit) {
|
|
434
456
|
const dir = this.scopeDir(scope);
|
|
435
457
|
const isRoot = dir === this.directoryRoot;
|
|
@@ -450,6 +472,7 @@ export class FileMemoryEngineBackend {
|
|
|
450
472
|
});
|
|
451
473
|
this.ledger = undefined;
|
|
452
474
|
const ledger = this.loadLedger();
|
|
475
|
+
const idlessSeen = audit && scope === this.restrictedIdlessScope ? new Set() : undefined;
|
|
453
476
|
const entries = [];
|
|
454
477
|
for (const f of files) {
|
|
455
478
|
const rel = relative(this.directoryRoot, f.path);
|
|
@@ -468,8 +491,10 @@ export class FileMemoryEngineBackend {
|
|
|
468
491
|
continue;
|
|
469
492
|
}
|
|
470
493
|
const parsed = parseEntryFile(text);
|
|
471
|
-
if (parsed.id === undefined)
|
|
494
|
+
if (parsed.id === undefined) {
|
|
495
|
+
idlessSeen?.add(rel);
|
|
472
496
|
continue;
|
|
497
|
+
}
|
|
473
498
|
const entry = entryFromFile(text, parsed.id, f.slug, scope);
|
|
474
499
|
const committed = ledger[entry.id];
|
|
475
500
|
if (committed === entry.rev) {
|
|
@@ -547,6 +572,8 @@ export class FileMemoryEngineBackend {
|
|
|
547
572
|
}
|
|
548
573
|
}
|
|
549
574
|
}
|
|
575
|
+
if (idlessSeen !== undefined && this.restrictedIdlessBaseline === undefined)
|
|
576
|
+
this.restrictedIdlessBaseline = idlessSeen;
|
|
550
577
|
return entries;
|
|
551
578
|
}
|
|
552
579
|
idProjectionElsewhere(id, excludeAbsPath) {
|
|
@@ -567,6 +594,10 @@ export class FileMemoryEngineBackend {
|
|
|
567
594
|
}
|
|
568
595
|
restrictedAdoptionView(opts) {
|
|
569
596
|
const audit = opts?.audit === true;
|
|
597
|
+
if (audit) {
|
|
598
|
+
this.restrictedIdlessScope = typeof opts?.writeScope === "string" ? opts.writeScope : undefined;
|
|
599
|
+
this.restrictedIdlessBaseline = undefined;
|
|
600
|
+
}
|
|
570
601
|
const refuse = (op) => {
|
|
571
602
|
throw new Error(`memory adoption-restricted view is read-only — ${op} must go through the backend itself`);
|
|
572
603
|
};
|
|
@@ -579,9 +610,11 @@ export class FileMemoryEngineBackend {
|
|
|
579
610
|
setConsolidationCursor: async () => refuse("setConsolidationCursor"),
|
|
580
611
|
};
|
|
581
612
|
}
|
|
582
|
-
auditRestrictedDivergence(scopes) {
|
|
613
|
+
auditRestrictedDivergence(scopes, opts) {
|
|
583
614
|
for (const scope of scopes)
|
|
584
615
|
this.readScopeCommitted(scope, true);
|
|
616
|
+
const idlessBaseline = opts?.idlessWriteScope !== undefined && opts.idlessWriteScope === this.restrictedIdlessScope ? this.restrictedIdlessBaseline : undefined;
|
|
617
|
+
const idlessScope = idlessBaseline !== undefined ? opts?.idlessWriteScope : undefined;
|
|
585
618
|
const ledger = { ...this.loadLedger() };
|
|
586
619
|
const present = new Map();
|
|
587
620
|
let complete = true;
|
|
@@ -602,8 +635,20 @@ export class FileMemoryEngineBackend {
|
|
|
602
635
|
continue;
|
|
603
636
|
}
|
|
604
637
|
const id = parseEntryFile(text).id;
|
|
605
|
-
if (id !== undefined)
|
|
638
|
+
if (id !== undefined) {
|
|
606
639
|
present.set(id, [...(present.get(id) ?? []), relative(this.directoryRoot, f.path)]);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (scope === idlessScope && idlessBaseline !== undefined) {
|
|
643
|
+
const rel = relative(this.directoryRoot, f.path);
|
|
644
|
+
if (!idlessBaseline.has(rel)) {
|
|
645
|
+
this.recordRestrictedFinding(`idless|${rel}|${createHash("sha256").update(text, "utf8").digest("hex").slice(0, 16)}`, {
|
|
646
|
+
path: rel,
|
|
647
|
+
code: "restricted_divergence",
|
|
648
|
+
reason: "an id-less file appeared under the session's write scope after materialize, through none of this session's channels: a concurrent session's in-flight material, or an out-of-band drop — left on disk for an unrestricted harvest to adjudicate",
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
607
652
|
}
|
|
608
653
|
}
|
|
609
654
|
for (const [id, paths] of present) {
|
|
@@ -637,12 +682,9 @@ export class FileMemoryEngineBackend {
|
|
|
637
682
|
}
|
|
638
683
|
}
|
|
639
684
|
async search(query, scopes, opts) {
|
|
640
|
-
return this.
|
|
641
|
-
}
|
|
642
|
-
searchWith(query, scopes, opts, adopt) {
|
|
643
|
-
return this.searchFrom(query, scopes, opts, (s) => this.readScope(s, adopt));
|
|
685
|
+
return this.searchFrom(query, scopes, opts, (s) => this.readScopeAdopting(s));
|
|
644
686
|
}
|
|
645
|
-
searchFrom(query, scopes, opts, read) {
|
|
687
|
+
async searchFrom(query, scopes, opts, read) {
|
|
646
688
|
const limit = opts?.limit ?? 20;
|
|
647
689
|
const q = termSet(query);
|
|
648
690
|
if (q.size === 0)
|
|
@@ -650,7 +692,7 @@ export class FileMemoryEngineBackend {
|
|
|
650
692
|
const scored = [];
|
|
651
693
|
for (const scope of scopes) {
|
|
652
694
|
const dir = this.scopeDir(scope);
|
|
653
|
-
for (const e of read(scope)) {
|
|
695
|
+
for (const e of await read(scope)) {
|
|
654
696
|
const haystack = `${e.frontmatter.name ?? e.slug} ${e.frontmatter.description ?? ""} ${e.body}`;
|
|
655
697
|
const d = jaccardDistance(q, haystack);
|
|
656
698
|
if (d === null)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* This accessor is the ONLY consumer-side reading: pure, lenient, and closed over bad values —
|
|
12
12
|
* a malformed line reads as FIELD ABSENT, never an error and never a partial adoption. Nothing in
|
|
13
13
|
* the engine writes these lines; the model does (the preference-discipline paragraph teaches it),
|
|
14
|
-
* because an engine-written confirmation would
|
|
14
|
+
* because an engine-written confirmation would self-declare the usage evidence it is supposed to record.
|
|
15
15
|
*/
|
|
16
16
|
/** The parsed v2 hints. Every field is absent unless a well-formed line was found. */
|
|
17
17
|
export interface V2HeaderHints {
|
|
@@ -111,6 +111,17 @@ export declare function resolveMemoryEngineRoot(explicit?: string): string;
|
|
|
111
111
|
* collision (two distinct scopes → one dir name), so even a hash collision cannot silently merge scopes.
|
|
112
112
|
*/
|
|
113
113
|
export declare function scopeDirName(scope: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* Whether `dir`'s volume FOLDS letter case — two names differing only in case address ONE
|
|
116
|
+
* directory entry (the macOS/Windows default). Probed on the actual directory, never guessed from
|
|
117
|
+
* the platform: case-sensitive APFS volumes and case-insensitive Linux mounts both exist, and this
|
|
118
|
+
* answer gates a fail-closed isolation refusal, so it must describe the volume the scope dirs
|
|
119
|
+
* really land on — a platform guess would fail OPEN exactly on the mismatched deployments.
|
|
120
|
+
* `undefined` = the volume answered on neither leg (unwritable AND no letter-bearing entry to read
|
|
121
|
+
* against); the caller must treat that as unresolved, never as either determined answer.
|
|
122
|
+
* Exported for the layout pins; not part of the package surface.
|
|
123
|
+
*/
|
|
124
|
+
export declare function dirCaseFolds(dir: string): boolean | undefined;
|
|
114
125
|
/** Which scope owns the memory dir ROOT (undefined = unclaimed). */
|
|
115
126
|
export declare function rootScopeOf(controlDir: string): string | undefined;
|
|
116
127
|
/**
|
|
@@ -121,8 +132,26 @@ export declare function rootScopeOf(controlDir: string): string | undefined;
|
|
|
121
132
|
export declare function claimRootScope(controlDir: string, scope: string): string;
|
|
122
133
|
/** Register `scope` in the durable registry (idempotent) and return its home dir under `memoryDir`.
|
|
123
134
|
* H5 fail-closed: a dir-name collision with a DIFFERENT registered scope throws — two scopes must
|
|
124
|
-
* never share one directory (silent cross-scope merge).
|
|
125
|
-
|
|
135
|
+
* never share one directory (silent cross-scope merge). The collision test has TWO arms:
|
|
136
|
+
* - exact: two scopes whose labels are the SAME string (hash collision / caller-declared registry);
|
|
137
|
+
* - case-fold (2026-08-12): two labels differing ONLY in letter case are distinct strings to this
|
|
138
|
+
* registry yet ONE physical directory on a case-folding volume — cross-scope bleed the exact arm
|
|
139
|
+
* cannot see. The fold arm compares the FINAL dir labels (ASCII by construction, so toLowerCase
|
|
140
|
+
* is an exact fold) and only fires when the volume actually folds — probed lazily on a fold-equal
|
|
141
|
+
* candidate ({@link dirCaseFolds}), so the no-collision path never pays the probe and a
|
|
142
|
+
* case-SENSITIVE volume keeps hosting case-differing scope pairs as the genuinely distinct
|
|
143
|
+
* directories they are there. An UNPROBEABLE volume (neither probe leg answers) refuses the
|
|
144
|
+
* fold-equal pair too — an unresolved semantics guess would fail open exactly where the guard
|
|
145
|
+
* is needed. `opts.caseFoldingFs` overrides the probe for callers (and pins) that already
|
|
146
|
+
* measured the volume's semantics. The scope→dir MAPPING itself is unchanged on purpose:
|
|
147
|
+
* folding scopeDirName would re-home every existing mixed-case scope's directory.
|
|
148
|
+
* Both arms compare the scope's EFFECTIVE label — the registered one when present (a registered
|
|
149
|
+
* scope's home NEVER moves; entries live there), the formula only for a never-registered scope —
|
|
150
|
+
* so a registry whose stored labels drifted from the current formula is judged by the directories
|
|
151
|
+
* it actually serves, not by labels nobody uses. Lock-serialized (独立轨 F5). */
|
|
152
|
+
export declare function registerScope(memoryDir: string, controlDir: string, scope: string, opts?: {
|
|
153
|
+
caseFoldingFs?: boolean;
|
|
154
|
+
}): string;
|
|
126
155
|
/** All registered scopes → home dir name (`""` = the root). */
|
|
127
156
|
export declare function registeredScopes(controlDir: string): Record<string, string>;
|
|
128
157
|
/** The stable home directory of `scope`'s entry files: the root when it owns the root, else the
|
|
@@ -444,7 +473,7 @@ export declare function appendChallengeEvents(controlDir: string, events: Readon
|
|
|
444
473
|
*
|
|
445
474
|
* `eventCount` rides along for the same reason it does on {@link appendChallengeEvents}: a resolve
|
|
446
475
|
* APPENDS an event too, so it crosses the size bound exactly as a challenge does (independent
|
|
447
|
-
*
|
|
476
|
+
* falsification review, verified — hanging the disclosure off the challenge writer alone leaves a ledger
|
|
448
477
|
* that crossed the bound on a resolve silent until the next challenge, which may never come).
|
|
449
478
|
* `undefined` when nothing was appended. */
|
|
450
479
|
export declare function resolveChallengeEvent(controlDir: string, entryId: string, generation: number, reason: string, now: () => number, requestId?: string): {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { closeSync, constants as fsConstants, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
-
const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW } = fsConstants;
|
|
1
|
+
import { closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_EXCL } = fsConstants;
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -184,6 +184,119 @@ export function scopeDirName(scope) {
|
|
|
184
184
|
const hash = createHash("sha256").update(scope, "utf8").digest("hex").slice(0, 6);
|
|
185
185
|
return `${cleaned}-${hash}`;
|
|
186
186
|
}
|
|
187
|
+
const caseFoldByDirIdentity = new Map();
|
|
188
|
+
let caseFoldProbeSeq = 0;
|
|
189
|
+
function lstatOrAbsent(p) {
|
|
190
|
+
try {
|
|
191
|
+
return lstatSync(p);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (err.code === "ENOENT")
|
|
195
|
+
return undefined;
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function probeDirCaseFolds(dir) {
|
|
200
|
+
const name = `.casefold-probe-${process.pid}-${caseFoldProbeSeq++}`;
|
|
201
|
+
const writeLeg = (() => {
|
|
202
|
+
let fd;
|
|
203
|
+
try {
|
|
204
|
+
fd = openSync(join(dir, name), O_WRONLY | O_CREAT | O_EXCL, 0o600);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
let identity;
|
|
210
|
+
try {
|
|
211
|
+
const st = fstatSync(fd);
|
|
212
|
+
identity = { dev: st.dev, ino: st.ino };
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
closeSync(fd);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
let swapped;
|
|
221
|
+
let orig;
|
|
222
|
+
try {
|
|
223
|
+
swapped = lstatOrAbsent(join(dir, name.toUpperCase()));
|
|
224
|
+
orig = swapped === undefined ? lstatOrAbsent(join(dir, name)) : undefined;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
if (swapped !== undefined) {
|
|
230
|
+
return identity === undefined || (swapped.dev === identity.dev && swapped.ino === identity.ino) ? true : undefined;
|
|
231
|
+
}
|
|
232
|
+
if (orig === undefined || identity === undefined)
|
|
233
|
+
return undefined;
|
|
234
|
+
return orig.dev === identity.dev && orig.ino === identity.ino ? false : undefined;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
try {
|
|
238
|
+
rmSync(join(dir, name), { force: true });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
if (writeLeg !== undefined)
|
|
245
|
+
return writeLeg;
|
|
246
|
+
try {
|
|
247
|
+
for (const entry of readdirSync(dir)) {
|
|
248
|
+
if (!/[A-Za-z]/.test(entry))
|
|
249
|
+
continue;
|
|
250
|
+
const swapped = entry.toLowerCase() !== entry ? entry.toLowerCase() : entry.toUpperCase();
|
|
251
|
+
try {
|
|
252
|
+
const orig = lstatOrAbsent(join(dir, entry));
|
|
253
|
+
if (orig === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
const other = lstatOrAbsent(join(dir, swapped));
|
|
256
|
+
const again = lstatOrAbsent(join(dir, entry));
|
|
257
|
+
if (again === undefined || again.dev !== orig.dev || again.ino !== orig.ino)
|
|
258
|
+
continue;
|
|
259
|
+
if (other === undefined)
|
|
260
|
+
return false;
|
|
261
|
+
return other.dev === orig.dev && other.ino === orig.ino;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
export function dirCaseFolds(dir) {
|
|
273
|
+
let key;
|
|
274
|
+
try {
|
|
275
|
+
ensureDirExists(dir);
|
|
276
|
+
const s = statSync(dir);
|
|
277
|
+
key = `${s.dev}:${s.ino}`;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
}
|
|
281
|
+
if (key !== undefined) {
|
|
282
|
+
const cached = caseFoldByDirIdentity.get(key);
|
|
283
|
+
if (cached !== undefined)
|
|
284
|
+
return cached;
|
|
285
|
+
}
|
|
286
|
+
const folds = probeDirCaseFolds(dir);
|
|
287
|
+
if (folds === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
try {
|
|
290
|
+
const after = statSync(dir);
|
|
291
|
+
if (key === undefined || key !== `${after.dev}:${after.ino}`)
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
caseFoldByDirIdentity.set(key, folds);
|
|
298
|
+
return folds;
|
|
299
|
+
}
|
|
187
300
|
function readScopesRecord(controlDir) {
|
|
188
301
|
const path = join(controlDir, SCOPES_FILE);
|
|
189
302
|
let raw;
|
|
@@ -217,6 +330,11 @@ function readScopesRecord(controlDir) {
|
|
|
217
330
|
for (const [k, v] of Object.entries(rec.scopes)) {
|
|
218
331
|
if (typeof v !== "string")
|
|
219
332
|
throw new ControlPlaneCorruptError(`scope registry entry ${JSON.stringify(k)} is not a string: ${path}`);
|
|
333
|
+
if ((v === "") !== (k === rec.rootScope)) {
|
|
334
|
+
throw new ControlPlaneCorruptError(v === ""
|
|
335
|
+
? `scope registry maps ${JSON.stringify(k)} to the root home but rootScope is ${rec.rootScope === undefined ? "unclaimed" : JSON.stringify(rec.rootScope)}: ${path}`
|
|
336
|
+
: `scope registry maps the root scope ${JSON.stringify(k)} to subdir ${JSON.stringify(v)} instead of the root home: ${path}`);
|
|
337
|
+
}
|
|
220
338
|
}
|
|
221
339
|
scopes = rec.scopes;
|
|
222
340
|
}
|
|
@@ -255,20 +373,26 @@ export function claimRootScope(controlDir, scope) {
|
|
|
255
373
|
return { next: { rootScope: scope, scopes: { ...rec.scopes, [scope]: "" } }, result: scope };
|
|
256
374
|
});
|
|
257
375
|
}
|
|
258
|
-
export function registerScope(memoryDir, controlDir, scope) {
|
|
376
|
+
export function registerScope(memoryDir, controlDir, scope, opts) {
|
|
259
377
|
return lockedScopesUpdate(controlDir, (rec) => {
|
|
260
|
-
const dirName = rec.rootScope === scope ? "" : scopeDirName(scope);
|
|
378
|
+
const dirName = rec.rootScope === scope ? "" : (rec.scopes?.[scope] ?? scopeDirName(scope));
|
|
261
379
|
for (const [other, otherDir] of Object.entries(rec.scopes ?? {})) {
|
|
262
|
-
if (other
|
|
380
|
+
if (other === scope || otherDir === "")
|
|
381
|
+
continue;
|
|
382
|
+
if (otherDir === dirName) {
|
|
263
383
|
throw new ControlPlaneCorruptError(`scope directory collision: ${JSON.stringify(scope)} and ${JSON.stringify(other)} both map to ${JSON.stringify(dirName)} — refusing (fail-closed)`);
|
|
264
384
|
}
|
|
385
|
+
if (dirName !== "" && otherDir.toLowerCase() === dirName.toLowerCase()) {
|
|
386
|
+
const folds = opts?.caseFoldingFs ?? dirCaseFolds(memoryDir);
|
|
387
|
+
if (folds !== false) {
|
|
388
|
+
throw new ControlPlaneCorruptError(`scope directory collision on a case-folding filesystem: ${JSON.stringify(scope)} → ${JSON.stringify(dirName)} and ${JSON.stringify(other)} → ${JSON.stringify(otherDir)} are one physical directory under ${memoryDir}` +
|
|
389
|
+
`${folds === undefined ? " (volume case semantics could not be probed — refusing the fold-equal pair rather than risking a silent cross-scope merge)" : ""} — refusing (fail-closed)`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
265
392
|
}
|
|
266
393
|
if (rec.scopes?.[scope] === undefined) {
|
|
267
394
|
return { next: { ...rec, scopes: { ...rec.scopes, [scope]: dirName } }, result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
|
|
268
395
|
}
|
|
269
|
-
if (rec.scopes[scope] !== dirName) {
|
|
270
|
-
return { result: rec.scopes[scope] === "" ? memoryDir : join(memoryDir, rec.scopes[scope]) };
|
|
271
|
-
}
|
|
272
396
|
return { result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
|
|
273
397
|
});
|
|
274
398
|
}
|
|
@@ -224,11 +224,12 @@ export interface MemorySessionHandle {
|
|
|
224
224
|
* leave the injection path reading them anyway. Absent ⇒ the normal "live file wins" behavior. */
|
|
225
225
|
indexOnDiskUntrusted?: boolean;
|
|
226
226
|
/** True ⇔ this session materialized through the ADOPTION-RESTRICTED (committed-view) read face:
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
227
|
+
* the caller declared the session unable to persist (an explicit session-level verdict — never
|
|
228
|
+
* inferred down here, and never derived from the plane's shape: a read-only layering
|
|
229
|
+
* (`writeScope === null`) keeps its ordinary adopt-on-read semantics and does NOT set this).
|
|
230
|
+
* Restricted sessions read committed state only (ledger + control-plane shadow); disk divergence
|
|
231
|
+
* with no transaction backing is neither adopted into the committed account nor served, and
|
|
232
|
+
* `inject` reads the materialize-time index text instead of the live on-disk file. */
|
|
232
233
|
adoptionRestricted?: boolean;
|
|
233
234
|
}
|
|
234
235
|
/** Stable rejection codes a harvest gate can produce (model-visible gate events — 镜头 I). */
|
|
@@ -162,7 +162,7 @@ export declare function composeSelectiveBody(manifestText: string, selected: Mem
|
|
|
162
162
|
/** A `(scope, id)` composite key for the multi-scope durable de-dup (design/84 Seam A decision 5). Note ids
|
|
163
163
|
* are only unique WITHIN a scope, so the cross-scope `priorSurfacedKeys`/`surfacedKeys` set MUST be keyed on
|
|
164
164
|
* both. Core owns the FORMAT so a caller can't drift the wire format (decision 7) — the encoding is OPAQUE
|
|
165
|
-
* and length-prefixes the scope so a scope/id containing the separator cannot
|
|
165
|
+
* and length-prefixes the scope so a scope/id containing the separator cannot self-declare a collision.
|
|
166
166
|
*
|
|
167
167
|
* ONE-WAY by design (codex MINOR (d)): the key is only ever PRODUCED here (`surfacedKeys`) and compared by
|
|
168
168
|
* STRING EQUALITY against the caller's persisted set ({@link LayeredRecallOptions.priorSurfacedKeys}, applied
|