@sema-agent/core 5.21.0 → 5.22.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 (73) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +45 -4
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +11 -0
  18. package/dist/core/hooks.d.ts +39 -0
  19. package/dist/core/hooks.js +26 -2
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +67 -1
  25. package/dist/core/memory-engine/engine.js +270 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +166 -0
  31. package/dist/core/memory-engine/layout.js +399 -0
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +91 -20
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +161 -0
  39. package/dist/core/permission-rule-org.js +211 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +313 -3
  42. package/dist/core/permission-rule-sync.d.ts +131 -0
  43. package/dist/core/permission-rule-sync.js +314 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +54 -1
  46. package/dist/core/runner/prepare-task.js +246 -27
  47. package/dist/core/runner/runtask.js +147 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +120 -2
  61. package/dist/core/tool-policy.js +116 -6
  62. package/dist/core/trace.d.ts +32 -1
  63. package/dist/core/types.d.ts +56 -3
  64. package/dist/index.d.ts +12 -7
  65. package/dist/index.js +10 -5
  66. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  69. package/dist/stores/file/permission-rule-adopt.js +95 -0
  70. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  71. package/dist/stores/file/permission-rule-store.js +189 -46
  72. package/dist/tools/fs/fs-search-tools.js +0 -1
  73. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
6
6
  import { formatMemoryAge } from "../memory-recall.js";
7
7
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
8
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
9
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
9
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
10
10
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
11
11
  export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
12
12
 
@@ -33,6 +33,7 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
33
33
  return MEMORY_INSTRUCTION_TEMPLATE.replaceAll("{{MEMORY_DIR}}", dir).replaceAll("{{INSTRUCTION_FILE}}", instructionFileName ?? "CLAUDE.md");
34
34
  }
35
35
  export const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry — the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
36
+ export const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines — write them yourself; nothing fills them in for you.";
36
37
  export const MEMORY_INDEX_MAX_LINES = 200;
37
38
  export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
38
39
  export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
@@ -87,6 +88,8 @@ export class MemoryEngine {
87
88
  }
88
89
  }
89
90
  recordRetrieved(ids) {
91
+ for (const id of ids)
92
+ this.retrievedThisSession.add(id);
90
93
  try {
91
94
  recordRetrievedAccount(this.controlDir, ids, this.now);
92
95
  }
@@ -124,6 +127,28 @@ export class MemoryEngine {
124
127
  }
125
128
  }
126
129
  }
130
+ try {
131
+ const rec = this.sessionPollution(sessionId) ?? { at: this.now(), reason };
132
+ const contributions = lineageContributionsOfSession(this.controlDir, sessionId);
133
+ this.challengeAndAnnounce(contributions.map((c) => ({
134
+ eventId: `pollution:${sessionId}:${rec.at}:${c.entryId}`,
135
+ entryId: c.entryId,
136
+ reason: `challenged: the contributing session was marked polluted`,
137
+ challengedRev: c.lastRev,
138
+ })));
139
+ }
140
+ catch (err) {
141
+ const sink = this.onIncident;
142
+ if (sink !== undefined) {
143
+ try {
144
+ const e = new Error(`memory challenge sweep for a polluted session failed: ${err instanceof Error ? err.message : String(err)}`);
145
+ e.code = "memory.challenge_sweep_failed";
146
+ sink(e);
147
+ }
148
+ catch {
149
+ }
150
+ }
151
+ }
127
152
  }
128
153
  sessionPollution(sessionId) {
129
154
  const inProcess = this.pollutedSessions.get(sessionId);
@@ -136,6 +161,103 @@ export class MemoryEngine {
136
161
  return undefined;
137
162
  }
138
163
  }
