@sema-agent/core 5.46.0 → 5.48.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 (78) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +4 -0
  3. package/dist/agents/agent-transcript-tool.js +10 -3
  4. package/dist/agents/send-message-tool.d.ts +43 -1
  5. package/dist/agents/send-message-tool.js +50 -11
  6. package/dist/agents/subagent.d.ts +18 -0
  7. package/dist/agents/subagent.js +231 -4
  8. package/dist/config/defaults.d.ts +20 -0
  9. package/dist/config/defaults.js +5 -0
  10. package/dist/core/background-agent-store.d.ts +1 -0
  11. package/dist/core/background-agent-store.js +13 -0
  12. package/dist/core/governance-codes.d.ts +13 -0
  13. package/dist/core/governance-codes.js +33 -0
  14. package/dist/core/mcp.d.ts +6 -1
  15. package/dist/core/mcp.js +34 -7
  16. package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
  17. package/dist/core/memory-engine/delegation-settlement.js +661 -0
  18. package/dist/core/memory-engine/engine.d.ts +159 -1
  19. package/dist/core/memory-engine/engine.js +699 -15
  20. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  21. package/dist/core/memory-engine/file-backend.js +3 -1
  22. package/dist/core/memory-engine/frontmatter.d.ts +46 -19
  23. package/dist/core/memory-engine/frontmatter.js +91 -77
  24. package/dist/core/memory-engine/index.d.ts +4 -3
  25. package/dist/core/memory-engine/index.js +3 -2
  26. package/dist/core/memory-engine/layout.d.ts +14 -0
  27. package/dist/core/memory-engine/layout.js +2 -2
  28. package/dist/core/memory-engine/memory-backend-contract.js +43 -0
  29. package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
  30. package/dist/core/memory-engine/origin-clearance.js +84 -0
  31. package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
  32. package/dist/core/memory-engine/provenance-wording.js +15 -0
  33. package/dist/core/memory-engine/tools.d.ts +61 -7
  34. package/dist/core/memory-engine/tools.js +34 -9
  35. package/dist/core/memory-engine/types.d.ts +70 -2
  36. package/dist/core/reminder-disclosure.d.ts +90 -0
  37. package/dist/core/reminder-disclosure.js +64 -0
  38. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  39. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  40. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  41. package/dist/core/runner/prepare-hands-readface.js +1 -0
  42. package/dist/core/runner/prepare-memory.js +50 -15
  43. package/dist/core/runner/prepare-task.d.ts +39 -0
  44. package/dist/core/runner/prepare-task.js +128 -40
  45. package/dist/core/runner/runtask.js +3 -1
  46. package/dist/core/session-reconcile.js +3 -2
  47. package/dist/core/session-store.d.ts +59 -1
  48. package/dist/core/session-store.js +82 -14
  49. package/dist/core/session.d.ts +83 -1
  50. package/dist/core/task-registry-agent.d.ts +28 -0
  51. package/dist/core/task-registry-agent.js +63 -2
  52. package/dist/core/task-registry.d.ts +21 -0
  53. package/dist/core/task-registry.js +4 -1
  54. package/dist/core/types.d.ts +98 -3
  55. package/dist/core/types.js +3 -0
  56. package/dist/core/untrusted-text.d.ts +63 -0
  57. package/dist/core/untrusted-text.js +48 -0
  58. package/dist/core/wiring-manifest.d.ts +35 -0
  59. package/dist/core/wiring-manifest.js +21 -1
  60. package/dist/engine/harness/types.d.ts +36 -1
  61. package/dist/index.d.ts +7 -5
  62. package/dist/index.js +6 -4
  63. package/dist/internal/harness-types.d.ts +1 -0
  64. package/dist/stores/file/index.d.ts +19 -3
  65. package/dist/stores/file/index.js +24 -1
  66. package/dist/stores/file/session-store.d.ts +18 -4
  67. package/dist/stores/file/session-store.js +73 -12
  68. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  69. package/dist/tools/fs/fs-pdf.js +17 -3
  70. package/dist/tools/fs/fs-read.d.ts +2 -1
  71. package/dist/tools/fs/fs-read.js +33 -5
  72. package/dist/tools/fs/fs-shared.d.ts +6 -2
  73. package/dist/tools/fs/index.d.ts +7 -0
  74. package/dist/tools/fs/index.js +1 -1
  75. package/dist/tools/task-list.d.ts +5 -1
  76. package/dist/tools/web.js +21 -2
  77. package/package.json +3 -2
  78. package/test/export-surface.snapshot.json +24 -2
