@sema-agent/core 5.21.1 → 5.23.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 (96) hide show
  1. package/CHANGELOG.md +103 -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 +48 -6
  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 +14 -0
  18. package/dist/core/hooks.d.ts +101 -1
  19. package/dist/core/hooks.js +116 -14
  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 +94 -1
  25. package/dist/core/memory-engine/engine.js +329 -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 +232 -0
  31. package/dist/core/memory-engine/layout.js +469 -1
  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 +93 -21
  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 +195 -0
  39. package/dist/core/permission-rule-org.js +234 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +314 -3
  42. package/dist/core/permission-rule-sync.d.ts +139 -0
  43. package/dist/core/permission-rule-sync.js +343 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +64 -3
  46. package/dist/core/runner/prepare-task.js +357 -32
  47. package/dist/core/runner/runtask.js +166 -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 +155 -4
  61. package/dist/core/tool-policy.js +148 -10
  62. package/dist/core/tool-result-store.d.ts +9 -1
  63. package/dist/core/tool-result-store.js +2 -1
  64. package/dist/core/trace.d.ts +79 -1
  65. package/dist/core/types.d.ts +94 -3
  66. package/dist/core/wiring-manifest.d.ts +16 -1
  67. package/dist/core/wiring-manifest.js +7 -1
  68. package/dist/index.d.ts +15 -8
  69. package/dist/index.js +12 -5
  70. package/dist/orchestration/goal.d.ts +10 -0
  71. package/dist/orchestration/goal.js +6 -5
  72. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  73. package/dist/stores/file/adoption/adopt.js +616 -0
  74. package/dist/stores/file/adoption/marker.d.ts +194 -0
  75. package/dist/stores/file/adoption/marker.js +198 -0
  76. package/dist/stores/file/background-agent-store.js +2 -0
  77. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  78. package/dist/stores/file/checkpoint-store.js +3 -0
  79. package/dist/stores/file/file-snapshot-store.js +2 -0
  80. package/dist/stores/file/index.d.ts +2 -0
  81. package/dist/stores/file/index.js +4 -0
  82. package/dist/stores/file/mailbox-store.js +2 -0
  83. package/dist/stores/file/memory-store.js +2 -0
  84. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  85. package/dist/stores/file/permission-rule-adopt.js +95 -0
  86. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  87. package/dist/stores/file/permission-rule-store.js +189 -46
  88. package/dist/stores/file/session-policy-store.js +2 -0
  89. package/dist/stores/file/session-store.js +2 -0
  90. package/dist/stores/file/task-list-store.js +2 -0
  91. package/dist/stores/file/tool-result-store.js +2 -0
  92. package/dist/stores/file/usage-window-store.js +2 -0
  93. package/dist/stores/file/workflow-journal-store.js +2 -0
  94. package/dist/stores/file/workflow-run-store.js +2 -0
  95. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  96. package/package.json +3 -2