164
+ retrievedThisSession = new Set();
165
+ readChallengeExclusions() {
166
+ const out = new Map();
167
+ for (const id of lineageLatchedIds(this.controlDir))
168
+ out.set(id, { code: "lineage_pending" });
169
+ for (const [id, g] of challengedEntryIds(this.controlDir))
170
+ out.set(id, { code: "challenged", generation: g.generation, at: g.at });
171
+ return out;
172
+ }
173
+ challengeAndAnnounce(events) {
174
+ if (events.length === 0)
175
+ return [];
176
+ const assigned = appendChallengeEvents(this.controlDir, events, this.now);
177
+ const fresh = assigned.filter((a) => !a.replayed);
178
+ if (fresh.length > 0) {
179
+ try {
180
+ enqueueMemoryAnnouncement(this.controlDir, {
181
+ kind: "gate",
182
+ at: this.now(),
183
+ items: fresh.map((a) => `memory entry challenged: id ${quoteId(a.entryId)} (generation ${a.generation}) — its content is withheld from memory tools and the injected index. If the fact is still needed, verify it with the user, record a fresh entry from their current statement, and tombstone the old one.`),
184
+ });
185
+ }
186
+ catch (err) {
187
+ this.discloseAnnounceFailure("challenge enqueue", err);
188
+ }
189
+ }
190
+ return assigned;
191
+ }
192
+ challengeEntries(ids, reason, requestId) {
193
+ if (typeof requestId !== "string" || requestId === "") {
194
+ const e = new Error("challengeEntries: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
195
+ e.code = "config.memory_challenge_request";
196
+ throw e;
197
+ }
198
+ return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
199
+ }
200
+ challengeSession(sessionId, reason, requestId) {
201
+ if (typeof requestId !== "string" || requestId === "") {
202
+ const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
203
+ e.code = "config.memory_challenge_request";
204
+ throw e;
205
+ }
206
+ const contributions = lineageContributionsOfSession(this.controlDir, sessionId);
207
+ return this.challengeAndAnnounce(contributions.map((c) => ({ eventId: `${requestId}:${c.entryId}`, entryId: c.entryId, reason, challengedRev: c.lastRev })));
208
+ }
209
+ resolveChallenge(entryId, generation, reason, requestId) {
210
+ return resolveChallengeEvent(this.controlDir, entryId, generation, reason, this.now, requestId);
211
+ }
212
+ adjudicatePendingLineage(txnId, action) {
213
+ const promoted = adjudicateLineagePending(this.controlDir, txnId, action, this.now);
214
+ this.settlePromotions(promoted);
215
+ return promoted;
216
+ }
217
+ listPendingLineage() {
218
+ return readLineageRecord(this.controlDir).pending;
219
+ }
220
+ readChallengeLedger() {
221
+ return readChallengeEvents(this.controlDir);
222
+ }
223
+ readChallengedHistoryAccount() {
224
+ try {
225
+ return readChallengedHistory(this.controlDir);
226
+ }
227
+ catch {
228
+ return {};
229
+ }
230
+ }
231
+ settlePromotions(promoted) {
232
+ if (promoted.length === 0)
233
+ return;
234
+ const events = [];
235
+ for (const p of promoted) {
236
+ let rec;
237
+ try {
238
+ rec = readSessionPollution(this.controlDir, p.sessionId);
239
+ }
240
+ catch {
241
+ rec = undefined;
242
+ }
243
+ if (rec === undefined)
244
+ rec = this.pollutedSessions.get(p.sessionId);
245
+ if (rec === undefined)
246
+ continue;
247
+ events.push({
248
+ eventId: `pollution:${p.sessionId}:${rec.at}:${p.entryId}`,
249
+ entryId: p.entryId,
250
+ reason: `challenged: the contributing session was marked polluted`,
251
+ challengedRev: p.rev,
252
+ });
253
+ }
254
+ this.challengeAndAnnounce(events);
255
+ try {
256
+ appendLineageAudit(this.controlDir, promoted.map((p) => ({ entryId: p.entryId, sessionId: p.sessionId, rev: p.rev, at: this.now() })));
257
+ }
258
+ catch {
259
+ }
260
+ }
139
261
  async materialize(scopes, writeScope) {
140
262
  ensureDirExists(this.memoryDir);
141
263
  ensureDirExists(this.controlDir);
@@ -316,7 +438,8 @@ export class MemoryEngine {
316
438
  return report;
317
439
  }
318
440
  async harvestCore(handle, opts) {
319
- const pollutedReason = opts?.polluted?.reason;
441
+ let pollutedReason = opts?.polluted?.reason ?? (opts?.sessionId !== undefined ? this.sessionPollution(opts.sessionId)?.reason : undefined);
442
+ const lineageSessionId = opts?.sessionId;
320
443
  const startedAt = this.now();
321
444
  const report = {
322
445
  ok: true,
@@ -344,6 +467,30 @@ export class MemoryEngine {
344
467
  report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${err instanceof Error ? err.message : String(err)}` };
345
468
  return report;
346
469
  }
470
+ try {
471
+ const rec = reconcileLineage(this.controlDir, this.now);
472
+ this.settlePromotions(rec.promoted);
473
+ for (const u of rec.undecidable) {
474
+ report.warnings.push(`memory lineage transaction ${u.txnId} is unsettled (a crash landed between commit and its durable credential) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the host adjudicates it (adjudicatePendingLineage)`);
475
+ }
476
+ if (pollutedReason !== undefined && lineageSessionId !== undefined) {
477
+ const rec2 = this.sessionPollution(lineageSessionId);
478
+ if (rec2 !== undefined) {
479
+ const contributions = lineageContributionsOfSession(this.controlDir, lineageSessionId);
480
+ this.challengeAndAnnounce(contributions.map((c) => ({
481
+ eventId: `pollution:${lineageSessionId}:${rec2.at}:${c.entryId}`,
482
+ entryId: c.entryId,
483
+ reason: `challenged: the contributing session was marked polluted`,
484
+ challengedRev: c.lastRev,
485
+ })));
486
+ }
487
+ }
488
+ }
489
+ catch (err) {
490
+ report.ok = false;
491
+ report.incident = { kind: "sidecar_corrupt", detail: `memory lineage/challenge ledger refused: ${err instanceof Error ? err.message : String(err)}` };
492
+ return report;
493
+ }
347
494
  if (!existsSync(handle.writableRoot)) {
348
495
  report.ok = false;
349
496
  report.incident = { kind: "dir_missing", detail: `memory writable root is gone: ${handle.writableRoot}` };
@@ -693,13 +840,99 @@ export class MemoryEngine {
693
840
  patchReport = { applied: [], conflicts: [] };
694
841
  }
695
842
  else {
696
- try {
697
- patchReport = await this.backend.applyPatches(patches);
843
+ const txnId = uuidv7();
844
+ const pendingRows = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev }));
845
+ const lineageArmed = lineageSessionId !== undefined && pendingRows.length > 0;
846
+ if (lineageArmed) {
847
+ try {
848
+ stageLineagePending(this.controlDir, txnId, lineageSessionId, pendingRows, this.now);
849
+ }
850
+ catch (err) {
851
+ report.ok = false;
852
+ report.incident = { kind: "sidecar_corrupt", detail: `memory lineage write-ahead refused: ${err instanceof Error ? err.message : String(err)}` };
853
+ return report;
854
+ }
698
855
  }
699
- catch (err) {
700
- report.ok = false;
701
- report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${err instanceof Error ? err.message : String(err)}` };
702
- return report;
856
+ const preCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
857
+ if (preCommit !== undefined) {
858
+ if (lineageArmed) {
859
+ try {
860
+ discardLineagePending(this.controlDir, txnId);
861
+ }
862
+ catch {
863
+ }
864
+ }
865
+ pollutedReason = preCommit.reason;
866
+ for (const f of records)
867
+ await containPollutedRecord(f);
868
+ patchReport = { applied: [], conflicts: [] };
869
+ }
870
+ else {
871
+ try {
872
+ patchReport = await this.backend.applyPatches(patches);
873
+ }
874
+ catch (err) {
875
+ if (lineageArmed) {
876
+ try {
877
+ discardLineagePending(this.controlDir, txnId);
878
+ }
879
+ catch {
880
+ }
881
+ }
882
+ report.ok = false;
883
+ report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${err instanceof Error ? err.message : String(err)}` };
884
+ return report;
885
+ }
886
+ if (lineageArmed) {
887
+ try {
888
+ recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
889
+ const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
890
+ this.settlePromotions(promoted);
891
+ }
892
+ catch (err) {
893
+ const detail = `memory lineage settlement failed after commit — the affected entries stay latched (memory reads refuse them) until a later harvest reconciles or the host adjudicates: ${err instanceof Error ? err.message : String(err)}`;
894
+ report.warnings.push(detail);
895
+ const sink = this.onIncident;
896
+ if (sink !== undefined) {
897
+ try {
898
+ const e = new Error(detail);
899
+ e.code = "memory.lineage_settle_failed";
900
+ sink(e);
901
+ }
902
+ catch {
903
+ }
904
+ }
905
+ }
906
+ }
907
+ const deletedIds = patchReport.applied.filter((a) => a.op === "delete").map((a) => a.id);
908
+ if (deletedIds.length > 0) {
909
+ try {
910
+ clearLineageForEntries(this.controlDir, deletedIds);
911
+ }
912
+ catch {
913
+ }
914
+ }
915
+ try {
916
+ recordChallengedHistory(this.controlDir, patchReport.applied
917
+ .filter((a) => (a.op === "update" || a.op === "delete") && this.retrievedThisSession.has(a.id))
918
+ .map((a) => ({ entryId: a.id, op: a.op === "delete" ? "tombstone" : "update" })), this.now);
919
+ }
920
+ catch {
921
+ }
922
+ const postCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
923
+ if (postCommit !== undefined) {
924
+ const appliedNonDelete = patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id);
925
+ try {
926
+ this.challengeAndAnnounce(appliedNonDelete.map((id) => ({
927
+ eventId: `pollution:${lineageSessionId}:${postCommit.at}:${id}`,
928
+ entryId: id,
929
+ reason: `challenged: the contributing session was marked polluted`,
930
+ })));
931
+ }
932
+ catch (err) {
933
+ report.warnings.push(`memory challenge of this harvest's commits failed (session polluted post-commit): ${err instanceof Error ? err.message : String(err)}`);
934
+ }
935
+ }
703
936
  }
704
937
  }
705
938
  const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
@@ -798,18 +1031,39 @@ export class MemoryEngine {
798
1031
  }
799
1032
  rebuildIndex(handle, headers, opts, warnings) {
800
1033
  const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
1034
+ let excludedIds;
1035
+ try {
1036
+ excludedIds = this.readChallengeExclusions();
1037
+ }
1038
+ catch (err) {
1039
+ const detail = `memory index withheld: the challenge/lineage ledger cannot be trusted (${err instanceof Error ? err.message : String(err)}) — fail-closed, nothing injected`;
1040
+ warnings?.push(detail);
1041
+ handle.indexOnDiskUntrusted = true;
1042
+ const sink = this.onIncident;
1043
+ if (sink !== undefined) {
1044
+ try {
1045
+ const e = new Error(detail);
1046
+ e.code = "memory.challenge_ledger_corrupt";
1047
+ sink(e);
1048
+ }
1049
+ catch {
1050
+ }
1051
+ }
1052
+ return "";
1053
+ }
801
1054
  const existing = opts.ignoreOnDisk === true ? "" : (readNoFollowSafe(indexPath) ?? "");
802
1055
  const indexRevs = readIndexRevs(this.controlDir);
803
1056
  const nextIndexRevs = {};
804
1057
  const expected = new Map();
1058
+ const excludedTargets = new Set();
805
1059
  for (const h of headers) {
806
1060
  const repoTarget = handle.repoIndexTargets?.get(h.id);
807
- if (repoTarget !== undefined) {
808
- expected.set(repoTarget, h);
1061
+ const target = repoTarget ?? relative(handle.writableRoot, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
1062
+ if (excludedIds.has(h.id)) {
1063
+ excludedTargets.add(target);
809
1064
  continue;
810
1065
  }
811
- const file = join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`);
812
- expected.set(relative(handle.writableRoot, canonicalize(file)), h);
1066
+ expected.set(target, h);
813
1067
  }
814
1068
  const covered = new Set();
815
1069
  const keptByTarget = new Map();
@@ -821,6 +1075,10 @@ export class MemoryEngine {
821
1075
  keptLines.push(line);
822
1076
  continue;
823
1077
  }
1078
+ if (excludedTargets.has(target)) {
1079
+ warnings?.push(`MEMORY.md index line removed: ${target} is withheld (challenged or unsettled) — its content stays off the injected index`);
1080
+ continue;
1081
+ }
824
1082
  if (covered.has(target)) {
825
1083
  if (line === keptByTarget.get(target))
826
1084
  continue;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * design/180 裁决 C — the v2 header hints, read-only, over `frontmatter.extra`.
3
+ *
4
+ * The v2 header fields (pinned / triggers / last-confirmed / applies-when) are DELIBERATELY not
5
+ * typed structure positions: the parser's closed grammar keeps unknown lines VERBATIM in `extra`,
6
+ * `extra` participates in the entry rev, and the sync wire carries it — so a file with these lines
7
+ * computes the SAME rev on every client vintage (old and new), and no backend/sync twin owes an
8
+ * upgrade. Promoting them to typed fields would make the same bytes hash to two different revs
9
+ * across a mixed fleet (the provenance note in frontmatter.ts names that exact incident shape).
10
+ *
11
+ * This accessor is the ONLY consumer-side reading: pure, lenient, and closed over bad values —
12
+ * a malformed line reads as FIELD ABSENT, never an error and never a partial adoption. Nothing in
13
+ * the engine writes these lines; the model does (the preference-discipline paragraph teaches it),
14
+ * because an engine-written confirmation would forge the usage evidence it is supposed to record.
15
+ */
16
+ /** The parsed v2 hints. Every field is absent unless a well-formed line was found. */
17
+ export interface V2HeaderHints {
18
+ /** `pinned: true` — only the exact literal counts (anything else = absent). */
19
+ pinned?: boolean;
20
+ /** `triggers: a, b, c` — comma-split, trimmed, empties dropped; an empty result = absent. */
21
+ triggers?: string[];
22
+ /** `last-confirmed: YYYY-MM-DD…` — kept as the raw string when it starts with an ISO-date shape;
23
+ * anything else = absent (bad values must not masquerade as confirmations). */
24
+ lastConfirmedAt?: string;
25
+ /** `applies-when: <free text>` — non-empty free text, trimmed. */
26
+ appliesWhen?: string;
27
+ }
28
+ /** Read the v2 header hints out of an entry's preserved-verbatim `extra` lines. First well-formed
29
+ * occurrence of each key wins (duplicate lines are model bookkeeping noise, not a merge input). */
30
+ export declare function readV2HeaderHints(extra: readonly string[] | undefined): V2HeaderHints;
@@ -0,0 +1,41 @@
1
+ const LINE_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/;
2
+ const ISO_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}(?:$|[T\s])/;
3
+ export function readV2HeaderHints(extra) {
4
+ const out = {};
5
+ if (extra === undefined)
6
+ return out;
7
+ for (const line of extra) {
8
+ const m = LINE_RE.exec(line);
9
+ if (m === null)
10
+ continue;
11
+ const [, key, value] = m;
12
+ switch (key) {
13
+ case "pinned":
14
+ if (out.pinned === undefined && value === "true")
15
+ out.pinned = true;
16
+ break;
17
+ case "triggers": {
18
+ if (out.triggers !== undefined)
19
+ break;
20
+ const items = value
21
+ .split(",")
22
+ .map((s) => s.trim())
23
+ .filter((s) => s !== "");
24
+ if (items.length > 0)
25
+ out.triggers = items;
26
+ break;
27
+ }
28
+ case "last-confirmed":
29
+ if (out.lastConfirmedAt === undefined && ISO_DATE_PREFIX_RE.test(value))
30
+ out.lastConfirmedAt = value;
31
+ break;
32
+ case "applies-when":
33
+ if (out.appliesWhen === undefined && value !== "")
34
+ out.appliesWhen = value;
35
+ break;
36
+ default:
37
+ break;
38
+ }
39
+ }
40
+ return out;
41
+ }
@@ -1,8 +1,9 @@
1
- export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
1
+ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
2
2
  export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
3
3
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
4
4
  export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
5
- 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, } from "./layout.js";
5
+ 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, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
6
+ export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
6
7
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
7
8
  export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
8
9
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
@@ -1,8 +1,9 @@
1
- export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
1
+ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
2
2
  export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
3
3
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
4
4
  export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
5
- 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, } from "./layout.js";
5
+ 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, } from "./layout.js";
6
+ export { readV2HeaderHints } from "./header-hints.js";
6
7
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
7
8
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
8
9
  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";
@@ -273,6 +273,172 @@ export declare function recordRetrievedAccount(controlDir: string, ids: readonly
273
273
  export declare function readRetrievedAccount(controlDir: string): Record<string, RetrievedAccountRow>;
274
274
  /** S2-C — clear the fuse for files harvest just ACCEPTED (a committed write resets the strike count). */
275
275
  export declare function clearScanFuse(controlDir: string, keys: Iterable<string>): void;
276
+ /** The write-ahead harvest lineage ledger (control plane, fail-closed). */
277
+ export declare const LINEAGE_FILE = "lineage.json";
278
+ /** The challenge generation-event ledger (control plane, fail-closed). */
279
+ export declare const CHALLENGES_FILE = "challenges.json";
280
+ /** Per-commit lineage AUDIT history (bookkeeping, fail-open, bounded rotation) — the load-bearing
281
+ * queries never read it; it exists so the deduped contribution set is not the only trace. */
282
+ export declare const LINEAGE_AUDIT_FILE = "lineage-audit.json";
283
+ /** Bound on retained audit rows (oldest rotated out first). */
284
+ export declare const LINEAGE_AUDIT_MAX_ROWS = 2048;
285
+ /** The challenged-history account (design/180 B-3: collect-only, fail-open bookkeeping —
286
+ * the mechanical shadow of "user saw a retrieved entry and corrected it in the same session"). */
287
+ export declare const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
288
+ /** One committed contribution: session → entry, latest rev/at only (the load-bearing invariant is
289
+ * "which sessions contributed this id", not the per-commit history — that is the audit account). */
290
+ export interface LineageContribution {
291
+ lastRev: string;
292
+ lastAt: number;
293
+ }
294
+ /** One staged (pre-commit) row: an entry this transaction WOULD commit. */
295
+ export interface LineagePendingRow {
296
+ entryId: string;
297
+ rev: string;
298
+ }
299
+ /** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
300
+ * succeeds; promotion consumes both. A pending txn without a credential is the crash window —
301
+ * it is NEVER auto-discarded (design/180 r5: the applyPatches-success→credential-write gap is
302
+ * undecidable on the pinned backend contract; the host adjudicates via the explicit API). */
303
+ export interface LineagePendingTxn {
304
+ sessionId: string;
305
+ at: number;
306
+ rows: LineagePendingRow[];
307
+ /** Present ⇔ `applyPatches` returned and its applied set was durably recorded. */
308
+ credential?: {
309
+ appliedIds: string[];
310
+ at: number;
311
+ };
312
+ }
313
+ /** Stage this transaction's would-commit rows BEFORE `applyPatches` (write-ahead). Throws on any
314
+ * write/lock/corruption failure — the caller refuses the whole harvest (fail-closed). */
315
+ export declare function stageLineagePending(controlDir: string, txnId: string, sessionId: string, rows: readonly LineagePendingRow[], now: () => number): void;
316
+ /** Discard a staged transaction that never reached `applyPatches` (the pre-commit abort path —
317
+ * step ③ of the handshake, or a refused commit). Safe by construction: nothing was committed. */
318
+ export declare function discardLineagePending(controlDir: string, txnId: string): void;
319
+ /** Record the commit credential — `applyPatches` returned, these ids were APPLIED (non-delete ops).
320
+ * Written before promotion so a crash between the two converges by credential, never by reading
321
+ * the backend's current state back (design/180 r4: later syncs/updates rewrite that scene). */
322
+ export declare function recordLineageCredential(controlDir: string, txnId: string, appliedIds: readonly string[], now: () => number): void;
323
+ /** What one promotion moved into the committed set. */
324
+ export interface LineagePromotion {
325
+ entryId: string;
326
+ sessionId: string;
327
+ rev: string;
328
+ }
329
+ /** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
330
+ * committed contribution set (deduped by (entryId, sessionId) — a repeat pair refreshes
331
+ * lastRev/lastAt, never grows the set); rows NOT in the applied set (conflicted/refused ops) are
332
+ * dropped without ever becoming ancestry. Throws when the txn is unknown or uncredentialed. */
333
+ export declare function promoteLineagePending(controlDir: string, txnId: string, now: () => number): LineagePromotion[];
334
+ /** The host adjudication for the UNDECIDABLE crash window (a pending txn with no credential —
335
+ * design/180 r5: never auto-discarded). `"promote"` = the host verified the commit landed (all
336
+ * staged rows join the committed set); `"discard"` = the host verified it did not. Idempotent on
337
+ * an unknown txn. Also accepts a credentialed txn (then `"promote"` follows the credential). */
338
+ export declare function adjudicateLineagePending(controlDir: string, txnId: string, action: "promote" | "discard", now: () => number): LineagePromotion[];
339
+ /** Reconciliation (harvest-opening / host-triggered): promote every CREDENTIALED orphan pending
340
+ * txn; report the uncredentialed ones (their latch stays until the host adjudicates). */
341
+ export declare function reconcileLineage(controlDir: string, now: () => number): {
342
+ promoted: LineagePromotion[];
343
+ undecidable: Array<{
344
+ txnId: string;
345
+ sessionId: string;
346
+ entryIds: string[];
347
+ }>;
348
+ };
349
+ /** The DIRTY-LATCH set: every entry id named by ANY pending row (credentialed or not). While an id
350
+ * is latched, its model-visible read faces (index mechanical rows / memory_search / memory_get)
351
+ * refuse — "the account is not settled, the content does not go on the table". Throws on a
352
+ * corrupt ledger (integrity unknown ≠ no latch). */
353
+ export declare function lineageLatchedIds(controlDir: string): Set<string>;
354
+ /** Every committed contribution of `sessionId` (the pollution-retroaction sweep input). Throws on
355
+ * a corrupt ledger. */
356
+ export declare function lineageContributionsOfSession(controlDir: string, sessionId: string): Array<{
357
+ entryId: string;
358
+ lastRev: string;
359
+ }>;
360
+ /** Drop an entry's lineage rows once the entry is DELETED (an applied tombstone) — space hygiene
361
+ * only, taken at the harvest that saw the applied delete (design/180 §10). */
362
+ export declare function clearLineageForEntries(controlDir: string, entryIds: readonly string[]): void;
363
+ /** Full-ledger read (tests / host observability). Throws on corruption. */
364
+ export declare function readLineageRecord(controlDir: string): {
365
+ committed: Record<string, Record<string, LineageContribution>>;
366
+ pending: Record<string, LineagePendingTxn>;
367
+ };
368
+ /** Fail-open per-commit AUDIT history (bounded rotation) — never consulted by the load-bearing
369
+ * queries; a broken audit account degrades silently, exactly like the retrieved account. */
370
+ export declare function appendLineageAudit(controlDir: string, rows: ReadonlyArray<{
371
+ entryId: string;
372
+ sessionId: string;
373
+ rev: string;
374
+ at: number;
375
+ }>): void;
376
+ /** One challenge/resolve event. Append-only; the exclusion predicate is DERIVED ("any unresolved
377
+ * challenge generation exists"), never stored as a mutable flag. */
378
+ export interface ChallengeEvent {
379
+ /** Stable identity for idempotent replay: the automatic arm derives it from the pollution event
380
+ * anchor (`pollution:<sessionId>:<markedAt>:<entryId>`); the host arm derives it from the
381
+ * caller's requestId (`<requestId>:<entryId>`). Same eventId replayed = same generation (a lost
382
+ * ack retried never opens a second generation); a DIFFERENT eventId with the same reason is a
383
+ * genuinely new event and opens a new generation (no immunity hole). */
384
+ eventId: string;
385
+ entryId: string;
386
+ kind: "challenge" | "resolve";
387
+ /** The generation this event opened (challenge) or closed (resolve). */
388
+ generation: number;
389
+ at: number;
390
+ reason: string;
391
+ /** Evidence grade — v2 mints only "S" (structural) events. */
392
+ grade?: "S";
393
+ /** Audit record only (the exclusion predicate keys the entry ID, never the rev). */
394
+ challengedRev?: string;
395
+ }
396
+ /** One challenge assignment {@link appendChallengeEvents} returns. `replayed` = the eventId already
397
+ * existed (idempotent retry — the ORIGINAL generation is returned, no new one opens). */
398
+ export interface ChallengeAssignment {
399
+ entryId: string;
400
+ eventId: string;
401
+ generation: number;
402
+ at: number;
403
+ replayed: boolean;
404
+ }
405
+ /** Append S-grade challenge events (idempotent per eventId, generation allocation inside the strict
406
+ * lock). Every NEW eventId opens a NEW generation for its entry — a resolve of an earlier
407
+ * generation never immunizes the entry against later pollution (design/180 R-8). */
408
+ export declare function appendChallengeEvents(controlDir: string, events: ReadonlyArray<{
409
+ eventId: string;
410
+ entryId: string;
411
+ reason: string;
412
+ challengedRev?: string;
413
+ }>, now: () => number): ChallengeAssignment[];
414
+ /** The trusted resolve: closes EXACTLY the named generation (a later challenge's generation stays
415
+ * open — resolves never blanket-clear). Idempotent: resolving an already-resolved generation is a
416
+ * no-op. Returns false ⇔ no such open generation existed. */
417
+ export declare function resolveChallengeEvent(controlDir: string, entryId: string, generation: number, reason: string, now: () => number, requestId?: string): boolean;
418
+ /** The exclusion predicate's input: entry id → its LATEST unresolved challenge generation. An entry
419
+ * appears iff at least one challenge generation has no matching resolve. Throws on corruption —
420
+ * consumers refuse service (integrity unknown ≠ clean). */
421
+ export declare function challengedEntryIds(controlDir: string): Map<string, {
422
+ generation: number;
423
+ at: number;
424
+ }>;
425
+ /** Full event read (host audit / tests). Throws on corruption. */
426
+ export declare function readChallengeEvents(controlDir: string): ChallengeEvent[];
427
+ /** One challenged-history row: how often a retrieved-then-corrected event hit this entry. */
428
+ export interface ChallengedHistoryRow {
429
+ count: number;
430
+ lastAt: number;
431
+ lastOp: "update" | "tombstone";
432
+ }
433
+ /** Record mechanical correction shadows (same-session retrieved → update/tombstone committed).
434
+ * Collect-only by contract: NOTHING in the engine reads this back into any decision (v3 candidate
435
+ * evidence only — design/178 ③-1 三级账 discipline). Throws propagate; callers treat fail-open. */
436
+ export declare function recordChallengedHistory(controlDir: string, rows: ReadonlyArray<{
437
+ entryId: string;
438
+ op: "update" | "tombstone";
439
+ }>, now: () => number): void;
440
+ /** Journal-aware read (observability/tests only — no engine consumer exists, on purpose). */
441
+ export declare function readChallengedHistory(controlDir: string): Record<string, ChallengedHistoryRow>;
276
442
  /**
277
443
  * REF-C6 — write EVERY byte of `data` to `fd`, looping until the OS has taken all of them.
278
444
  *