@sema-agent/core 5.25.0 → 5.27.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 +115 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/agent-transcript-tool.d.ts +5 -2
- package/dist/agents/agent-transcript-tool.js +2 -1
- package/dist/agents/send-message-tool.d.ts +4 -1
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +6 -2
- package/dist/agents/subagent.js +5 -0
- package/dist/core/checkpoint-store.d.ts +7 -2
- package/dist/core/hooks.d.ts +39 -4
- package/dist/core/hooks.js +18 -14
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +53 -6
- package/dist/core/memory-engine/engine.js +43 -11
- package/dist/core/memory-engine/file-backend.d.ts +81 -0
- package/dist/core/memory-engine/file-backend.js +250 -24
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/types.d.ts +8 -1
- package/dist/core/memory-vector.d.ts +6 -1
- package/dist/core/memory-vector.js +14 -4
- package/dist/core/memory.js +1 -6
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/permission-rule-model.d.ts +70 -5
- package/dist/core/permission-rule-model.js +58 -0
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +99 -26
- package/dist/core/runner/prepare-task.d.ts +9 -3
- package/dist/core/runner/prepare-task.js +60 -15
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.d.ts +4 -3
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/task-registry.d.ts +6 -3
- package/dist/core/tool-policy.d.ts +9 -2
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +103 -7
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/loop/types.d.ts +10 -3
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +5 -3
- package/dist/orchestration/workflow.d.ts +9 -6
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/checkpoint-store.d.ts +2 -1
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/index.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +5 -4
- package/package.json +1 -1
|
@@ -34,6 +34,11 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
|
|
|
34
34
|
}
|
|
35
35
|
export const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry — the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
|
|
36
36
|
export 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 — write them yourself; nothing fills them in for you.";
|
|
37
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately above concern a READ-ONLY memory store: any guidance in them to record, update, or tombstone an entry cannot be applied to that store this session — surface it to the user instead of claiming it done.";
|
|
38
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here — surface it to the user instead of claiming it done.";
|
|
39
|
+
export const MEMORY_READONLY_NOTICE = `# Memory
|
|
40
|
+
|
|
41
|
+
You have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel — the engine will not accept writes into the memory store. If the user asks you to remember something for later, say plainly that you cannot save it in this session — never claim to have noted or remembered it.`;
|
|
37
42
|
export const MEMORY_INDEX_MAX_LINES = 200;
|
|
38
43
|
export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
39
44
|
export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
|
|
@@ -41,6 +46,7 @@ export const DEFAULT_MAX_MEMORY_FILES = 500;
|
|
|
41
46
|
export const DEFAULT_HARVEST_DEADLINE_MS = 5_000;
|
|
42
47
|
export const DEFAULT_HARVEST_FILE_BUDGET = 2_000;
|
|
43
48
|
export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
49
|
+
let indexCaptureSeq = 0;
|
|
44
50
|
export class MemoryEngine {
|
|
45
51
|
backend;
|
|
46
52
|
memoryDir;
|
|
@@ -317,9 +323,13 @@ export class MemoryEngine {
|
|
|
317
323
|
catch {
|
|
318
324
|
}
|
|
319
325
|
}
|
|
320
|
-
async materialize(scopes, writeScope) {
|
|
326
|
+
async materialize(scopes, writeScope, opts) {
|
|
321
327
|
ensureDirExists(this.memoryDir);
|
|
322
328
|
ensureDirExists(this.controlDir);
|
|
329
|
+
const restricted = opts?.adoptionRestricted === true;
|
|
330
|
+
const readBackend = restricted
|
|
331
|
+
? (this.backend.restrictedAdoptionView?.({ audit: true }) ?? this.backend)
|
|
332
|
+
: this.backend;
|
|
323
333
|
if (writeScope !== null)
|
|
324
334
|
claimRootScope(this.controlDir, writeScope);
|
|
325
335
|
const scopeDirs = new Map();
|
|
@@ -330,8 +340,8 @@ export class MemoryEngine {
|
|
|
330
340
|
for (const [scope, dir] of scopeDirs)
|
|
331
341
|
this.chmodScopeTree(dir, 0o755, 0o644, { excludeTopDirs: this.siblingScopeDirNames(dir, scope) });
|
|
332
342
|
const allScopes = [...scopeDirs.keys()];
|
|
333
|
-
const headers = await
|
|
334
|
-
const entries = await
|
|
343
|
+
const headers = await readBackend.listHeaders(allScopes);
|
|
344
|
+
const entries = await readBackend.getByIds(headers.map((h) => h.id));
|
|
335
345
|
const byScope = new Map();
|
|
336
346
|
for (const e of entries) {
|
|
337
347
|
const arr = byScope.get(e.scope) ?? [];
|
|
@@ -351,6 +361,7 @@ export class MemoryEngine {
|
|
|
351
361
|
repoIndexTargets: new Map(),
|
|
352
362
|
indexBaselineLines: 0,
|
|
353
363
|
indexText: "",
|
|
364
|
+
...(restricted ? { adoptionRestricted: true } : {}),
|
|
354
365
|
};
|
|
355
366
|
if (writeScope !== null)
|
|
356
367
|
ensureDirExists(handle.writableRoot);
|
|
@@ -376,7 +387,8 @@ export class MemoryEngine {
|
|
|
376
387
|
const { deleted: _stubDrop, ...stubFm } = entry.frontmatter;
|
|
377
388
|
const text = stub ? serializeEntryFile({ id: entry.id, frontmatter: stubFm, body: STUB_ARCHIVED_LINE }) : fullText;
|
|
378
389
|
const target = join(dir, `${entry.slug}.md`);
|
|
379
|
-
|
|
390
|
+
if (!(zeroCopy && restricted))
|
|
391
|
+
this.writeIfChanged(target, text);
|
|
380
392
|
const onDiskRev = stub ? revOfText(text, entry.id) : entry.rev;
|
|
381
393
|
const path = canonicalize(target);
|
|
382
394
|
handle.materialized.push({
|
|
@@ -409,7 +421,7 @@ export class MemoryEngine {
|
|
|
409
421
|
this.discloseAnnounceFailure("index-gate enqueue", err);
|
|
410
422
|
}
|
|
411
423
|
}
|
|
412
|
-
const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null, ignoreOnDisk: indexGate !== undefined });
|
|
424
|
+
const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null && !restricted, ignoreOnDisk: indexGate !== undefined || restricted });
|
|
413
425
|
handle.indexBaselineLines = countIndexLines(indexText);
|
|
414
426
|
handle.indexText = indexText;
|
|
415
427
|
for (const [scope, dir] of scopeDirs) {
|
|
@@ -425,8 +437,9 @@ export class MemoryEngine {
|
|
|
425
437
|
inject(handle, opts) {
|
|
426
438
|
const writeChannel = handle.writeScope !== null && opts?.writeToolMounted !== false;
|
|
427
439
|
const instruction = writeChannel ? buildMemoryInstruction(handle.writableRoot) : "";
|
|
440
|
+
const readOnlyNotice = handle.writeScope === null ? MEMORY_READONLY_NOTICE : undefined;
|
|
428
441
|
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
429
|
-
const onDisk = handle.indexOnDiskUntrusted === true ? undefined : readSafe(indexPath);
|
|
442
|
+
const onDisk = handle.indexOnDiskUntrusted === true || handle.adoptionRestricted === true ? undefined : readSafe(indexPath);
|
|
430
443
|
const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
|
|
431
444
|
const truncated = truncateIndex(indexText);
|
|
432
445
|
const index = composeMemoryBlock(truncated, handle.writeScope ?? handle.scopes[0] ?? "memory");
|
|
@@ -440,14 +453,17 @@ export class MemoryEngine {
|
|
|
440
453
|
if (drained.queue.length > 0 || drained.folded > 0) {
|
|
441
454
|
announcements = drained.queue;
|
|
442
455
|
announceBlock = renderAnnouncements(drained.queue, drained.folded);
|
|
456
|
+
if (handle.writeScope === null)
|
|
457
|
+
announceBlock = `${announceBlock}\n\n${MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA}`;
|
|
443
458
|
}
|
|
444
459
|
}
|
|
445
460
|
catch (err) {
|
|
446
461
|
this.discloseAnnounceFailure("inject drain", err);
|
|
447
462
|
}
|
|
448
|
-
const block = [instruction, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
463
|
+
const block = [instruction || readOnlyNotice, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
449
464
|
return {
|
|
450
465
|
instruction,
|
|
466
|
+
...(readOnlyNotice !== undefined ? { readOnlyNotice } : {}),
|
|
451
467
|
...(index !== undefined ? { index } : {}),
|
|
452
468
|
...(announcements !== undefined ? { announcements } : {}),
|
|
453
469
|
...(announceBlock !== undefined ? { announceBlock } : {}),
|
|
@@ -459,8 +475,14 @@ export class MemoryEngine {
|
|
|
459
475
|
const root = handle.writableRoot;
|
|
460
476
|
if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`))
|
|
461
477
|
return { ok: true };
|
|
462
|
-
if (handle.writeScope === null)
|
|
463
|
-
return {
|
|
478
|
+
if (handle.writeScope === null) {
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
code: "read_only_layering",
|
|
482
|
+
reason: "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written.",
|
|
483
|
+
muted: false,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
464
486
|
const findings = [];
|
|
465
487
|
const nameFinding = scanMemoryFileName(relative(root, canonicalPath));
|
|
466
488
|
if (nameFinding !== undefined)
|
|
@@ -512,10 +534,20 @@ export class MemoryEngine {
|
|
|
512
534
|
warnings: [],
|
|
513
535
|
};
|
|
514
536
|
const writeScope = handle.writeScope;
|
|
515
|
-
if (writeScope === null) {
|
|
537
|
+
if (writeScope === null || opts?.admitNothing !== undefined) {
|
|
538
|
+
try {
|
|
539
|
+
this.backend.auditRestrictedDivergence?.([
|
|
540
|
+
...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])]),
|
|
541
|
+
]);
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
report.warnings.push(`restricted-session divergence audit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
545
|
+
}
|
|
516
546
|
const roFindings = this.backend.drainInboundFindings?.();
|
|
517
547
|
if (roFindings !== undefined && roFindings.length > 0)
|
|
518
548
|
report.inboundFindings = roFindings;
|
|
549
|
+
if (opts?.admitNothing !== undefined)
|
|
550
|
+
report.warnings.push(opts.admitNothing.reason);
|
|
519
551
|
return report;
|
|
520
552
|
}
|
|
521
553
|
try {
|
|
@@ -1240,7 +1272,7 @@ export class MemoryEngine {
|
|
|
1240
1272
|
}
|
|
1241
1273
|
}
|
|
1242
1274
|
if (!landed)
|
|
1243
|
-
writeFileSync(`${dest}.${process.pid}`, text, { encoding: "utf8", flag: "wx" });
|
|
1275
|
+
writeFileSync(`${dest}.${process.pid}.${indexCaptureSeq++}`, text, { encoding: "utf8", flag: "wx" });
|
|
1244
1276
|
captured = true;
|
|
1245
1277
|
}
|
|
1246
1278
|
catch (err) {
|
|
@@ -128,6 +128,15 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
128
128
|
/** The names of sibling scope subdirs to exclude when scanning the ROOT scope's own nested content. */
|
|
129
129
|
private excludedSubdirNames;
|
|
130
130
|
private scopeDir;
|
|
131
|
+
/** B2/C-F9 — contain one inbound-gate REJECT, shared by the adopting read (`readScope` sync) and
|
|
132
|
+
* the adoption-restricted committed view (`readScopeCommitted`) so the two doors can never drift:
|
|
133
|
+
* report the finding, capture the SCANNED bytes into control-plane quarantine (never a path
|
|
134
|
+
* re-read — TOCTOU), restore the committed shadow over the suspect where one exists (the write
|
|
135
|
+
* lands by `rename`, which REPLACES a link at the final component instead of traversing it —
|
|
136
|
+
* pinned by test), and escalate any deviation as a report-visible `quarantine_failed`. Returns
|
|
137
|
+
* the shadow text that now backs the entry (undefined ⇔ never committed / shadow-less legacy —
|
|
138
|
+
* each caller decides what leaving the set means on its channel). */
|
|
139
|
+
private containInboundReject;
|
|
131
140
|
/** B2 inbound gate for content that did NOT come through applyPatches (git pull / hand edits /
|
|
132
141
|
* a hand-seeded file): secret + cap + structure. Returns the rejection, or undefined (clean). */
|
|
133
142
|
private inboundGate;
|
|
@@ -142,8 +151,10 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
142
151
|
private readScope;
|
|
143
152
|
listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
|
|
144
153
|
private listHeadersWith;
|
|
154
|
+
private listHeadersFrom;
|
|
145
155
|
getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
|
|
146
156
|
private getByIdsWith;
|
|
157
|
+
private getByIdsFrom;
|
|
147
158
|
/**
|
|
148
159
|
* design/178 §3 (ruled 2026-08-08) — a SIDE-EFFECT-FREE projection of this backend for the session's
|
|
149
160
|
* RETRIEVAL face (`memory_search` / `memory_get`).
|
|
@@ -162,11 +173,81 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
162
173
|
* WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
|
|
163
174
|
*/
|
|
164
175
|
retrievalView(): MemoryBackend;
|
|
176
|
+
/** Dedup keys for divergence findings the restricted view already recorded on THIS instance: one
|
|
177
|
+
* materialize reads a scope several times (listHeaders, then getByIds per scope), and a
|
|
178
|
+
* checkpoint-cadence harvest re-audits the same still-pending state — without the key, one
|
|
179
|
+
* untouched divergence would mint a finding per read. Per-process on purpose (no durable state to
|
|
180
|
+
* lifecycle: a cross-process resume re-discloses once, which is the desired "still pending"
|
|
181
|
+
* notice); a divergence whose bytes CHANGE mints a new key and is reported again. */
|
|
182
|
+
private restrictedFindingKeys;
|
|
183
|
+
private recordRestrictedFinding;
|
|
184
|
+
/**
|
|
185
|
+
* The ADOPTION-RESTRICTED read of one scope: serve the COMMITTED account (ledger + control-plane
|
|
186
|
+
* shadow) and never advance it. This is the tamper boundary for a session with no write intent:
|
|
187
|
+
* the ordinary `readScope(sync=true)` ADOPTS disk divergence as the new committed baseline
|
|
188
|
+
* regardless of session intent, so a byte landed by a side channel (e.g. a shell write under a
|
|
189
|
+
* session whose persistence is declared unavailable, or into a read-only layering) would be
|
|
190
|
+
* committed by the very session that was told it cannot persist — its own checkpoint-resume
|
|
191
|
+
* materialize included. Here instead:
|
|
192
|
+
* - disk rev == committed rev ⇒ served as-is (it IS the committed content);
|
|
193
|
+
* - divergence whose bytes equal the committed SHADOW ⇒ served as-is: the shadow is written at
|
|
194
|
+
* the applyPatches commit point, so matching it is transactional proof that a concurrent
|
|
195
|
+
* writable session committed this state and only this call's ledger snapshot raced it —
|
|
196
|
+
* never treated as tampering;
|
|
197
|
+
* - other divergence of a shadow-backed entry ⇒ the SHADOW (committed content) is served; the
|
|
198
|
+
* divergent disk bytes stay in place UNLESS they fail the inbound gate (the existing
|
|
199
|
+
* quarantine door, unchanged under restriction). Scan-clean divergence is never quarantined,
|
|
200
|
+
* overwritten, or adopted — it stays on disk as deferred external state a later unrestricted
|
|
201
|
+
* session adjudicates normally;
|
|
202
|
+
* - a divergent entry with NO shadow (shadow-less legacy), and a never-committed id-bearing
|
|
203
|
+
* file, have no committed content to serve ⇒ WITHHELD with a loud finding, never served from
|
|
204
|
+
* disk, never adopted.
|
|
205
|
+
* The ledger is reloaded from disk on every call (a stale cache would misread a concurrent commit
|
|
206
|
+
* as divergence) and NEVER written here; the only writes this path may perform are the
|
|
207
|
+
* gate-failing containment above and a shadow backfill for a rev-matching entry (recording
|
|
208
|
+
* committed content — the account itself does not move).
|
|
209
|
+
*
|
|
210
|
+
* `audit` picks the mount: true = the engine's materialize/harvest boundary (findings recorded,
|
|
211
|
+
* gate-failing divergence contained); false = the retrieval-tool face (a PURE committed read —
|
|
212
|
+
* no findings, no containment, no writes: a model lookup must be side-effect-free).
|
|
213
|
+
*/
|
|
214
|
+
private readScopeCommitted;
|
|
215
|
+
/** Does `id` have a projection file anywhere in the store OTHER than `excludeAbsPath`? Returns
|
|
216
|
+
* that projection's rel path, or undefined. Early-exits on the first hit; only consulted on the
|
|
217
|
+
* rare divergent-with-shadow path (the scope-binding guard above), never on clean reads. */
|
|
218
|
+
private idProjectionElsewhere;
|
|
219
|
+
/**
|
|
220
|
+
* The ADOPTION-RESTRICTED projection of this backend (see {@link readScopeCommitted}) — what a
|
|
221
|
+
* session with no write intent materializes and retrieves through. The CALLER decides restriction
|
|
222
|
+
* (an explicit verdict: a read-only layering, or persistence declared unavailable) and mounts this
|
|
223
|
+
* view; the backend never guesses a session's intent from its own state.
|
|
224
|
+
* - `audit: true` (the engine's materialize/harvest mount): divergence findings are recorded for
|
|
225
|
+
* the harvest report + announcement queue, and gate-FAILING divergence is contained by the
|
|
226
|
+
* existing quarantine door;
|
|
227
|
+
* - `audit: false` (the retrieval-tool mount): a pure committed-view read — no findings, no
|
|
228
|
+
* containment, no writes.
|
|
229
|
+
* WRITES ARE REFUSED loudly — same posture as {@link retrievalView}.
|
|
230
|
+
*/
|
|
231
|
+
restrictedAdoptionView(opts?: {
|
|
232
|
+
audit?: boolean;
|
|
233
|
+
}): MemoryBackend;
|
|
234
|
+
/**
|
|
235
|
+
* The harvest-boundary divergence sweep for an adoption-restricted session (the engine's
|
|
236
|
+
* zero-admission harvest arm calls this BEFORE draining findings): re-run the committed-view audit
|
|
237
|
+
* over the session's scopes — bytes landed AFTER materialize (a mid-session shell write) are
|
|
238
|
+
* detected here, at the very boundary where an unrestricted harvest would have admitted them —
|
|
239
|
+
* then check the committed account for entries MISSING from disk entirely (an unattributed delete
|
|
240
|
+
* is divergence too). Ledger rows carry no scope, so presence is judged across every registered
|
|
241
|
+
* scope, the same resolution `getByIds` uses; a rename or cross-scope move therefore never reads
|
|
242
|
+
* as missing. Non-destructive throughout: nothing is restored, forgotten, or adopted here.
|
|
243
|
+
*/
|
|
244
|
+
auditRestrictedDivergence(scopes: readonly string[]): void;
|
|
165
245
|
/** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
|
|
166
246
|
search(query: string, scopes: readonly string[], opts?: {
|
|
167
247
|
limit?: number;
|
|
168
248
|
}): Promise<ScoredMemoryEntry[]>;
|
|
169
249
|
private searchWith;
|
|
250
|
+
private searchFrom;
|
|
170
251
|
/**
|
|
171
252
|
* H4 — the entry transaction, staged and journaled:
|
|
172
253
|
* 1. RELOAD the ledger from disk (验收 M2: a concurrently-committed transaction from another
|
|
@@ -229,6 +229,29 @@ export class FileMemoryEngineBackend {
|
|
|
229
229
|
scopeDir(scope) {
|
|
230
230
|
return registerScope(this.directoryRoot, this.controlPlaneRoot, scope);
|
|
231
231
|
}
|
|
232
|
+
containInboundReject(finding, absPath, text, entryId, committed) {
|
|
233
|
+
this.inboundFindings.push(finding);
|
|
234
|
+
const q = quarantineAndTombstone(absPath, text, join(this.controlPlaneRoot, QUARANTINE_DIR), this.now);
|
|
235
|
+
const shadowText = committed !== undefined ? this.readCommittedShadow(entryId) : undefined;
|
|
236
|
+
let restoredShadow = false;
|
|
237
|
+
if (shadowText !== undefined) {
|
|
238
|
+
try {
|
|
239
|
+
atomicWriteFileSync(absPath, shadowText);
|
|
240
|
+
restoredShadow = true;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (q.detail !== undefined || (!q.removed && !restoredShadow)) {
|
|
246
|
+
const contained = q.removed || restoredShadow;
|
|
247
|
+
this.inboundFindings.push({
|
|
248
|
+
path: finding.path,
|
|
249
|
+
code: "quarantine_failed",
|
|
250
|
+
reason: `quarantine escalation: ${q.detail ?? "suspect copy not captured"}${contained ? "" : " — rejected content may still be on the model-visible plane"}`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return shadowText;
|
|
254
|
+
}
|
|
232
255
|
inboundGate(relPath, text) {
|
|
233
256
|
const nameFinding = scanMemoryFileName(relPath);
|
|
234
257
|
if (nameFinding !== undefined)
|
|
@@ -288,27 +311,7 @@ export class FileMemoryEngineBackend {
|
|
|
288
311
|
}
|
|
289
312
|
}
|
|
290
313
|
if (finding) {
|
|
291
|
-
this.
|
|
292
|
-
const q = quarantineAndTombstone(f.path, text, join(this.controlPlaneRoot, QUARANTINE_DIR), this.now);
|
|
293
|
-
const shadowText = committed !== undefined ? this.readCommittedShadow(entry.id) : undefined;
|
|
294
|
-
let restoredShadow = false;
|
|
295
|
-
if (shadowText !== undefined) {
|
|
296
|
-
try {
|
|
297
|
-
atomicWriteFileSync(f.path, shadowText);
|
|
298
|
-
restoredShadow = true;
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
}
|
|
302
|
-
entry = entryFromFile(shadowText, entry.id, f.slug, scope);
|
|
303
|
-
}
|
|
304
|
-
if (q.detail !== undefined || (!q.removed && !restoredShadow)) {
|
|
305
|
-
const contained = q.removed || restoredShadow;
|
|
306
|
-
this.inboundFindings.push({
|
|
307
|
-
path: finding.path,
|
|
308
|
-
code: "quarantine_failed",
|
|
309
|
-
reason: `quarantine escalation: ${q.detail ?? "suspect copy not captured"}${contained ? "" : " — rejected content may still be on the model-visible plane"}`,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
314
|
+
const shadowText = this.containInboundReject(finding, f.path, text, entry.id, committed);
|
|
312
315
|
if (shadowText === undefined) {
|
|
313
316
|
if (committed !== undefined) {
|
|
314
317
|
delete ledger[entry.id];
|
|
@@ -316,6 +319,7 @@ export class FileMemoryEngineBackend {
|
|
|
316
319
|
}
|
|
317
320
|
continue;
|
|
318
321
|
}
|
|
322
|
+
entry = entryFromFile(shadowText, entry.id, f.slug, scope);
|
|
319
323
|
}
|
|
320
324
|
else {
|
|
321
325
|
if (committed !== undefined)
|
|
@@ -377,10 +381,13 @@ export class FileMemoryEngineBackend {
|
|
|
377
381
|
return this.listHeadersWith(scopes, true);
|
|
378
382
|
}
|
|
379
383
|
listHeadersWith(scopes, adopt) {
|
|
384
|
+
return this.listHeadersFrom(scopes, (s) => this.readScope(s, adopt));
|
|
385
|
+
}
|
|
386
|
+
listHeadersFrom(scopes, read) {
|
|
380
387
|
const out = [];
|
|
381
388
|
for (const scope of scopes) {
|
|
382
389
|
const dir = this.scopeDir(scope);
|
|
383
|
-
for (const e of
|
|
390
|
+
for (const e of read(scope)) {
|
|
384
391
|
out.push(headerOf(e, join(dir, `${e.slug}.md`)));
|
|
385
392
|
}
|
|
386
393
|
}
|
|
@@ -390,10 +397,13 @@ export class FileMemoryEngineBackend {
|
|
|
390
397
|
return this.getByIdsWith(ids, true);
|
|
391
398
|
}
|
|
392
399
|
getByIdsWith(ids, adopt) {
|
|
400
|
+
return this.getByIdsFrom(ids, (s) => this.readScope(s, adopt));
|
|
401
|
+
}
|
|
402
|
+
getByIdsFrom(ids, read) {
|
|
393
403
|
const want = new Set(ids);
|
|
394
404
|
const out = [];
|
|
395
405
|
for (const scope of Object.keys(registeredScopes(this.controlPlaneRoot))) {
|
|
396
|
-
for (const e of
|
|
406
|
+
for (const e of read(scope)) {
|
|
397
407
|
if (want.has(e.id))
|
|
398
408
|
out.push(e);
|
|
399
409
|
}
|
|
@@ -413,10 +423,226 @@ export class FileMemoryEngineBackend {
|
|
|
413
423
|
setConsolidationCursor: async () => refuse("setConsolidationCursor"),
|
|
414
424
|
};
|
|
415
425
|
}
|
|
426
|
+
restrictedFindingKeys = new Set();
|
|
427
|
+
recordRestrictedFinding(key, finding) {
|
|
428
|
+
if (this.restrictedFindingKeys.has(key))
|
|
429
|
+
return;
|
|
430
|
+
this.restrictedFindingKeys.add(key);
|
|
431
|
+
this.inboundFindings.push(finding);
|
|
432
|
+
}
|
|
433
|
+
readScopeCommitted(scope, audit) {
|
|
434
|
+
const dir = this.scopeDir(scope);
|
|
435
|
+
const isRoot = dir === this.directoryRoot;
|
|
436
|
+
const files = scanEntryFiles(dir, {
|
|
437
|
+
exclude: isRoot ? this.excludedSubdirNames(scope) : undefined,
|
|
438
|
+
onSkip: audit
|
|
439
|
+
? (p, kind) => {
|
|
440
|
+
if (kind !== "unreadable")
|
|
441
|
+
return;
|
|
442
|
+
const rel = relative(this.directoryRoot, p);
|
|
443
|
+
this.recordRestrictedFinding(`unreadable|${rel}`, {
|
|
444
|
+
path: rel,
|
|
445
|
+
code: "unreadable",
|
|
446
|
+
reason: "the path could not be read (lstat/readdir failed) — the restricted-session divergence audit is incomplete under it",
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
: undefined,
|
|
450
|
+
});
|
|
451
|
+
this.ledger = undefined;
|
|
452
|
+
const ledger = this.loadLedger();
|
|
453
|
+
const entries = [];
|
|
454
|
+
for (const f of files) {
|
|
455
|
+
const rel = relative(this.directoryRoot, f.path);
|
|
456
|
+
let text;
|
|
457
|
+
try {
|
|
458
|
+
text = readFileSync(f.path, "utf8");
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
if (audit) {
|
|
462
|
+
this.recordRestrictedFinding(`unreadable|${rel}`, {
|
|
463
|
+
path: rel,
|
|
464
|
+
code: "unreadable",
|
|
465
|
+
reason: "memory entry file could not be read — it is missing from this restricted session's view",
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const parsed = parseEntryFile(text);
|
|
471
|
+
if (parsed.id === undefined)
|
|
472
|
+
continue;
|
|
473
|
+
const entry = entryFromFile(text, parsed.id, f.slug, scope);
|
|
474
|
+
const committed = ledger[entry.id];
|
|
475
|
+
if (committed === entry.rev) {
|
|
476
|
+
if (audit && this.readCommittedShadow(entry.id) === undefined) {
|
|
477
|
+
try {
|
|
478
|
+
atomicWriteFileSync(this.shadowPath(entry.id), text);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
entries.push(entry);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const shadowText = committed !== undefined ? this.readCommittedShadow(entry.id) : undefined;
|
|
487
|
+
if (shadowText !== undefined) {
|
|
488
|
+
const shadowEntry = entryFromFile(shadowText, entry.id, f.slug, scope);
|
|
489
|
+
if (shadowEntry.rev === entry.rev) {
|
|
490
|
+
entries.push(entry);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const otherProjection = this.idProjectionElsewhere(entry.id, f.path);
|
|
494
|
+
if (otherProjection !== undefined) {
|
|
495
|
+
if (audit) {
|
|
496
|
+
const finding = this.inboundGate(rel, text);
|
|
497
|
+
if (finding !== undefined) {
|
|
498
|
+
this.containInboundReject(finding, f.path, text, entry.id, undefined);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
this.recordRestrictedFinding(`dupserve|${rel}|${entry.rev}`, {
|
|
502
|
+
path: rel,
|
|
503
|
+
code: "restricted_divergence",
|
|
504
|
+
reason: `this file carries the id of a committed entry that has another projection (${otherProjection}) — the committed copy may only stand in for an entry's unique projection, so this copy is withheld; an unrestricted session's harvest adjudicates it`,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (audit) {
|
|
511
|
+
const finding = this.inboundGate(rel, text);
|
|
512
|
+
if (finding !== undefined) {
|
|
513
|
+
this.containInboundReject(finding, f.path, text, entry.id, committed);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
this.recordRestrictedFinding(`divergence|${rel}|${entry.rev}`, {
|
|
517
|
+
path: rel,
|
|
518
|
+
code: "restricted_divergence",
|
|
519
|
+
reason: "on-disk content diverges from the committed entry with no backend transaction backing it; this adoption-restricted session serves the committed content and leaves the disk state pending — an unrestricted session can adopt it normally",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
entries.push(shadowEntry);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (committed !== undefined) {
|
|
527
|
+
if (audit) {
|
|
528
|
+
this.recordRestrictedFinding(`legacy|${rel}|${entry.rev}`, {
|
|
529
|
+
path: rel,
|
|
530
|
+
code: "restricted_divergence",
|
|
531
|
+
reason: "on-disk content diverges from a committed entry that has no committed shadow copy; this adoption-restricted session withholds the entry (nothing trustworthy to serve) — an unrestricted session can adjudicate it",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (audit) {
|
|
537
|
+
const finding = this.inboundGate(rel, text);
|
|
538
|
+
if (finding !== undefined) {
|
|
539
|
+
this.containInboundReject(finding, f.path, text, entry.id, undefined);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
this.recordRestrictedFinding(`uncommitted|${rel}|${entry.rev}`, {
|
|
543
|
+
path: rel,
|
|
544
|
+
code: "restricted_divergence",
|
|
545
|
+
reason: "an id-bearing memory file exists on disk that the committed account has never seen; this adoption-restricted session neither adopts nor serves it — it stays on disk for an unrestricted session to adopt normally",
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return entries;
|
|
551
|
+
}
|
|
552
|
+
idProjectionElsewhere(id, excludeAbsPath) {
|
|
553
|
+
for (const scope of Object.keys(registeredScopes(this.controlPlaneRoot))) {
|
|
554
|
+
const dir = this.scopeDir(scope);
|
|
555
|
+
const isRoot = dir === this.directoryRoot;
|
|
556
|
+
for (const f of scanEntryFiles(dir, { exclude: isRoot ? this.excludedSubdirNames(scope) : undefined })) {
|
|
557
|
+
if (f.path === excludeAbsPath)
|
|
558
|
+
continue;
|
|
559
|
+
const text = readSafe(f.path);
|
|
560
|
+
if (text === undefined)
|
|
561
|
+
continue;
|
|
562
|
+
if (parseEntryFile(text).id === id)
|
|
563
|
+
return relative(this.directoryRoot, f.path);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
restrictedAdoptionView(opts) {
|
|
569
|
+
const audit = opts?.audit === true;
|
|
570
|
+
const refuse = (op) => {
|
|
571
|
+
throw new Error(`memory adoption-restricted view is read-only — ${op} must go through the backend itself`);
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
listHeaders: async (scopes) => this.listHeadersFrom(scopes, (s) => this.readScopeCommitted(s, audit)),
|
|
575
|
+
getByIds: async (ids) => this.getByIdsFrom(ids, (s) => this.readScopeCommitted(s, audit)),
|
|
576
|
+
search: async (query, scopes, o) => this.searchFrom(query, scopes, o, (s) => this.readScopeCommitted(s, audit)),
|
|
577
|
+
applyPatches: async () => refuse("applyPatches"),
|
|
578
|
+
getConsolidationCursor: async (scope) => this.getConsolidationCursor(scope),
|
|
579
|
+
setConsolidationCursor: async () => refuse("setConsolidationCursor"),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
auditRestrictedDivergence(scopes) {
|
|
583
|
+
for (const scope of scopes)
|
|
584
|
+
this.readScopeCommitted(scope, true);
|
|
585
|
+
const ledger = { ...this.loadLedger() };
|
|
586
|
+
const present = new Map();
|
|
587
|
+
let complete = true;
|
|
588
|
+
for (const scope of Object.keys(registeredScopes(this.controlPlaneRoot))) {
|
|
589
|
+
const dir = this.scopeDir(scope);
|
|
590
|
+
const isRoot = dir === this.directoryRoot;
|
|
591
|
+
const files = scanEntryFiles(dir, {
|
|
592
|
+
exclude: isRoot ? this.excludedSubdirNames(scope) : undefined,
|
|
593
|
+
onSkip: (_p, kind) => {
|
|
594
|
+
if (kind === "unreadable")
|
|
595
|
+
complete = false;
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
for (const f of files) {
|
|
599
|
+
const text = readSafe(f.path);
|
|
600
|
+
if (text === undefined) {
|
|
601
|
+
complete = false;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const id = parseEntryFile(text).id;
|
|
605
|
+
if (id !== undefined)
|
|
606
|
+
present.set(id, [...(present.get(id) ?? []), relative(this.directoryRoot, f.path)]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
for (const [id, paths] of present) {
|
|
610
|
+
if (paths.length < 2 || ledger[id] === undefined)
|
|
611
|
+
continue;
|
|
612
|
+
this.recordRestrictedFinding(`dup|${id}|${paths.sort().join(",")}`, {
|
|
613
|
+
path: paths[0] ?? id,
|
|
614
|
+
code: "restricted_divergence",
|
|
615
|
+
reason: `committed memory entry ${id} appears at ${paths.length} paths (${paths.join(", ")}) — a filesystem copy is not a rename; this adoption-restricted session flags it and an unrestricted session's harvest adjudicates which projection is real`,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (!complete) {
|
|
619
|
+
this.recordRestrictedFinding(`missing-audit-incomplete`, {
|
|
620
|
+
path: ".",
|
|
621
|
+
code: "restricted_divergence",
|
|
622
|
+
reason: "the divergence audit could not scan every path (unreadable subtree) — missing-entry detection was skipped this pass rather than judged from an incomplete scan",
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
for (const [id, rev] of Object.entries(ledger)) {
|
|
627
|
+
if (present.has(id))
|
|
628
|
+
continue;
|
|
629
|
+
this.ledger = undefined;
|
|
630
|
+
if (this.loadLedger()[id] === undefined)
|
|
631
|
+
continue;
|
|
632
|
+
this.recordRestrictedFinding(`missing|${id}|${rev}`, {
|
|
633
|
+
path: `id:${id}`,
|
|
634
|
+
code: "restricted_divergence",
|
|
635
|
+
reason: `committed memory entry ${id} is missing from disk with no transaction backing the removal; this adoption-restricted session neither restores nor forgets it — an unrestricted session can restore it from the committed copy`,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
416
639
|
async search(query, scopes, opts) {
|
|
417
640
|
return this.searchWith(query, scopes, opts, true);
|
|
418
641
|
}
|
|
419
642
|
searchWith(query, scopes, opts, adopt) {
|
|
643
|
+
return this.searchFrom(query, scopes, opts, (s) => this.readScope(s, adopt));
|
|
644
|
+
}
|
|
645
|
+
searchFrom(query, scopes, opts, read) {
|
|
420
646
|
const limit = opts?.limit ?? 20;
|
|
421
647
|
const q = termSet(query);
|
|
422
648
|
if (q.size === 0)
|
|
@@ -424,7 +650,7 @@ export class FileMemoryEngineBackend {
|
|
|
424
650
|
const scored = [];
|
|
425
651
|
for (const scope of scopes) {
|
|
426
652
|
const dir = this.scopeDir(scope);
|
|
427
|
-
for (const e of
|
|
653
|
+
for (const e of read(scope)) {
|
|
428
654
|
const haystack = `${e.frontmatter.name ?? e.slug} ${e.frontmatter.description ?? ""} ${e.body}`;
|
|
429
655
|
const d = jaccardDistance(q, haystack);
|
|
430
656
|
if (d === null)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
|
|
@@ -223,9 +223,16 @@ export interface MemorySessionHandle {
|
|
|
223
223
|
* the gate's whole purpose is keeping those bytes out of the prompt, so a refused clear must not
|
|
224
224
|
* leave the injection path reading them anyway. Absent ⇒ the normal "live file wins" behavior. */
|
|
225
225
|
indexOnDiskUntrusted?: boolean;
|
|
226
|
+
/** True ⇔ this session materialized through the ADOPTION-RESTRICTED (committed-view) read face:
|
|
227
|
+
* either the plane is a read-only layering (`writeScope === null`) or the caller declared the
|
|
228
|
+
* session unable to persist (an explicit verdict — never inferred down here). Restricted sessions
|
|
229
|
+
* read committed state only (ledger + control-plane shadow); disk divergence with no transaction
|
|
230
|
+
* backing is neither adopted into the committed account nor served, and `inject` reads the
|
|
231
|
+
* materialize-time index text instead of the live on-disk file. */
|
|
232
|
+
adoptionRestricted?: boolean;
|
|
226
233
|
}
|
|
227
234
|
/** Stable rejection codes a harvest gate can produce (model-visible gate events — 镜头 I). */
|
|
228
|
-
export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "polluted" | "invalid";
|
|
235
|
+
export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "polluted" | "restricted_divergence" | "invalid";
|
|
229
236
|
/** One rejected file: path (relative to the memory dir), stable code, and a model-readable reason. */
|
|
230
237
|
export interface HarvestRejection {
|
|
231
238
|
path: string;
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
* MySQL is NOT "can't support vectors" — it is `portable`-capable (JSON column + in-process cosine), just not
|
|
14
14
|
* `native`. A deployment injects an embedder (config-driven); the store then reports the achieved rung.
|
|
15
15
|
*/
|
|
16
|
-
/** Lower-cased
|
|
16
|
+
/** Lower-cased lexical term set: alphanumeric runs PLUS CJK character bigrams (a single-character
|
|
17
|
+
* run contributes its unigram). CJK scripts have no `a-z0-9` runs at all, so an alphanumeric-only
|
|
18
|
+
* tokenizer made every pure-CJK entry an EMPTY set — never a lexical-rung candidate for any query
|
|
19
|
+
* — and a pure-CJK query returned nothing; character bigrams are the standard analyzer unit there
|
|
20
|
+
* (word boundaries are not written). Bigrams stay within one run: adjacency across an intervening
|
|
21
|
+
* non-CJK character is not real adjacency. */
|
|
17
22
|
export declare function termSet(s: string): Set<string>;
|
|
18
23
|
/** Lexical stand-in distance: `1 - Jaccard(terms)` ∈ [0,1] ⊂ [0,2]; `null` = no overlap (not a candidate). */
|
|
19
24
|
export declare function jaccardDistance(query: Set<string>, text: string): number | null;
|