@@ -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, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, 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,162 @@ 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 { assignments: assigned, eventCount } = appendChallengeEvents(this.controlDir, events, this.now);
177
+ if (eventCount !== undefined)
178
+ this.discloseChallengeLedgerSize(eventCount);
179
+ const fresh = assigned.filter((a) => !a.replayed);
180
+ if (fresh.length > 0) {
181
+ try {
182
+ enqueueMemoryAnnouncement(this.controlDir, {
183
+ kind: "gate",
184
+ at: this.now(),
185
+ 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.`),
186
+ });
187
+ }
188
+ catch (err) {
189
+ this.discloseAnnounceFailure("challenge enqueue", err);
190
+ }
191
+ }
192
+ return assigned;
193
+ }
194
+ challengeLedgerSizeDisclosed = false;
195
+ discloseChallengeLedgerSize(eventCount) {
196
+ if (this.challengeLedgerSizeDisclosed || eventCount <= CHALLENGE_LEDGER_MAX_EVENTS)
197
+ return;
198
+ this.challengeLedgerSizeDisclosed = true;
199
+ const sink = this.onIncident;
200
+ if (sink === undefined)
201
+ return;
202
+ try {
203
+ const err = new Error(`memory challenge ledger holds ${eventCount} events, past the calibrated bound of ${CHALLENGE_LEDGER_MAX_EVENTS}: ` +
204
+ `the whole file is rewritten on every challenge and parsed on every model-visible read (index rebuild, memory tools), ` +
205
+ `so this account now costs the session latency. It is NOT rotated — dropping events would un-withhold their entries — ` +
206
+ `so the deployment resolves the settled generations or archives the account deliberately.`);
207
+ err.code = "memory.challenge_ledger_oversize";
208
+ sink(err);
209
+ }
210
+ catch {
211
+ }
212
+ }
213
+ rebuildCorruptControlPlaneLedger(ledger, reason) {
214
+ if (typeof reason !== "string" || reason.trim() === "") {
215
+ const e = new Error("rebuildCorruptControlPlaneLedger: a non-empty reason is required (this call drops taint evidence — it is recorded, never implicit)");
216
+ e.code = "config.memory_rebuild_reason";
217
+ throw e;
218
+ }
219
+ if (!isStrictControlPlaneLedgerCorrupt(this.controlDir, ledger)) {
220
+ const e = new Error(`${ledger === "challenges" ? "memory challenge ledger" : "memory lineage ledger"} reads cleanly — refusing to rebuild it. This call exists to recover a ledger whose corruption already refuses every read; resetting a healthy one would drop taint evidence that is still doing its job.`);
221
+ e.code = "memory.control_plane_not_corrupt";
222
+ throw e;
223
+ }
224
+ enqueueMemoryAnnouncement(this.controlDir, {
225
+ kind: "gate",
226
+ at: this.now(),
227
+ items: [
228
+ ledger === "challenges"
229
+ ? `memory control plane: the challenge ledger was found CORRUPT and a deployment-initiated reset to empty was requested — entries that were withheld by an unresolved challenge may no longer be withheld. Treat facts recalled from memory as unverified until confirmed with the user.`
230
+ : `memory control plane: the harvest lineage ledger was found CORRUPT and a deployment-initiated reset to empty was requested — which session contributed which entry may no longer be recorded for anything written before this point.`,
231
+ ],
232
+ });
233
+ const receipt = rebuildStrictControlPlaneLedger(this.controlDir, ledger, this.now);
234
+ const sink = this.onIncident;
235
+ if (sink !== undefined) {
236
+ try {
237
+ const err = new Error(`memory control plane: ${ledger} ledger rebuilt after corruption (reason: ${reason}). ` +
238
+ `The corrupt bytes are quarantined at ${receipt.quarantinedTo.join(", ") || "(no file existed)"} — ` +
239
+ `they are the only remaining record of what this account held.`);
240
+ err.code = "memory.control_plane_rebuilt";
241
+ sink(err);
242
+ }
243
+ catch {
244
+ }
245
+ }
246
+ return receipt;
247
+ }
248
+ challengeEntries(ids, reason, requestId) {
249
+ if (typeof requestId !== "string" || requestId === "") {
250
+ const e = new Error("challengeEntries: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
251
+ e.code = "config.memory_challenge_request";
252
+ throw e;
253
+ }
254
+ return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
255
+ }
256
+ challengeSession(sessionId, reason, requestId) {
257
+ if (typeof requestId !== "string" || requestId === "") {
258
+ const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
259
+ e.code = "config.memory_challenge_request";
260
+ throw e;
261
+ }
262
+ const contributions = lineageContributionsOfSession(this.controlDir, sessionId);
263
+ return this.challengeAndAnnounce(contributions.map((c) => ({ eventId: `${requestId}:${c.entryId}`, entryId: c.entryId, reason, challengedRev: c.lastRev })));
264
+ }
265
+ resolveChallenge(entryId, generation, reason, requestId) {
266
+ const { resolved, eventCount } = resolveChallengeEvent(this.controlDir, entryId, generation, reason, this.now, requestId);
267
+ if (eventCount !== undefined)
268
+ this.discloseChallengeLedgerSize(eventCount);
269
+ return resolved;
270
+ }
271
+ adjudicatePendingLineage(txnId, action) {
272
+ const promoted = adjudicateLineagePending(this.controlDir, txnId, action, this.now);
273
+ this.settlePromotions(promoted);
274
+ return promoted;
275
+ }
276
+ listPendingLineage() {
277
+ return readLineageRecord(this.controlDir).pending;
278
+ }
279
+ readChallengeLedger() {
280
+ return readChallengeEvents(this.controlDir);
281
+ }
282
+ readChallengedHistoryAccount() {
283
+ try {
284
+ return readChallengedHistory(this.controlDir);
285
+ }
286
+ catch {
287
+ return {};
288
+ }
289
+ }
290
+ settlePromotions(promoted) {
291
+ if (promoted.length === 0)
292
+ return;
293
+ const events = [];
294
+ for (const p of promoted) {
295
+ let rec;
296
+ try {
297
+ rec = readSessionPollution(this.controlDir, p.sessionId);
298
+ }
299
+ catch {
300
+ rec = undefined;
301
+ }
302
+ if (rec === undefined)
303
+ rec = this.pollutedSessions.get(p.sessionId);
304
+ if (rec === undefined)
305
+ continue;
306
+ events.push({
307
+ eventId: `pollution:${p.sessionId}:${rec.at}:${p.entryId}`,
308
+ entryId: p.entryId,
309
+ reason: `challenged: the contributing session was marked polluted`,
310
+ challengedRev: p.rev,
311
+ });
312
+ }
313
+ this.challengeAndAnnounce(events);
314
+ try {
315
+ appendLineageAudit(this.controlDir, promoted.map((p) => ({ entryId: p.entryId, sessionId: p.sessionId, rev: p.rev, at: this.now() })));
316
+ }
317
+ catch {
318
+ }
319
+ }
139
320
  async materialize(scopes, writeScope) {
140
321
  ensureDirExists(this.memoryDir);
141
322
  ensureDirExists(this.controlDir);
@@ -316,7 +497,8 @@ export class MemoryEngine {
316
497
  return report;
317
498
  }
318
499
  async harvestCore(handle, opts) {
319
- const pollutedReason = opts?.polluted?.reason;
500
+ let pollutedReason = opts?.polluted?.reason ?? (opts?.sessionId !== undefined ? this.sessionPollution(opts.sessionId)?.reason : undefined);
501
+ const lineageSessionId = opts?.sessionId;
320
502
  const startedAt = this.now();
321
503
  const report = {
322
504
  ok: true,
@@ -344,6 +526,30 @@ export class MemoryEngine {
344
526
  report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${err instanceof Error ? err.message : String(err)}` };
345
527
  return report;
346
528
  }
529
+ try {
530
+ const rec = reconcileLineage(this.controlDir, this.now);
531
+ this.settlePromotions(rec.promoted);
532
+ for (const u of rec.undecidable) {
533
+ 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)`);
534
+ }
535
+ if (pollutedReason !== undefined && lineageSessionId !== undefined) {
536
+ const rec2 = this.sessionPollution(lineageSessionId);
537
+ if (rec2 !== undefined) {
538
+ const contributions = lineageContributionsOfSession(this.controlDir, lineageSessionId);
539
+ this.challengeAndAnnounce(contributions.map((c) => ({
540
+ eventId: `pollution:${lineageSessionId}:${rec2.at}:${c.entryId}`,
541
+ entryId: c.entryId,
542
+ reason: `challenged: the contributing session was marked polluted`,
543
+ challengedRev: c.lastRev,
544
+ })));
545
+ }
546
+ }
547
+ }
548
+ catch (err) {
549
+ report.ok = false;
550
+ report.incident = { kind: "sidecar_corrupt", detail: `memory lineage/challenge ledger refused: ${err instanceof Error ? err.message : String(err)}` };
551
+ return report;
552
+ }
347
553
  if (!existsSync(handle.writableRoot)) {
348
554
  report.ok = false;
349
555
  report.incident = { kind: "dir_missing", detail: `memory writable root is gone: ${handle.writableRoot}` };
@@ -693,13 +899,99 @@ export class MemoryEngine {
693
899
  patchReport = { applied: [], conflicts: [] };
694
900
  }
695
901
  else {
696
- try {
697
- patchReport = await this.backend.applyPatches(patches);
902
+ const txnId = uuidv7();
903
+ const pendingRows = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev }));
904
+ const lineageArmed = lineageSessionId !== undefined && pendingRows.length > 0;
905
+ if (lineageArmed) {
906
+ try {
907
+ stageLineagePending(this.controlDir, txnId, lineageSessionId, pendingRows, this.now);
908
+ }
909
+ catch (err) {
910
+ report.ok = false;
911
+ report.incident = { kind: "sidecar_corrupt", detail: `memory lineage write-ahead refused: ${err instanceof Error ? err.message : String(err)}` };
912
+ return report;
913
+ }
698
914
  }
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;
915
+ const preCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
916
+ if (preCommit !== undefined) {
917
+ if (lineageArmed) {
918
+ try {
919
+ discardLineagePending(this.controlDir, txnId);
920
+ }
921
+ catch {
922
+ }
923
+ }
924
+ pollutedReason = preCommit.reason;
925
+ for (const f of records)
926
+ await containPollutedRecord(f);
927
+ patchReport = { applied: [], conflicts: [] };
928
+ }
929
+ else {
930
+ try {
931
+ patchReport = await this.backend.applyPatches(patches);
932
+ }
933
+ catch (err) {
934
+ if (lineageArmed) {
935
+ try {
936
+ discardLineagePending(this.controlDir, txnId);
937
+ }
938
+ catch {
939
+ }
940
+ }
941
+ report.ok = false;
942
+ report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${err instanceof Error ? err.message : String(err)}` };
943
+ return report;
944
+ }
945
+ if (lineageArmed) {
946
+ try {
947
+ recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
948
+ const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
949
+ this.settlePromotions(promoted);
950
+ }
951
+ catch (err) {
952
+ 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)}`;
953
+ report.warnings.push(detail);
954
+ const sink = this.onIncident;
955
+ if (sink !== undefined) {
956
+ try {
957
+ const e = new Error(detail);
958
+ e.code = "memory.lineage_settle_failed";
959
+ sink(e);
960
+ }
961
+ catch {
962
+ }
963
+ }
964
+ }
965
+ }
966
+ const deletedIds = patchReport.applied.filter((a) => a.op === "delete").map((a) => a.id);
967
+ if (deletedIds.length > 0) {
968
+ try {
969
+ clearLineageForEntries(this.controlDir, deletedIds);
970
+ }
971
+ catch {
972
+ }
973
+ }
974
+ try {
975
+ recordChallengedHistory(this.controlDir, patchReport.applied
976
+ .filter((a) => (a.op === "update" || a.op === "delete") && this.retrievedThisSession.has(a.id))
977
+ .map((a) => ({ entryId: a.id, op: a.op === "delete" ? "tombstone" : "update" })), this.now);
978
+ }
979
+ catch {
980
+ }
981
+ const postCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
982
+ if (postCommit !== undefined) {
983
+ const appliedNonDelete = patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id);
984
+ try {
985
+ this.challengeAndAnnounce(appliedNonDelete.map((id) => ({
986
+ eventId: `pollution:${lineageSessionId}:${postCommit.at}:${id}`,
987
+ entryId: id,
988
+ reason: `challenged: the contributing session was marked polluted`,
989
+ })));
990
+ }
991
+ catch (err) {
992
+ report.warnings.push(`memory challenge of this harvest's commits failed (session polluted post-commit): ${err instanceof Error ? err.message : String(err)}`);
993
+ }
994
+ }
703
995
  }