@@ -900,6 +900,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
900
900
  /** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
901
901
  search(query: string, scopes: readonly string[], opts?: {
902
902
  limit?: number;
903
+ exposureBands?: boolean;
903
904
  }): Promise<ScoredMemoryEntry[]>;
904
905
  private searchFrom;
905
906
  /**
@@ -2135,7 +2135,8 @@ export class FileMemoryEngineBackend {
2135
2135
  scored.push({ ...headerOf(e, join(dir, `${e.slug}.md`)), score: d });
2136
2136
  }
2137
2137
  }
2138
- scored.sort((a, b) => a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
2138
+ const band = opts?.exposureBands === true ? (h) => (h.exposure === "external" ? 1 : 0) : () => 0;
2139
+ scored.sort((a, b) => band(a) - band(b) || a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
2139
2140
  return scored.slice(0, limit);
2140
2141
  }
2141
2142
  async applyPatches(patches) {
@@ -3694,6 +3695,7 @@ function headerOf(e, path) {
3694
3695
  mtimeMs,
3695
3696
  rev: e.rev,
3696
3697
  sizeBytes: Buffer.byteLength(serializeEntryFile(e), "utf8"),
3698
+ ...(committedOriginOf(e.frontmatter) !== undefined ? { exposure: "external" } : {}),
3697
3699
  };
3698
3700
  }
3699
3701
  function readSafe(path) {
@@ -27,32 +27,59 @@ export declare function computeEntryRev(entry: Pick<MemoryEntry, "id" | "frontma
27
27
  /** True ⇔ `extra` carries origin-form bytes (an invalid/foreign origin block preserved verbatim,
28
28
  * or an origin block a pre-336 vintage folded into `extra`). */
29
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
+ */
30
58
  export declare function committedOriginOf(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): MemoryEntryOrigin | undefined;
31
59
  /** Deep equality over the origin marker (every member, absence included) — the immutability law's
32
60
  * comparator: a downgraded cause or a rewritten `at` is as refused as a strip. */
33
61
  export declare function originEquals(a: MemoryEntryOrigin | undefined, b: MemoryEntryOrigin | undefined): boolean;
34
62
  /**
35
- * design/336 (adversarial rounds 1+2) — the AMBIGUOUS origin-representation predicate, ONE law for
63
+ * design/336 (adversarial rounds 1-3) — the AMBIGUOUS origin-representation predicate, ONE law for
36
64
  * every inbound boundary (backend patch law, sync wire, bundle validation, out-of-band adoption):
37
- * a frontmatter whose marker has MORE THAN ONE carrier is refused unless it is the one legal
38
- * legacy shape a typed origin beside EXACTLY ONE extra-form carrier that normalizes EQUAL.
39
- * Covered vehicles: typed A + disagreeing extra block B (round 1 the typed member satisfies the
40
- * immutability comparator while B rides into storage and a merging reparse answers B); extra-only
41
- * duplicate blocks (round 2cross-block field combination plus the parser's block reordering
42
- * across a serialize cycle rewrite members with no typed origin at all); typed + multiple extra
43
- * carriers. Single-carrier shapes and the equal legacy carriage stay legal, so carry-forward of
44
- * old committed forms never bricks. The model-writable face never reaches this predicate — the
45
- * harvest strips every origin form and re-judges (§2.3-1).
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 1the 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).
46
72
  *
47
- * KNOWN OPEN (adversarial round 3 ESCALATED under the three-same-root-rounds rule, no further
48
- * spelling patch here): the predicate's accepted set is not SERIALIZATION-CLOSED. A scalar
49
- * `origin: …` sub-line INSIDE an accepted block is not counted as a carrier, yet a serialize →
50
- * reparse cycle migrates the block to the typed field and drops that sub-line into `extra` — the
51
- * semantic rev moves, so the committed account diverges from the reparsed bytes (downstream: CAS
52
- * refusal / quarantine, the FAIL-CLOSED direction the marker VALUE itself is preserved, no
53
- * member is rewritten). The registered deep fix is a serialization-closed carrier tokenization
54
- * with round-trip property tests (carrier count, normalized origin, ambiguity verdict and rev all
55
- * invariant across serialize/parse at every boundary) a slice-2 admission item, not a patch.
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).
56
83
  */
