@sema-agent/core 5.45.0 → 5.47.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/subagent.js +130 -3
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.d.ts +13 -0
- package/dist/core/governance-codes.js +33 -0
- package/dist/core/hooks.d.ts +9 -2
- package/dist/core/hooks.js +6 -5
- package/dist/core/memory-engine/content-origin.d.ts +3 -1
- package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
- package/dist/core/memory-engine/delegation-settlement.js +661 -0
- package/dist/core/memory-engine/engine.d.ts +209 -4
- package/dist/core/memory-engine/engine.js +885 -39
- package/dist/core/memory-engine/export-bundle.d.ts +10 -1
- package/dist/core/memory-engine/export-bundle.js +21 -0
- package/dist/core/memory-engine/file-backend.d.ts +34 -4
- package/dist/core/memory-engine/file-backend.js +168 -40
- package/dist/core/memory-engine/frontmatter.d.ts +69 -1
- package/dist/core/memory-engine/frontmatter.js +156 -2
- package/dist/core/memory-engine/header-hints.d.ts +17 -0
- package/dist/core/memory-engine/header-hints.js +6 -0
- package/dist/core/memory-engine/index.d.ts +7 -5
- package/dist/core/memory-engine/index.js +5 -3
- package/dist/core/memory-engine/layout.d.ts +39 -2
- package/dist/core/memory-engine/layout.js +27 -14
- package/dist/core/memory-engine/memory-backend-contract.js +108 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
- package/dist/core/memory-engine/origin-clearance.js +84 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
- package/dist/core/memory-engine/provenance-wording.js +15 -0
- package/dist/core/memory-engine/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +64 -3
- package/dist/core/memory-engine/tools.js +37 -9
- package/dist/core/memory-engine/types.d.ts +145 -3
- package/dist/core/memory-engine/types.js +1 -1
- package/dist/core/reminder-mint.d.ts +70 -0
- package/dist/core/reminder-mint.js +25 -0
- package/dist/core/runner/git-status-frame.d.ts +3 -14
- package/dist/core/runner/git-status-frame.js +39 -14
- package/dist/core/runner/prepare-config-doors.d.ts +4 -0
- package/dist/core/runner/prepare-config-doors.js +15 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
- package/dist/core/runner/prepare-hands-readface.js +26 -0
- package/dist/core/runner/prepare-memory.d.ts +11 -0
- package/dist/core/runner/prepare-memory.js +61 -24
- package/dist/core/runner/prepare-task.d.ts +46 -1
- package/dist/core/runner/prepare-task.js +128 -23
- package/dist/core/runner/runtask.js +62 -55
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/types.d.ts +85 -10
- package/dist/core/types.js +3 -0
- package/dist/engine/harness/types.d.ts +46 -1
- package/dist/engine/harness/types.js +11 -0
- package/dist/engine/session/import-validate.js +6 -1
- package/dist/engine/session/session.d.ts +20 -0
- package/dist/engine/session/session.js +26 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +16 -0
- package/dist/orchestration/run-workflow-tool.js +23 -3
- package/dist/orchestration/workflow-governance.d.ts +8 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/types.d.ts +6 -0
- package/dist/prompts/default.d.ts +13 -1
- package/dist/prompts/default.js +5 -1
- package/dist/tools/fs/fs-bash.d.ts +4 -0
- package/dist/tools/fs/fs-bash.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +8 -7
- package/dist/tools/fs/fs-shared.d.ts +10 -4
- package/dist/tools/fs/fs-shared.js +6 -3
- package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
- package/dist/tools/fs/gh-rate-limit.js +3 -2
- package/dist/tools/fs/index.d.ts +10 -2
- package/dist/tools/fs/index.js +2 -1
- package/dist/tools/task-list.d.ts +5 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryOrigin } from "./types.js";
|
|
2
2
|
/** A parsed entry file: frontmatter (may be absent — CC allows bare files; harvest mints them) + body. */
|
|
3
3
|
export interface ParsedEntryFile {
|
|
4
4
|
/** The hidden immutable id (`id:` frontmatter line), when present. */
|
|
@@ -24,5 +24,73 @@ export declare function serializeEntryFile(entry: Pick<MemoryEntry, "id" | "fron
|
|
|
24
24
|
* of incidental serialization formatting, or every materialize would see a phantom diff.
|
|
25
25
|
*/
|
|
26
26
|
export declare function computeEntryRev(entry: Pick<MemoryEntry, "id" | "frontmatter" | "body">): string;
|
|
27
|
+
/** True ⇔ `extra` carries origin-form bytes (an invalid/foreign origin block preserved verbatim,
|
|
28
|
+
* or an origin block a pre-336 vintage folded into `extra`). */
|
|
29
|
+
export declare function hasOriginFormExtra(fm: Pick<MemoryEntryFrontmatter, "extra">): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* design/336 slice 2 — the ONE serialization-closed tokenization of `extra`'s origin-form bytes
|
|
32
|
+
* (the adversarial round-3 deep fix; every predicate below reads THROUGH this walk, never through
|
|
33
|
+
* a private re-implementation). A CARRIER is one independent origin representation:
|
|
34
|
+
* - a bare block header (`origin:` — judged on the TRIMMED line, so indented spellings count),
|
|
35
|
+
* together with its indented continuation lines: ONE carrier, normalized field-by-field
|
|
36
|
+
* (first-wins per key; unknown sub-lines tolerated — an in-block `origin: …` scalar is part of
|
|
37
|
+
* THIS carrier, never its own);
|
|
38
|
+
* - a scalar `origin: …` line OUTSIDE any block: ONE carrier, always the sentinel (a scalar names
|
|
39
|
+
* no fields).
|
|
40
|
+
* Per-carrier normalization is INDEPENDENT on purpose (adversarial round 2: a cross-carrier
|
|
41
|
+
* accumulation let two individually-invalid blocks combine into a valid marker, and parse-time
|
|
42
|
+
* block reordering then changed which fields won): a carrier whose own fields form a valid origin
|
|
43
|
+
* (taint external, `at` present AND serialization-stable, cause absent-or-known) normalizes to
|
|
44
|
+
* that value; anything else normalizes to the `{ taint: "external", at: 0 }` sentinel (cause
|
|
45
|
+
* absent, `at` = 0 — a clock here would make engine and backend normalize to different values and
|
|
46
|
+
* the engine's own patches would never converge). Marker-form bytes are NEVER dropped and NEVER
|
|
47
|
+
* read as clean absence.
|
|
48
|
+
*/
|
|
49
|
+
export declare function extraOriginCarriers(extra: readonly string[] | undefined): MemoryEntryOrigin[];
|
|
50
|
+
/**
|
|
51
|
+
* design/336 §2.2 — the ONE reading of an entry's committed external-origin marker: the typed
|
|
52
|
+
* field when present, else the first `extra` carrier's deterministic normalization
|
|
53
|
+
* ({@link extraOriginCarriers}). Returns `undefined` ⇔ the entry carries no marker in any form.
|
|
54
|
+
* The first-carrier choice is immaterial on every committable shape: a representation whose
|
|
55
|
+
* carriers DISAGREE is refused at every inbound boundary ({@link ambiguousOriginRepresentation}),
|
|
56
|
+
* so a committed multi-carrier shape always answers the one agreed value.
|
|
57
|
+
*/
|
|
58
|
+
export declare function committedOriginOf(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): MemoryEntryOrigin | undefined;
|
|
59
|
+
/** Deep equality over the origin marker (every member, absence included) — the immutability law's
|
|
60
|
+
* comparator: a downgraded cause or a rewritten `at` is as refused as a strip. */
|
|
61
|
+
export declare function originEquals(a: MemoryEntryOrigin | undefined, b: MemoryEntryOrigin | undefined): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* design/336 (adversarial rounds 1-3) — the AMBIGUOUS origin-representation predicate, ONE law for
|
|
64
|
+
* every inbound boundary (backend patch law, sync wire, bundle validation, out-of-band adoption):
|
|
65
|
+
* a representation is refused ⇔ its carriers DISAGREE — any two of {the typed field, each `extra`
|
|
66
|
+
* carrier's independent normalization ({@link extraOriginCarriers})} differ on any member — or the
|
|
67
|
+
* typed `at` is not serialization-stable (a value that cannot survive its own serialization is a
|
|
68
|
+
* representation conflict with the entry's next parse). Covered vehicles: typed A + disagreeing
|
|
69
|
+
* extra block B (round 1 — the typed member satisfies the immutability comparator while B rides
|
|
70
|
+
* into storage and a reparse answers B); disagreeing extra-only blocks (round 2 — cross-block
|
|
71
|
+
* field combination plus parse-time block reordering rewrite members with no typed origin at all).
|
|
72
|
+
*
|
|
73
|
+
* The refusal boundary keys on DISAGREEMENT, not on carrier count (slice-2 serialization closure,
|
|
74
|
+
* the round-3 deep fix): agreeing carriers are never a member-rewrite vehicle — refusing them by
|
|
75
|
+
* count alone made the verdict flip across a serialize/parse cycle (a promotion reseats one
|
|
76
|
+
* carrier without changing any value). This generalizes the old "typed beside EXACTLY ONE equal
|
|
77
|
+
* extra form" legacy-carriage exception: N agreeing representations are that same carriage, so
|
|
78
|
+
* carry-forward of old committed forms never bricks. The model-writable face never reaches this
|
|
79
|
+
* predicate — the harvest strips every origin form and re-judges (§2.3-1). Together with the
|
|
80
|
+
* pristine-only typed seating (parse) and the carrier-canonical rev ({@link computeEntryRev}),
|
|
81
|
+
* the accepted set is SERIALIZATION-CLOSED: carrier count, normalized origin, ambiguity verdict
|
|
82
|
+
* and rev are all invariant across serialize/parse at every boundary (property-tested).
|
|
83
|
+
*/
|
|
84
|
+
export declare function ambiguousOriginRepresentation(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* design/336 §2.3-1 — the harvest-side STRIP of model-written origin bytes (typed field AND
|
|
87
|
+
* origin-form `extra` lines, a block header's indented continuation included). The origin seat is
|
|
88
|
+
* engine-authored in every mode: whatever a file on the model-writable plane says about its own
|
|
89
|
+
* origin is not evidence — the engine strips it and mints (or carries forward) its own judgment.
|
|
90
|
+
* Same family as the invalid-id "dropped, fresh one minted" exception to the round-trip law.
|
|
91
|
+
* MUTATES the given (already-copied) frontmatter; returns true ⇔ anything was stripped (the
|
|
92
|
+
* caller discloses via a report warning).
|
|
93
|
+
*/
|
|
94
|
+
export declare function stripModelWrittenOrigin(fm: MemoryEntryFrontmatter): boolean;
|
|
27
95
|
/** Parse a file's text directly into a full MemoryEntry (id must already be present/decided). */
|
|
28
96
|
export declare function entryFromFile(text: string, id: string, slug: string, scope: string): MemoryEntry;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
2
3
|
const FM_FENCE = "---";
|
|
3
4
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/;
|
|
4
5
|
export function isValidEntryId(value) {
|
|
5
6
|
return ID_RE.test(value);
|
|
6
7
|
}
|
|
8
|
+
const ORIGIN_AT_RE = /^-?\d+(\.\d+)?$/;
|
|
9
|
+
function atSerializationStable(at) {
|
|
10
|
+
return Number.isFinite(at) && ORIGIN_AT_RE.test(String(at));
|
|
11
|
+
}
|
|
7
12
|
export function parseEntryFile(text) {
|
|
8
13
|
const fm = {};
|
|
9
14
|
if (!text.startsWith(`${FM_FENCE}\n`) && text.trimStart() !== FM_FENCE) {
|
|
@@ -28,8 +33,12 @@ export function parseEntryFile(text) {
|
|
|
28
33
|
const provRaw = [];
|
|
29
34
|
const provUnknown = [];
|
|
30
35
|
const prov = {};
|
|
36
|
+
let inOrigin = false;
|
|
37
|
+
const originRaw = [];
|
|
38
|
+
let originPristine = true;
|
|
39
|
+
const orig = {};
|
|
31
40
|
for (let i = 1; i < end; i++) {
|
|
32
|
-
const line = lines[i];
|
|
41
|
+
const line = lines[i] ?? "";
|
|
33
42
|
const trimmed = line.trim();
|
|
34
43
|
if (trimmed === "")
|
|
35
44
|
continue;
|
|
@@ -37,6 +46,7 @@ export function parseEntryFile(text) {
|
|
|
37
46
|
if (!indented) {
|
|
38
47
|
inMetadata = false;
|
|
39
48
|
inProvenance = false;
|
|
49
|
+
inOrigin = false;
|
|
40
50
|
}
|
|
41
51
|
if (inMetadata) {
|
|
42
52
|
const m = /^\s+type:\s*(.+?)\s*$/.exec(line);
|
|
@@ -72,11 +82,35 @@ export function parseEntryFile(text) {
|
|
|
72
82
|
inMetadata = true;
|
|
73
83
|
continue;
|
|
74
84
|
}
|
|
85
|
+
if (inOrigin) {
|
|
86
|
+
originRaw.push(line);
|
|
87
|
+
const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
|
|
88
|
+
if (m) {
|
|
89
|
+
const [, k = "", v = ""] = m;
|
|
90
|
+
if (k === "taint" && orig.taint === undefined)
|
|
91
|
+
orig.taint = v;
|
|
92
|
+
else if (k === "cause" && orig.cause === undefined)
|
|
93
|
+
orig.cause = v;
|
|
94
|
+
else if (k === "at" && orig.at === undefined)
|
|
95
|
+
orig.at = ORIGIN_AT_RE.test(v) ? Number(v) : Number.NaN;
|
|
96
|
+
else
|
|
97
|
+
originPristine = false;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
originPristine = false;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
75
104
|
if (/^provenance:\s*$/.test(trimmed)) {
|
|
76
105
|
inProvenance = true;
|
|
77
106
|
provRaw.push(line);
|
|
78
107
|
continue;
|
|
79
108
|
}
|
|
109
|
+
if (/^origin:\s*$/.test(trimmed) && originRaw.length === 0) {
|
|
110
|
+
inOrigin = true;
|
|
111
|
+
originRaw.push(line);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
80
114
|
const kv = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(trimmed);
|
|
81
115
|
if (!kv || indented) {
|
|
82
116
|
extra.push(line);
|
|
@@ -126,6 +160,18 @@ export function parseEntryFile(text) {
|
|
|
126
160
|
extra.push(...provRaw);
|
|
127
161
|
}
|
|
128
162
|
}
|
|
163
|
+
if (originRaw.length > 0) {
|
|
164
|
+
if (originPristine &&
|
|
165
|
+
orig.taint === "external" &&
|
|
166
|
+
typeof orig.at === "number" &&
|
|
167
|
+
atSerializationStable(orig.at) &&
|
|
168
|
+
(orig.cause === undefined || MEMORY_ORIGIN_CAUSES.includes(orig.cause))) {
|
|
169
|
+
fm.origin = { taint: "external", ...(orig.cause !== undefined ? { cause: orig.cause } : {}), at: orig.at };
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
extra.push(...originRaw);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
129
175
|
if (extra.length > 0)
|
|
130
176
|
fm.extra = extra;
|
|
131
177
|
let body = lines.slice(end + 1).join("\n");
|
|
@@ -151,26 +197,134 @@ export function serializeEntryFile(entry) {
|
|
|
151
197
|
lines.push(`trust: ${fm.trust}`);
|
|
152
198
|
if (fm.extra)
|
|
153
199
|
lines.push(...fm.extra);
|
|
200
|
+
if (fm.origin !== undefined) {
|
|
201
|
+
lines.push("origin:", ` taint: ${fm.origin.taint}`, ...(fm.origin.cause !== undefined ? [` cause: ${fm.origin.cause}`] : []), ` at: ${fm.origin.at}`);
|
|
202
|
+
}
|
|
154
203
|
lines.push(FM_FENCE, "");
|
|
155
204
|
const body = entry.body.replace(/\s+$/, "");
|
|
156
205
|
return `${lines.join("\n")}${body}${body ? "\n" : ""}`;
|
|
157
206
|
}
|
|
158
207
|
export function computeEntryRev(entry) {
|
|
159
208
|
const fm = entry.frontmatter;
|
|
209
|
+
const hasCarrierExtra = fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line));
|
|
210
|
+
const canonicalOrigin = fm.origin !== undefined ? fm.origin : hasCarrierExtra ? committedOriginOf(fm) : undefined;
|
|
160
211
|
const canonical = JSON.stringify([
|
|
161
212
|
entry.id,
|
|
162
213
|
fm.name ?? null,
|
|
163
214
|
fm.description ?? null,
|
|
164
215
|
fm.type ?? null,
|
|
165
216
|
fm.deleted === true,
|
|
166
|
-
fm.extra ?? [],
|
|
217
|
+
hasCarrierExtra ? partitionOriginLines(fm.extra).kept : (fm.extra ?? []),
|
|
167
218
|
entry.body.replace(/\s+$/, ""),
|
|
168
219
|
...(fm.provenance !== undefined || fm.trust !== undefined
|
|
169
220
|
? [fm.trust ?? null, fm.provenance !== undefined ? [fm.provenance.kind, fm.provenance.path, fm.provenance.contentHash, fm.provenance.ingestedAt] : null]
|
|
170
221
|
: []),
|
|
222
|
+
...(canonicalOrigin !== undefined ? [["origin", canonicalOrigin.taint, canonicalOrigin.cause ?? null, canonicalOrigin.at]] : []),
|
|
171
223
|
]);
|
|
172
224
|
return createHash("sha256").update(canonical, "utf8").digest("hex").slice(0, 16);
|
|
173
225
|
}
|
|
226
|
+
const ORIGIN_FORM_LINE_RE = /^\s*origin\s*:/;
|
|
227
|
+
export function hasOriginFormExtra(fm) {
|
|
228
|
+
return fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line));
|
|
229
|
+
}
|
|
230
|
+
export function extraOriginCarriers(extra) {
|
|
231
|
+
if (extra === undefined)
|
|
232
|
+
return [];
|
|
233
|
+
const carriers = [];
|
|
234
|
+
let block;
|
|
235
|
+
const closeBlock = () => {
|
|
236
|
+
if (block === undefined)
|
|
237
|
+
return;
|
|
238
|
+
const { taint, cause, at } = block;
|
|
239
|
+
block = undefined;
|
|
240
|
+
if (taint === "external" && typeof at === "number" && atSerializationStable(at) && (cause === undefined || MEMORY_ORIGIN_CAUSES.includes(cause))) {
|
|
241
|
+
carriers.push({ taint: "external", ...(cause !== undefined ? { cause: cause } : {}), at });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
carriers.push({ taint: "external", at: 0 });
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
for (const line of extra) {
|
|
248
|
+
if (/^\s*origin\s*:\s*$/.test(line)) {
|
|
249
|
+
closeBlock();
|
|
250
|
+
block = {};
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (block !== undefined && /^\s/.test(line)) {
|
|
254
|
+
const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
|
|
255
|
+
if (m !== null) {
|
|
256
|
+
const [, k = "", v = ""] = m;
|
|
257
|
+
if (k === "taint" && block.taint === undefined)
|
|
258
|
+
block.taint = v;
|
|
259
|
+
else if (k === "cause" && block.cause === undefined)
|
|
260
|
+
block.cause = v;
|
|
261
|
+
else if (k === "at" && block.at === undefined)
|
|
262
|
+
block.at = ORIGIN_AT_RE.test(v) ? Number(v) : Number.NaN;
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
closeBlock();
|
|
267
|
+
if (ORIGIN_FORM_LINE_RE.test(line))
|
|
268
|
+
carriers.push({ taint: "external", at: 0 });
|
|
269
|
+
}
|
|
270
|
+
closeBlock();
|
|
271
|
+
return carriers;
|
|
272
|
+
}
|
|
273
|
+
export function committedOriginOf(fm) {
|
|
274
|
+
if (fm.origin !== undefined)
|
|
275
|
+
return { taint: fm.origin.taint, ...(fm.origin.cause !== undefined ? { cause: fm.origin.cause } : {}), at: fm.origin.at };
|
|
276
|
+
return extraOriginCarriers(fm.extra)[0];
|
|
277
|
+
}
|
|
278
|
+
export function originEquals(a, b) {
|
|
279
|
+
if (a === undefined || b === undefined)
|
|
280
|
+
return a === b;
|
|
281
|
+
return a.taint === b.taint && a.cause === b.cause && a.at === b.at;
|
|
282
|
+
}
|
|
283
|
+
export function ambiguousOriginRepresentation(fm) {
|
|
284
|
+
if (fm.origin !== undefined && !atSerializationStable(fm.origin.at))
|
|
285
|
+
return true;
|
|
286
|
+
const values = extraOriginCarriers(fm.extra);
|
|
287
|
+
if (fm.origin !== undefined)
|
|
288
|
+
values.push({ taint: fm.origin.taint, ...(fm.origin.cause !== undefined ? { cause: fm.origin.cause } : {}), at: fm.origin.at });
|
|
289
|
+
return values.some((v) => !originEquals(v, values[0]));
|
|
290
|
+
}
|
|
291
|
+
function partitionOriginLines(extra) {
|
|
292
|
+
const kept = [];
|
|
293
|
+
let removedAny = false;
|
|
294
|
+
let inBlock = false;
|
|
295
|
+
for (const line of extra) {
|
|
296
|
+
if (ORIGIN_FORM_LINE_RE.test(line)) {
|
|
297
|
+
inBlock = /^\s*origin\s*:\s*$/.test(line);
|
|
298
|
+
removedAny = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (inBlock && /^\s/.test(line)) {
|
|
302
|
+
removedAny = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
inBlock = false;
|
|
306
|
+
kept.push(line);
|
|
307
|
+
}
|
|
308
|
+
return { kept, removedAny };
|
|
309
|
+
}
|
|
310
|
+
export function stripModelWrittenOrigin(fm) {
|
|
311
|
+
let stripped = false;
|
|
312
|
+
if (fm.origin !== undefined) {
|
|
313
|
+
delete fm.origin;
|
|
314
|
+
stripped = true;
|
|
315
|
+
}
|
|
316
|
+
if (fm.extra !== undefined) {
|
|
317
|
+
const { kept, removedAny } = partitionOriginLines(fm.extra);
|
|
318
|
+
if (removedAny) {
|
|
319
|
+
stripped = true;
|
|
320
|
+
if (kept.length > 0)
|
|
321
|
+
fm.extra = kept;
|
|
322
|
+
else
|
|
323
|
+
delete fm.extra;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return stripped;
|
|
327
|
+
}
|
|
174
328
|
export function entryFromFile(text, id, slug, scope) {
|
|
175
329
|
const parsed = parseEntryFile(text);
|
|
176
330
|
const entry = { id, slug, frontmatter: parsed.frontmatter, body: parsed.body, rev: "", scope };
|
|
@@ -25,6 +25,23 @@ export interface V2HeaderHints {
|
|
|
25
25
|
/** `applies-when: <free text>` — non-empty free text, trimmed. */
|
|
26
26
|
appliesWhen?: string;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* design/336 §4.1 — the MECHANICAL instruction-form predicate (deliberately zero semantic judgment:
|
|
30
|
+
* no model, no content heuristics — SOTA R4's LLM-judge ban). An entry is instruction-form ⇔
|
|
31
|
+
* - `metadata.type === "feedback"` (the declared behavior-correction type), OR
|
|
32
|
+
* - its v2 header hints declare an injection/trigger privilege: `pinned` / `triggers` /
|
|
33
|
+
* `applies-when` (the three hints whose whole purpose is automatic or conditional injection;
|
|
34
|
+
* `last-confirmed` is bookkeeping and does not count).
|
|
35
|
+
* Escape analysis: an entry that declares none of these rides the ordinary-entry row — where the
|
|
36
|
+
* origin-marker floor governs — so NOT declaring buys no privilege; declaring buys injection
|
|
37
|
+
* privilege AND walks through the exposure hard gate with it. ONE exported predicate on purpose:
|
|
38
|
+
* the write side (harvest hard gate) and any read-side consumer must judge through the same
|
|
39
|
+
* function, never through re-implementations that can drift.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isInstructionEntry(fm: {
|
|
42
|
+
type?: string;
|
|
43
|
+
extra?: readonly string[];
|
|
44
|
+
}): boolean;
|
|
28
45
|
/** Read the v2 header hints out of an entry's preserved-verbatim `extra` lines. First well-formed
|
|
29
46
|
* occurrence of each key wins (duplicate lines are model bookkeeping noise, not a merge input). */
|
|
30
47
|
export declare function readV2HeaderHints(extra: readonly string[] | undefined): V2HeaderHints;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const LINE_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/;
|
|
2
2
|
const ISO_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}(?:$|[T\s])/;
|
|
3
|
+
export function isInstructionEntry(fm) {
|
|
4
|
+
if (fm.type === "feedback")
|
|
5
|
+
return true;
|
|
6
|
+
const hints = readV2HeaderHints(fm.extra);
|
|
7
|
+
return hints.pinned !== undefined || hints.triggers !== undefined || hints.appliesWhen !== undefined;
|
|
8
|
+
}
|
|
3
9
|
export function readV2HeaderHints(extra) {
|
|
4
10
|
const out = {};
|
|
5
11
|
if (extra === undefined)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, } from "./engine.js";
|
|
2
|
-
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, } from "./engine.js";
|
|
2
|
+
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
|
+
export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
|
|
3
4
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
5
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, type MemoryExportSnapshot, } from "./file-backend.js";
|
|
5
6
|
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, type MemoryExportBundle, type MemoryImportReport, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, } from "./export-bundle.js";
|
|
6
7
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
|
|
7
|
-
export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
|
|
8
|
-
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
|
|
9
|
-
export
|
|
8
|
+
export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
|
|
9
|
+
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
|
|
10
|
+
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
11
|
+
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
10
12
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
|
|
11
13
|
export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, } from "./scope-contract.js";
|
|
12
14
|
export { migrateScope, type MigrateScopeReport } from "./migrate.js";
|
|
@@ -1,11 +1,13 @@
|
|
|
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";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, 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
|
+
export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
|
|
3
4
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
5
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, erasureSelectHash, } from "./file-backend.js";
|
|
5
6
|
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
6
7
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
|
|
7
|
-
export { readV2HeaderHints } from "./header-hints.js";
|
|
8
|
-
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
|
|
8
|
+
export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
|
|
9
|
+
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation } from "./frontmatter.js";
|
|
10
|
+
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
9
11
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
|
|
10
12
|
export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, } from "./scope-contract.js";
|
|
11
13
|
export { migrateScope } from "./migrate.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MemoryAnnouncement } from "./types.js";
|
|
2
2
|
/** Cursor sidecar for the design/84 Seam B pair on the FileBackend (`{ [scope]: cursor }`). B3: control plane. */
|
|
3
3
|
export declare const CURSORS_FILE = "cursors.json";
|
|
4
4
|
/** The control-plane subdir name under a key dir — ONE spelling for every derivation and for the
|
|
@@ -285,6 +285,11 @@ export declare const SESSION_POLLUTION_DIR = "session-pollution";
|
|
|
285
285
|
export interface SessionPollutionRecord {
|
|
286
286
|
at: number;
|
|
287
287
|
reason: string;
|
|
288
|
+
/** design/336 §2.2 — the structured mechanical cause of the exposure (the value an origin marker
|
|
289
|
+
* minted off this record carries). ADDITIVE: records written before the field existed read as
|
|
290
|
+
* honestly cause-less (the mint then falls back to `"observed"`); old readers narrow-take the
|
|
291
|
+
* two fields they know. */
|
|
292
|
+
cause?: import("./types.js").MemoryOriginCause;
|
|
288
293
|
}
|
|
289
294
|
/**
|
|
290
295
|
* Mark `sessionId` polluted (idempotent; the FIRST record wins — `wx` create refuses overwrite).
|
|
@@ -299,7 +304,7 @@ export interface SessionPollutionRecord {
|
|
|
299
304
|
* stays polluted for this process either way).
|
|
300
305
|
*/
|
|
301
306
|
export type SessionPollutionMarkOutcome = "created" | "existed" | "unpersisted";
|
|
302
|
-
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number): SessionPollutionMarkOutcome;
|
|
307
|
+
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number, cause?: import("./types.js").MemoryOriginCause): SessionPollutionMarkOutcome;
|
|
303
308
|
/** Read the durable pollution record for `sessionId` (undefined = no marker).
|
|
304
309
|
* A marker that EXISTS still reads as polluted whether or not its bytes can be read or parsed
|
|
305
310
|
* (fail-closed: neither corruption nor an unreadable node may launder the state) — a synthesized
|
|
@@ -355,11 +360,30 @@ export declare const LINEAGE_AUDIT_MAX_ROWS = 2048;
|
|
|
355
360
|
/** The challenged-history account (design/180 B-3: collect-only, fail-open bookkeeping —
|
|
356
361
|
* the mechanical shadow of "user saw a retrieved entry and corrected it in the same session"). */
|
|
357
362
|
export declare const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
|
|
363
|
+
/** The locked, journaled, FAIL-CLOSED sidecar update. `coerce` must throw
|
|
364
|
+
* {@link ControlPlaneCorruptError} on any unacceptable shape; only ENOENT reads as the empty
|
|
365
|
+
* initial state (`undefined` handed to `coerce`). Exported module-level (not on the root barrel)
|
|
366
|
+
* for the strict-account family's siblings (design/336 slice 2: the delegation-settlement /
|
|
367
|
+
* session-account / instruction-hold sidecars live in their own module but MUST be this exact
|
|
368
|
+
* lock+journal discipline — a second implementation would be a drift seam). */
|
|
369
|
+
export declare function lockedStrictUpdate<S, T>(controlDir: string, fileName: string, what: string, coerce: (raw: unknown) => S, fn: (current: S) => {
|
|
370
|
+
next?: S;
|
|
371
|
+
result: T;
|
|
372
|
+
}): T;
|
|
373
|
+
/** Lock-less strict READ (journal-aware): a parseable journal wins (committed next state); an
|
|
374
|
+
* unparseable journal, or an unreadable/unparseable file, is corrupt. Exported module-level for
|
|
375
|
+
* the strict-account family's siblings (see {@link lockedStrictUpdate}). */
|
|
376
|
+
export declare function readStrictSidecar(controlDir: string, fileName: string, what: string): unknown;
|
|
358
377
|
/** One committed contribution: session → entry, latest rev/at only (the load-bearing invariant is
|
|
359
378
|
* "which sessions contributed this id", not the per-commit history — that is the audit account). */
|
|
360
379
|
export interface LineageContribution {
|
|
361
380
|
lastRev: string;
|
|
362
381
|
lastAt: number;
|
|
382
|
+
/** design/336 — carried from the staged row at promotion: the contribution's content (at
|
|
383
|
+
* `lastRev`) carries its external-origin marker, so the pollution retroaction sweeps owe it no
|
|
384
|
+
* challenge (the account travels with the entry). Absence = unmarked content — the sweeps'
|
|
385
|
+
* domain (older vintages read as unmarked, the conservative side). */
|
|
386
|
+
marked?: true;
|
|
363
387
|
}
|
|
364
388
|
/** One staged (pre-commit) row: an entry this transaction WOULD commit. `kind: "latch-only"` marks
|
|
365
389
|
* a bundle-import SYNTHETIC latch row (design/178 v2-c §1): it withholds the id on the model-visible
|
|
@@ -370,6 +394,12 @@ export interface LineagePendingRow {
|
|
|
370
394
|
entryId: string;
|
|
371
395
|
rev: string;
|
|
372
396
|
kind?: "latch-only";
|
|
397
|
+
/** design/336 — set ⇔ the staged content CARRIES its external-origin marker (`frontmatter.origin`
|
|
398
|
+
* in the very rev this row names): the exposure account travels WITH the entry, so promotion
|
|
399
|
+
* settlement owes it no pollution challenge (challenging it would re-quarantine a tag-admission
|
|
400
|
+
* at the read face). Absence = the row's content is unmarked (older vintages included) — the
|
|
401
|
+
* conservative side: an unmarked row of a marked session is challenged. */
|
|
402
|
+
marked?: true;
|
|
373
403
|
}
|
|
374
404
|
/** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
|
|
375
405
|
* succeeds; promotion consumes both. A pending txn without a credential is the crash window —
|
|
@@ -406,6 +436,10 @@ export interface LineagePromotion {
|
|
|
406
436
|
entryId: string;
|
|
407
437
|
sessionId: string;
|
|
408
438
|
rev: string;
|
|
439
|
+
/** design/336 — carried through from {@link LineagePendingRow.marked}: the promoted content
|
|
440
|
+
* already carries its external-origin marker, so promotion settlement skips the pollution
|
|
441
|
+
* challenge for it. */
|
|
442
|
+
marked?: true;
|
|
409
443
|
}
|
|
410
444
|
/** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
|
|
411
445
|
* committed contribution set (deduped by (entryId, sessionId) — a repeat pair refreshes
|
|
@@ -473,6 +507,8 @@ export declare function lineageLatchedIds(controlDir: string): Set<string>;
|
|
|
473
507
|
export declare function lineageContributionsOfSession(controlDir: string, sessionId: string): Array<{
|
|
474
508
|
entryId: string;
|
|
475
509
|
lastRev: string;
|
|
510
|
+
lastAt: number;
|
|
511
|
+
marked?: true;
|
|
476
512
|
}>;
|
|
477
513
|
/** design/178 v2-a §1.3 — the BY-ENTRY lineage account, from ONE parse of the ledger (the
|
|
478
514
|
* by-session read's transpose; the committed set is keyed by entryId, so the index is O(1) after
|
|
@@ -538,6 +574,7 @@ export declare function importLineageCommitted(controlDir: string, rows: Readonl
|
|
|
538
574
|
sessionId: string;
|
|
539
575
|
lastRev: string;
|
|
540
576
|
lastAt: number;
|
|
577
|
+
marked?: true;
|
|
541
578
|
}>): LineageImportDivergence[];
|
|
542
579
|
/** Full-ledger read (tests / host observability). Throws on corruption. */
|
|
543
580
|
export declare function readLineageRecord(controlDir: string): {
|
|
@@ -3,6 +3,7 @@ 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";
|
|
6
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
6
7
|
const SCOPES_FILE = "scopes.json";
|
|
7
8
|
export const CURSORS_FILE = "cursors.json";
|
|
8
9
|
export const CONTROL_PLANE_DIR = ".engine";
|
|
@@ -734,11 +735,11 @@ export const SESSION_POLLUTION_DIR = "session-pollution";
|
|
|
734
735
|
function pollutionPath(controlDir, sessionId) {
|
|
735
736
|
return join(controlDir, SESSION_POLLUTION_DIR, `${encodeURIComponent(sessionId)}.json`);
|
|
736
737
|
}
|
|
737
|
-
export function markSessionPolluted(controlDir, sessionId, reason, now) {
|
|
738
|
+
export function markSessionPolluted(controlDir, sessionId, reason, now, cause) {
|
|
738
739
|
const path = pollutionPath(controlDir, sessionId);
|
|
739
740
|
try {
|
|
740
741
|
ensureDirExists(dirname(path));
|
|
741
|
-
const record = { at: now(), reason };
|
|
742
|
+
const record = { at: now(), reason, ...(cause !== undefined ? { cause } : {}) };
|
|
742
743
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
743
744
|
return "created";
|
|
744
745
|
}
|
|
@@ -764,8 +765,10 @@ export function readSessionPollution(controlDir, sessionId) {
|
|
|
764
765
|
if (parsed !== null && typeof parsed === "object") {
|
|
765
766
|
const at = parsed.at;
|
|
766
767
|
const reason = parsed.reason;
|
|
768
|
+
const cause = parsed.cause;
|
|
769
|
+
const knownCause = typeof cause === "string" && MEMORY_ORIGIN_CAUSES.includes(cause) ? cause : undefined;
|
|
767
770
|
if (typeof at === "number" && typeof reason === "string")
|
|
768
|
-
return { at, reason };
|
|
771
|
+
return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}) };
|
|
769
772
|
}
|
|
770
773
|
}
|
|
771
774
|
catch {
|
|
@@ -804,7 +807,7 @@ export function importSessionPollution(controlDir, sessionId, record) {
|
|
|
804
807
|
const path = pollutionPath(controlDir, sessionId);
|
|
805
808
|
ensureDirExists(dirname(path));
|
|
806
809
|
try {
|
|
807
|
-
writeFileSync(path, `${JSON.stringify({ at: record.at, reason: record.reason }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
810
|
+
writeFileSync(path, `${JSON.stringify({ at: record.at, reason: record.reason, ...(record.cause !== undefined ? { cause: record.cause } : {}) }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
808
811
|
return "written";
|
|
809
812
|
}
|
|
810
813
|
catch (err) {
|
|
@@ -891,7 +894,7 @@ function rollForwardStrictSidecar(file, journal) {
|
|
|
891
894
|
atomicWriteFileSync(file, raw);
|
|
892
895
|
rmSync(journal, { force: true });
|
|
893
896
|
}
|
|
894
|
-
function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
|
|
897
|
+
export function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
|
|
895
898
|
ensureDirExists(controlDir);
|
|
896
899
|
const file = join(controlDir, fileName);
|
|
897
900
|
const journal = `${file}.journal`;
|
|
@@ -931,7 +934,7 @@ function readStrictSidecarRaw(file, what) {
|
|
|
931
934
|
throw new ControlPlaneCorruptError(`${what} is unparseable: ${file}`);
|
|
932
935
|
}
|
|
933
936
|
}
|
|
934
|
-
function readStrictSidecar(controlDir, fileName, what) {
|
|
937
|
+
export function readStrictSidecar(controlDir, fileName, what) {
|
|
935
938
|
const file = join(controlDir, fileName);
|
|
936
939
|
const journal = `${file}.journal`;
|
|
937
940
|
let journalRaw;
|
|
@@ -976,6 +979,8 @@ function coerceLineage(raw) {
|
|
|
976
979
|
if (!row || typeof row !== "object" || typeof row.lastRev !== "string" || typeof row.lastAt !== "number") {
|
|
977
980
|
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}]`);
|
|
978
981
|
}
|
|
982
|
+
if (row.marked !== undefined && row.marked !== true)
|
|
983
|
+
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] marked`);
|
|
979
984
|
}
|
|
980
985
|
}
|
|
981
986
|
for (const [txnId, t] of Object.entries(r.pending)) {
|
|
@@ -991,6 +996,8 @@ function coerceLineage(raw) {
|
|
|
991
996
|
throw badShape(`pending[${JSON.stringify(txnId)}] row`);
|
|
992
997
|
if (p.kind !== undefined && p.kind !== "latch-only")
|
|
993
998
|
throw badShape(`pending[${JSON.stringify(txnId)}] row kind`);
|
|
999
|
+
if (p.marked !== undefined && p.marked !== true)
|
|
1000
|
+
throw badShape(`pending[${JSON.stringify(txnId)}] row marked`);
|
|
994
1001
|
}
|
|
995
1002
|
if (txn.credential !== undefined) {
|
|
996
1003
|
const c = txn.credential;
|
|
@@ -1049,8 +1056,8 @@ export function promoteLineagePending(controlDir, txnId, now) {
|
|
|
1049
1056
|
if (!applied.has(row.entryId))
|
|
1050
1057
|
continue;
|
|
1051
1058
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1052
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1053
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1059
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1060
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1054
1061
|
}
|
|
1055
1062
|
delete rec.pending[txnId];
|
|
1056
1063
|
return { next: rec, result: promoted };
|
|
@@ -1075,8 +1082,8 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
|
|
|
1075
1082
|
if (applied !== undefined && !applied.has(row.entryId))
|
|
1076
1083
|
continue;
|
|
1077
1084
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1078
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1079
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1085
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1086
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1080
1087
|
}
|
|
1081
1088
|
delete rec.pending[txnId];
|
|
1082
1089
|
return { next: rec, result: promoted };
|
|
@@ -1099,8 +1106,8 @@ export function reconcileLineage(controlDir, now) {
|
|
|
1099
1106
|
continue;
|
|
1100
1107
|
at ??= now();
|
|
1101
1108
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1102
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1103
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1109
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1110
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1104
1111
|
}
|
|
1105
1112
|
delete rec.pending[txnId];
|
|
1106
1113
|
changed = true;
|
|
@@ -1182,7 +1189,7 @@ export function lineageContributionsOfSession(controlDir, sessionId) {
|
|
|
1182
1189
|
for (const [entryId, sessions] of Object.entries(rec.committed)) {
|
|
1183
1190
|
const c = Object.prototype.hasOwnProperty.call(sessions, sessionId) ? sessions[sessionId] : undefined;
|
|
1184
1191
|
if (c !== undefined)
|
|
1185
|
-
out.push({ entryId, lastRev: c.lastRev });
|
|
1192
|
+
out.push({ entryId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
|
|
1186
1193
|
}
|
|
1187
1194
|
return out;
|
|
1188
1195
|
}
|
|
@@ -1262,12 +1269,18 @@ export function importLineageCommitted(controlDir, rows) {
|
|
|
1262
1269
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1263
1270
|
const standing = Object.prototype.hasOwnProperty.call(sessions, row.sessionId) ? sessions[row.sessionId] : undefined;
|
|
1264
1271
|
if (standing === undefined || row.lastAt > standing.lastAt) {
|
|
1265
|
-
sessions[row.sessionId] = { lastRev: row.lastRev, lastAt: row.lastAt };
|
|
1272
|
+
sessions[row.sessionId] = { lastRev: row.lastRev, lastAt: row.lastAt, ...(row.marked === true ? { marked: true } : {}) };
|
|
1266
1273
|
changed = true;
|
|
1267
1274
|
continue;
|
|
1268
1275
|
}
|
|
1269
1276
|
if (row.lastAt === standing.lastAt && row.lastRev !== standing.lastRev) {
|
|
1270
1277
|
divergence.push({ entryId: row.entryId, sessionId: row.sessionId, kept: "destination" });
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (row.lastAt === standing.lastAt && row.lastRev === standing.lastRev && row.marked === true && standing.marked !== true) {
|
|
1281
|
+
sessions[row.sessionId] = { ...standing, marked: true };
|
|
1282
|
+
changed = true;
|
|
1283
|
+
continue;
|
|
1271
1284
|
}
|
|
1272
1285
|
}
|
|
1273
1286
|
return { ...(changed ? { next: rec } : {}), result: divergence };
|