704
996
  }
705
997
  const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
@@ -798,18 +1090,39 @@ export class MemoryEngine {
798
1090
  }
799
1091
  rebuildIndex(handle, headers, opts, warnings) {
800
1092
  const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
1093
+ let excludedIds;
1094
+ try {
1095
+ excludedIds = this.readChallengeExclusions();
1096
+ }
1097
+ catch (err) {
1098
+ const detail = `memory index withheld: the challenge/lineage ledger cannot be trusted (${err instanceof Error ? err.message : String(err)}) — fail-closed, nothing injected`;
1099
+ warnings?.push(detail);
1100
+ handle.indexOnDiskUntrusted = true;
1101
+ const sink = this.onIncident;
1102
+ if (sink !== undefined) {
1103
+ try {
1104
+ const e = new Error(detail);
1105
+ e.code = "memory.challenge_ledger_corrupt";
1106
+ sink(e);
1107
+ }
1108
+ catch {
1109
+ }
1110
+ }
1111
+ return "";
1112
+ }
801
1113
  const existing = opts.ignoreOnDisk === true ? "" : (readNoFollowSafe(indexPath) ?? "");
802
1114
  const indexRevs = readIndexRevs(this.controlDir);
803
1115
  const nextIndexRevs = {};
804
1116
  const expected = new Map();
1117
+ const excludedTargets = new Set();
805
1118
  for (const h of headers) {
806
1119
  const repoTarget = handle.repoIndexTargets?.get(h.id);
807
- if (repoTarget !== undefined) {
808
- expected.set(repoTarget, h);
1120
+ const target = repoTarget ?? relative(handle.writableRoot, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
1121
+ if (excludedIds.has(h.id)) {
1122
+ excludedTargets.add(target);
809
1123
  continue;
810
1124
  }
811
- const file = join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`);
812
- expected.set(relative(handle.writableRoot, canonicalize(file)), h);
1125
+ expected.set(target, h);
813
1126
  }
814
1127
  const covered = new Set();
815
1128
  const keptByTarget = new Map();
@@ -821,6 +1134,10 @@ export class MemoryEngine {
821
1134
  keptLines.push(line);
822
1135
  continue;
823
1136
  }
1137
+ if (excludedTargets.has(target)) {
1138
+ warnings?.push(`MEMORY.md index line removed: ${target} is withheld (challenged or unsettled) — its content stays off the injected index`);
1139
+ continue;
1140
+ }
824
1141
  if (covered.has(target)) {
825
1142
  if (line === keptByTarget.get(target))
826
1143
  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, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, 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, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } 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";