57
84
  export declare function ambiguousOriginRepresentation(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): boolean;
58
85
  /**
@@ -5,6 +5,10 @@ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/;
5
5
  export function isValidEntryId(value) {
6
6
  return ID_RE.test(value);
7
7
  }
8
+ const ORIGIN_AT_RE = /^-?\d+(\.\d+)?$/;
9
+ function atSerializationStable(at) {
10
+ return Number.isFinite(at) && ORIGIN_AT_RE.test(String(at));
11
+ }
8
12
  export function parseEntryFile(text) {
9
13
  const fm = {};
10
14
  if (!text.startsWith(`${FM_FENCE}\n`) && text.trimStart() !== FM_FENCE) {
@@ -31,7 +35,7 @@ export function parseEntryFile(text) {
31
35
  const prov = {};
32
36
  let inOrigin = false;
33
37
  const originRaw = [];
34
- const originUnknown = [];
38
+ let originPristine = true;
35
39
  const orig = {};
36
40
  for (let i = 1; i < end; i++) {
37
41
  const line = lines[i] ?? "";
@@ -83,17 +87,17 @@ export function parseEntryFile(text) {
83
87
  const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
84
88
  if (m) {
85
89
  const [, k = "", v = ""] = m;
86
- if (k === "taint")
90
+ if (k === "taint" && orig.taint === undefined)
87
91
  orig.taint = v;
88
- else if (k === "cause")
92
+ else if (k === "cause" && orig.cause === undefined)
89
93
  orig.cause = v;
90
- else if (k === "at")
91
- orig.at = /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : Number.NaN;
94
+ else if (k === "at" && orig.at === undefined)
95
+ orig.at = ORIGIN_AT_RE.test(v) ? Number(v) : Number.NaN;
92
96
  else
93
- originUnknown.push(line);
97
+ originPristine = false;
94
98
  }
95
99
  else {
96
- originUnknown.push(line);
100
+ originPristine = false;
97
101
  }
98
102
  continue;
99
103
  }
@@ -157,9 +161,12 @@ export function parseEntryFile(text) {
157
161
  }
158
162
  }
159
163
  if (originRaw.length > 0) {
160
- if (orig.taint === "external" && typeof orig.at === "number" && Number.isFinite(orig.at) && (orig.cause === undefined || MEMORY_ORIGIN_CAUSES.includes(orig.cause))) {
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))) {
161
169
  fm.origin = { taint: "external", ...(orig.cause !== undefined ? { cause: orig.cause } : {}), at: orig.at };
162
- extra.push(...originUnknown);
163
170
  }
164
171
  else {
165
172
  extra.push(...originRaw);
@@ -188,29 +195,31 @@ export function serializeEntryFile(entry) {
188
195
  }
189
196
  if (fm.trust !== undefined)
190
197
  lines.push(`trust: ${fm.trust}`);
198
+ if (fm.extra)
199
+ lines.push(...fm.extra);
191
200
  if (fm.origin !== undefined) {
192
201
  lines.push("origin:", ` taint: ${fm.origin.taint}`, ...(fm.origin.cause !== undefined ? [` cause: ${fm.origin.cause}`] : []), ` at: ${fm.origin.at}`);
193
202
  }
194
- if (fm.extra)
195
- lines.push(...fm.extra);
196
203
  lines.push(FM_FENCE, "");
197
204
  const body = entry.body.replace(/\s+$/, "");
198
205
  return `${lines.join("\n")}${body}${body ? "\n" : ""}`;
199
206
  }
200
207
  export function computeEntryRev(entry) {
201
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;
202
211
  const canonical = JSON.stringify([
203
212
  entry.id,
204
213
  fm.name ?? null,
205
214
  fm.description ?? null,
206
215
  fm.type ?? null,
207
216
  fm.deleted === true,
208
- fm.extra ?? [],
217
+ hasCarrierExtra ? partitionOriginLines(fm.extra).kept : (fm.extra ?? []),
209
218
  entry.body.replace(/\s+$/, ""),
210
219
  ...(fm.provenance !== undefined || fm.trust !== undefined
211
220
  ? [fm.trust ?? null, fm.provenance !== undefined ? [fm.provenance.kind, fm.provenance.path, fm.provenance.contentHash, fm.provenance.ingestedAt] : null]
212
221
  : []),
213
- ...(fm.origin !== undefined ? [["origin", fm.origin.taint, fm.origin.cause ?? null, fm.origin.at]] : []),
222
+ ...(canonicalOrigin !== undefined ? [["origin", canonicalOrigin.taint, canonicalOrigin.cause ?? null, canonicalOrigin.at]] : []),
214
223
  ]);
215
224
  return createHash("sha256").update(canonical, "utf8").digest("hex").slice(0, 16);
216
225
  }
@@ -218,55 +227,53 @@ const ORIGIN_FORM_LINE_RE = /^\s*origin\s*:/;
218
227
  export function hasOriginFormExtra(fm) {
219
228
  return fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line));
220
229
  }
221
- function walkExtraOrigin(extra) {
230
+ export function extraOriginCarriers(extra) {
222
231
  if (extra === undefined)
223
- return { carriers: 0, first: undefined };
224
- let carriers = 0;
225
- let inBlock = false;
226
- let blockIndex = 0;
227
- let taint;
228
- let cause;
229
- let at;
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
+ };
230
247
  for (const line of extra) {
231
- const trimmed = line.trim();
232
- if (/^origin\s*:\s*$/.test(trimmed)) {
233
- carriers += 1;
234
- inBlock = true;
235
- blockIndex += 1;
248
+ if (/^\s*origin\s*:\s*$/.test(line)) {
249
+ closeBlock();
250
+ block = {};
236
251
  continue;
237
252
  }
238
- if (ORIGIN_FORM_LINE_RE.test(line) && !inBlock) {
239
- carriers += 1;
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
+ }
240
264
  continue;
241
265
  }
242
- if (!/^\s/.test(line))
243
- inBlock = false;
244
- if (!inBlock || blockIndex !== 1)
245
- continue;
246
- const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
247
- if (m === null)
248
- continue;
249
- const [, k = "", v = ""] = m;
250
- if (k === "taint" && taint === undefined)
251
- taint = v;
252
- else if (k === "cause" && cause === undefined)
253
- cause = v;
254
- else if (k === "at" && at === undefined)
255
- at = /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : Number.NaN;
266
+ closeBlock();
267
+ if (ORIGIN_FORM_LINE_RE.test(line))
268
+ carriers.push({ taint: "external", at: 0 });
256
269
  }
257
- if (carriers === 0)
258
- return { carriers: 0, first: undefined };
259
- if (taint === "external" && typeof at === "number" && Number.isFinite(at) && (cause === undefined || MEMORY_ORIGIN_CAUSES.includes(cause))) {
260
- return { carriers, first: { taint: "external", ...(cause !== undefined ? { cause: cause } : {}), at } };
261
- }
262
- return { carriers, first: { taint: "external", at: 0 } };
270
+ closeBlock();
271
+ return carriers;
263
272
  }
264
273
  export function committedOriginOf(fm) {
265
274
  if (fm.origin !== undefined)
266
275
  return { taint: fm.origin.taint, ...(fm.origin.cause !== undefined ? { cause: fm.origin.cause } : {}), at: fm.origin.at };
267
- if (!hasOriginFormExtra(fm))
268
- return undefined;
269
- return walkExtraOrigin(fm.extra).first ?? { taint: "external", at: 0 };
276
+ return extraOriginCarriers(fm.extra)[0];
270
277
  }
271
278
  export function originEquals(a, b) {
272
279
  if (a === undefined || b === undefined)
@@ -274,13 +281,31 @@ export function originEquals(a, b) {
274
281
  return a.taint === b.taint && a.cause === b.cause && a.at === b.at;
275
282
  }
276
283
  export function ambiguousOriginRepresentation(fm) {
277
- const { carriers, first } = walkExtraOrigin(fm.extra);
278
- const total = carriers + (fm.origin !== undefined ? 1 : 0);
279
- if (total <= 1)
280
- return false;
281
- if (total === 2 && fm.origin !== undefined && carriers === 1)
282
- return !originEquals(fm.origin, first);
283
- return true;
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 };
284
309
  }
285
310
  export function stripModelWrittenOrigin(fm) {
286
311
  let stripped = false;
@@ -288,26 +313,15 @@ export function stripModelWrittenOrigin(fm) {
288
313
  delete fm.origin;
289
314
  stripped = true;
290
315
  }
291
- if (fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line))) {
292
- const kept = [];
293
- let inBlock = false;
294
- for (const line of fm.extra) {
295
- if (ORIGIN_FORM_LINE_RE.test(line)) {
296
- inBlock = /^\s*origin\s*:\s*$/.test(line);
297
- stripped = true;
298
- continue;
299
- }
300
- if (inBlock && /^\s/.test(line)) {
301
- stripped = true;
302
- continue;
303
- }
304
- inBlock = false;
305
- kept.push(line);
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;
306
324
  }
307
- if (kept.length > 0)
308
- fm.extra = kept;
309
- else
310
- delete fm.extra;
311
325
  }
312
326
  return stripped;
313
327
  }
@@ -1,11 +1,12 @@
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
8
  export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
8
- export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, type ParsedEntryFile } from "./frontmatter.js";
9
+ export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
9
10
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
10
11
  export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
11
12
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
@@ -1,11 +1,12 @@
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
8
  export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
8
- export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals } from "./frontmatter.js";
9
+ export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation } from "./frontmatter.js";
9
10
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
10
11
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
11
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";
@@ -360,6 +360,20 @@ export declare const LINEAGE_AUDIT_MAX_ROWS = 2048;
360
360
  /** The challenged-history account (design/180 B-3: collect-only, fail-open bookkeeping —
361
361
  * the mechanical shadow of "user saw a retrieved entry and corrected it in the same session"). */
