@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/agents/subagent.js +130 -3
  3. package/dist/core/checkpoint-store.d.ts +12 -0
  4. package/dist/core/governance-codes.d.ts +13 -0
  5. package/dist/core/governance-codes.js +33 -0
  6. package/dist/core/hooks.d.ts +9 -2
  7. package/dist/core/hooks.js +6 -5
  8. package/dist/core/memory-engine/content-origin.d.ts +3 -1
  9. package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
  10. package/dist/core/memory-engine/delegation-settlement.js +661 -0
  11. package/dist/core/memory-engine/engine.d.ts +209 -4
  12. package/dist/core/memory-engine/engine.js +885 -39
  13. package/dist/core/memory-engine/export-bundle.d.ts +10 -1
  14. package/dist/core/memory-engine/export-bundle.js +21 -0
  15. package/dist/core/memory-engine/file-backend.d.ts +34 -4
  16. package/dist/core/memory-engine/file-backend.js +168 -40
  17. package/dist/core/memory-engine/frontmatter.d.ts +69 -1
  18. package/dist/core/memory-engine/frontmatter.js +156 -2
  19. package/dist/core/memory-engine/header-hints.d.ts +17 -0
  20. package/dist/core/memory-engine/header-hints.js +6 -0
  21. package/dist/core/memory-engine/index.d.ts +7 -5
  22. package/dist/core/memory-engine/index.js +5 -3
  23. package/dist/core/memory-engine/layout.d.ts +39 -2
  24. package/dist/core/memory-engine/layout.js +27 -14
  25. package/dist/core/memory-engine/memory-backend-contract.js +108 -0
  26. package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
  27. package/dist/core/memory-engine/origin-clearance.js +84 -0
  28. package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
  29. package/dist/core/memory-engine/provenance-wording.js +15 -0
  30. package/dist/core/memory-engine/sync-client.d.ts +1 -1
  31. package/dist/core/memory-engine/sync-client.js +33 -1
  32. package/dist/core/memory-engine/tools.d.ts +64 -3
  33. package/dist/core/memory-engine/tools.js +37 -9
  34. package/dist/core/memory-engine/types.d.ts +145 -3
  35. package/dist/core/memory-engine/types.js +1 -1
  36. package/dist/core/reminder-mint.d.ts +70 -0
  37. package/dist/core/reminder-mint.js +25 -0
  38. package/dist/core/runner/git-status-frame.d.ts +3 -14
  39. package/dist/core/runner/git-status-frame.js +39 -14
  40. package/dist/core/runner/prepare-config-doors.d.ts +4 -0
  41. package/dist/core/runner/prepare-config-doors.js +15 -0
  42. package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
  43. package/dist/core/runner/prepare-hands-readface.js +26 -0
  44. package/dist/core/runner/prepare-memory.d.ts +11 -0
  45. package/dist/core/runner/prepare-memory.js +61 -24
  46. package/dist/core/runner/prepare-task.d.ts +46 -1
  47. package/dist/core/runner/prepare-task.js +128 -23
  48. package/dist/core/runner/runtask.js +62 -55
  49. package/dist/core/session-reconcile.js +3 -2
  50. package/dist/core/side-query.d.ts +11 -1
  51. package/dist/core/side-query.js +3 -0
  52. package/dist/core/types.d.ts +85 -10
  53. package/dist/core/types.js +3 -0
  54. package/dist/engine/harness/types.d.ts +46 -1
  55. package/dist/engine/harness/types.js +11 -0
  56. package/dist/engine/session/import-validate.js +6 -1
  57. package/dist/engine/session/session.d.ts +20 -0
  58. package/dist/engine/session/session.js +26 -1
  59. package/dist/index.d.ts +3 -1
  60. package/dist/index.js +3 -1
  61. package/dist/orchestration/run-workflow-tool.d.ts +16 -0
  62. package/dist/orchestration/run-workflow-tool.js +23 -3
  63. package/dist/orchestration/workflow-governance.d.ts +8 -1
  64. package/dist/prompt-assembly/epoch.js +2 -0
  65. package/dist/prompt-assembly/types.d.ts +6 -0
  66. package/dist/prompts/default.d.ts +13 -1
  67. package/dist/prompts/default.js +5 -1
  68. package/dist/tools/fs/fs-bash.d.ts +4 -0
  69. package/dist/tools/fs/fs-bash.js +1 -1
  70. package/dist/tools/fs/fs-read.d.ts +1 -1
  71. package/dist/tools/fs/fs-read.js +8 -7
  72. package/dist/tools/fs/fs-shared.d.ts +10 -4
  73. package/dist/tools/fs/fs-shared.js +6 -3
  74. package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
  75. package/dist/tools/fs/gh-rate-limit.js +3 -2
  76. package/dist/tools/fs/index.d.ts +10 -2
  77. package/dist/tools/fs/index.js +2 -1
  78. package/dist/tools/task-list.d.ts +5 -1
  79. package/package.json +1 -1
  80. package/test/export-surface.snapshot.json +21 -2
@@ -1,4 +1,4 @@
1
- import type { MemoryEntry, MemoryEntryFrontmatter } from "./types.js";
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 type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
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 { MemoryAnnouncement } from "./types.js";
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 };