362
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;
363
377
  /** One committed contribution: session → entry, latest rev/at only (the load-bearing invariant is
364
378
  * "which sessions contributed this id", not the per-commit history — that is the audit account). */
365
379
  export interface LineageContribution {
@@ -894,7 +894,7 @@ function rollForwardStrictSidecar(file, journal) {
894
894
  atomicWriteFileSync(file, raw);
895
895
  rmSync(journal, { force: true });
896
896
  }
897
- function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
897
+ export function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
898
898
  ensureDirExists(controlDir);
899
899
  const file = join(controlDir, fileName);
900
900
  const journal = `${file}.journal`;
@@ -934,7 +934,7 @@ function readStrictSidecarRaw(file, what) {
934
934
  throw new ControlPlaneCorruptError(`${what} is unparseable: ${file}`);
935
935
  }
936
936
  }
937
- function readStrictSidecar(controlDir, fileName, what) {
937
+ export function readStrictSidecar(controlDir, fileName, what) {
938
938
  const file = join(controlDir, fileName);
939
939
  const journal = `${file}.journal`;
940
940
  let journalRaw;
@@ -350,6 +350,49 @@ export async function memoryBackendContract(hooks) {
350
350
  const hits = await b.search("same words", ["s1"]);
351
351
  assert.deepStrictEqual(hits.map((h) => h.id), ["id-tie-a-01", "id-tie-b-01"]);
352
352
  });
353
+ defer("design/336: headers carry the exposure fact on BOTH faces (listHeaders + search), typed and extra-form marker alike; unmarked entries carry none", async () => {
354
+ const b = await hooks.make();
355
+ const marked = entry("id-exp-typed-01", "s1", "marked-typed", "same words here");
356
+ marked.frontmatter.origin = { taint: "external", cause: "observed", at: 1700000000600 };
357
+ marked.rev = computeEntryRev(marked);
358
+ const extraForm = entry("id-exp-extra-01", "s1", "marked-extra", "same words here");
359
+ extraForm.frontmatter.extra = ["origin:", " taint: external", " at: 5"];
360
+ extraForm.rev = computeEntryRev(extraForm);
361
+ const clean = entry("id-exp-clean-01", "s1", "unmarked", "same words here");
362
+ const rep = await b.applyPatches([
363
+ { op: "add", id: marked.id, entry: marked },
364
+ { op: "add", id: extraForm.id, entry: extraForm },
365
+ { op: "add", id: clean.id, entry: clean },
366
+ ]);
367
+ assert.deepStrictEqual(rep.conflicts, []);
368
+ const headers = await b.listHeaders(["s1"]);
369
+ assert.strictEqual(headers.find((h) => h.id === marked.id)?.exposure, "external", "typed marker ⇒ header exposure");
370
+ assert.strictEqual(headers.find((h) => h.id === extraForm.id)?.exposure, "external", "extra-form marker ⇒ header exposure (one normalization)");
371
+ assert.strictEqual(headers.find((h) => h.id === clean.id)?.exposure, undefined, "unmarked ⇒ no exposure member");
372
+ const hits = await b.search("same words", ["s1"]);
373
+ assert.strictEqual(hits.find((h) => h.id === marked.id)?.exposure, "external", "search hits carry the same fact");
374
+ assert.strictEqual(hits.find((h) => h.id === clean.id)?.exposure, undefined);
375
+ });
376
+ defer("design/336: search exposureBands orders unmarked-first BEFORE truncation (the limit-boundary clean entry is served, never starved); absent flag keeps the single-band contract order", async () => {
377
+ const b = await hooks.make();
378
+ const mk = (id, slug, marked) => {
379
+ const e = entry(id, "s1", slug, "same words here");
380
+ if (marked) {
381
+ e.frontmatter.origin = { taint: "external", cause: "observed", at: 1700000000700 };
382
+ e.rev = computeEntryRev(e);
383
+ }
384
+ return e;
385
+ };
386
+ const m1 = mk("id-band-a-01", "band-a", true);
387
+ const m2 = mk("id-band-b-01", "band-b", true);
388
+ const m3 = mk("id-band-c-01", "band-c", true);
389
+ const clean = mk("id-band-z-01", "band-z", false);
390
+ assert.deepStrictEqual((await b.applyPatches([m1, m2, m3, clean].map((e) => ({ op: "add", id: e.id, entry: e })))).conflicts, []);
391
+ const banded = await b.search("same words", ["s1"], { limit: 3, exposureBands: true });
392
+ assert.deepStrictEqual(banded.map((h) => h.id), ["id-band-z-01", "id-band-a-01", "id-band-b-01"], "two-band order must apply before the limit truncation (clean first, contract order within the band)");
393
+ const flat = await b.search("same words", ["s1"], { limit: 3 });
394
+ assert.deepStrictEqual(flat.map((h) => h.id), ["id-band-a-01", "id-band-b-01", "id-band-c-01"]);
395
+ });
353
396
  defer("consolidation cursor round-trips per scope; unset → undefined", async () => {
354
397
  const b = await hooks.make();
355
398
  assert.strictEqual(await b.getConsolidationCursor("s1"), undefined);
@@ -0,0 +1,66 @@
1
+ import type { MemoryEntryOrigin } from "./types.js";
2
+ export declare const ORIGIN_CLEARANCES_FILE = "origin-clearances.json";
3
+ /** One clearance's terminal event (appended, never rewritten). */
4
+ export interface OriginClearanceEvent {
5
+ eventId: string;
6
+ at: number;
7
+ to: "done" | "failed";
8
+ /** The resolver of THIS event (the opener on the normal path; a later resumer on a crash path). */
9
+ requestId: string;
10
+ /** Mechanical detail (landing slug, refusal reason) — engine-composed, bounded by the writer. */
11
+ detail?: string;
12
+ }
13
+ export interface OriginClearanceRow {
14
+ clearanceId: string;
15
+ /** The entry whose marker was cleared. The re-record keeps the SAME id (§2.3-2's later-batch
16
+ * arm), so this key joins the clearance to the entry across its whole life. */
17
+ entryId: string;
18
+ scope: string;
19
+ slug: string;
20
+ /** The committed rev the clearance was judged against (the tombstone's CAS anchor). */
21
+ baseRev: string;
22
+ /** The cleared marker, verbatim (audit: what the host vouched away). */
23
+ origin: MemoryEntryOrigin;
24
+ /** WHO cleared — the mandatory audit attribution (never defaulted). */
25
+ requestId: string;
26
+ /** WHY — the host's stated ground (never defaulted). */
27
+ reason: string;
28
+ at: number;
29
+ status: "pending" | "done" | "failed";
30
+ /** CUSTODY: the full serialized cleared-entry file text. A crash between the tombstone batch and
31
+ * the re-record batch leaves this as the only copy — the pending row is the loud recovery seat
32
+ * (a later clearEntryOrigin call for the same entry resumes from it). */
33
+ entryText: string;
34
+ events: OriginClearanceEvent[];
35
+ }
36
+ /** Lock-free strict read of the whole account (host audit face; journal-aware, corrupt = throw). */
37
+ export declare function readOriginClearances(controlDir: string): OriginClearanceRow[];
38
+ /**
39
+ * WRITE-AHEAD open: append the pending row (custody included) before any store mutation. Refuses a
40
+ * second OPEN while a pending row for the same entry stands — the pending row is a resume seat,
41
+ * not a queue (the caller resumes it instead).
42
+ */
43
+ export declare function openOriginClearance(controlDir: string, row: Omit<OriginClearanceRow, "status" | "events">): void;
44
+ /**
45
+ * Terminal event append + status flip. Unknown clearanceId is a corrupt-caller refusal.
46
+ *
47
+ * SINGLE-WINNER terminal (codex r1): a row already terminal ignores a late contradictory
48
+ * settlement — two concurrent resumers race the store legs (the store's own CAS decides those),
49
+ * and the loser's stale verdict must not overwrite the winner's audit state (a successfully
50
+ * cleared entry reading `failed` with contradictory events). The loser's own thrown refusal is
51
+ * its loud channel; a re-call then answers off the CURRENT committed state.
52
+ *
53
+ * `keepPending: true` appends the event WITHOUT flipping the status: the resume-seat form for a
54
+ * refused re-record leg — the tombstone has committed and the custody row is the ONLY copy of the
55
+ * bytes, so the row must stay resumable (a `failed` flip would strand them: the resume arm only
56
+ * completes pending rows).
57
+ */
58
+ export declare function settleOriginClearance(controlDir: string, input: {
59
+ clearanceId: string;
60
+ to: "done" | "failed";
61
+ requestId: string;
62
+ eventId: string;
63
+ now: () => number;
64
+ detail?: string;
65
+ keepPending?: boolean;
66
+ }): void;