@sema-agent/core 5.65.0 → 6.0.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 (159) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -474,6 +474,36 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
474
474
  private enqueueExternalItems;
475
475
  /** One-shot dedup for adopting-face notices (see {@link adoptionNoticeKeys}). */
476
476
  private announceAdoptionNotice;
477
+ /**
478
+ * §3.4 — the standing unbound-row disclosure, in ONE spelling for BOTH faces that carry it (the
479
+ * harvest preflight `checkControlPlane` and the restricted sweep `auditRestrictedDivergence` used
480
+ * to hold drifting hand-built copies of this sentence; the sweep's copy had lost the remediation
481
+ * clause, which is if anything MORE apt there — a restricted session has no write channel of its
482
+ * own to converge the row).
483
+ *
484
+ * De-duplicated against what is ALREADY QUEUED, not against a per-instance latch. The repeat this
485
+ * closes was not merely noise: the queue is bounded ({@link MEMORY_ANNOUNCEMENTS_MAX}), so a
486
+ * long-lived instance with one stuck row folded REAL notices out of it to make room for copies of
487
+ * this one. But the latch the sibling adoption notices use ({@link announceAdoptionNotice}) is the
488
+ * wrong instrument HERE, twice over, and both were found in review:
489
+ * · a backend instance is a RUNNER-level dependency reused across every session it serves, so a
490
+ * per-instance latch would announce the standing condition to the first session and then hold
491
+ * its peace forever — turning a flood into silence, against this disclosure's own contract
492
+ * (「the STATE does not go silent」). The queue is DRAINED at each session's first injection, so
493
+ * "is an identical line still pending?" is exactly the right question: no repeat within a
494
+ * session, one fresh statement in every session that has not yet been told.
495
+ * · a latch key would have to encode the id set, and ledger row ids are arbitrary external
496
+ * strings (a comma is legal) — `join(",")` makes `["a,b","c"]` and `["a","b,c"]` the same key,
497
+ * so a real change of state could be suppressed. Comparing the RENDERED LINE has no such seam:
498
+ * two states that render the same sentence are, to every consumer, the same statement.
499
+ *
500
+ * The test runs INSIDE the queue's own locked read-modify-write
501
+ * ({@link enqueueMemoryAnnouncement}'s `supersedesPrefix`), not as a peek followed by an append: a
502
+ * peek-then-enqueue would race a concurrent drain, and two processes could both peek-miss and both
503
+ * append. Fail-open like every announce site — an unwritable queue never fails an operation that
504
+ * had something to say.
505
+ */
506
+ private announceUnboundRows;
477
507
  /** Resolve a binding's absolute projection path WITHOUT registering anything (probes must stay
478
508
  * side-effect-free; `scopeDir` would register an unknown scope name from a hand-edited row). */
479
509
  private bindingAbsPath;
@@ -179,6 +179,7 @@ function deleteRow(rows, id) {
179
179
  function hasUnboundRows(rows) {
180
180
  return rowsEntries(rows).some(([id]) => rowsBinding(rows, id) === undefined);
181
181
  }
182
+ const UNBOUND_DISCLOSURE_PREFIX = "memory ledger has ";
182
183
  function unboundRowIds(rows) {
183
184
  return rowsEntries(rows)
184
185
  .filter(([id]) => rowsBinding(rows, id) === undefined)
@@ -570,13 +571,8 @@ export class FileMemoryEngineBackend {
570
571
  registeredScopes(this.controlPlaneRoot);
571
572
  if (this.persistedSchemaVersion === "v1" || hasUnboundRows(this.loadLedger()))
572
573
  this.tryOpportunisticMigrate();
573
- const rows = this.loadLedger();
574
- if (this.persistedSchemaVersion === "v2" && hasUnboundRows(rows)) {
575
- const ids = unboundRowIds(rows);
576
- this.enqueueExternalItems([
577
- `memory ledger has ${ids.length} unbound row(s) (no derivable committed projection): ${ids.slice(0, 8).join(", ")}${ids.length > 8 ? ", …" : ""} — v2 binding guarantees do not cover them until they converge (duplicate projections need an unrestricted harvest or manual removal)`,
578
- ]);
579
- }
574
+ if (this.persistedSchemaVersion === "v2")
575
+ this.announceUnboundRows(this.loadLedger());
580
576
  }
581
577
  drainInboundFindings() {
582
578
  const out = this.inboundFindings;
@@ -642,6 +638,15 @@ export class FileMemoryEngineBackend {
642
638
  this.adoptionNoticeKeys.add(key);
643
639
  this.enqueueExternalItems([item]);
644
640
  }
641
+ announceUnboundRows(rows) {
642
+ const ids = unboundRowIds(rows);
643
+ const item = ids.length === 0 ? undefined : `${UNBOUND_DISCLOSURE_PREFIX}${ids.length} unbound row(s) (no derivable committed projection): ${ids.slice(0, 8).join(", ")}${ids.length > 8 ? ", …" : ""} — v2 binding guarantees do not cover them until they converge (duplicate projections need an unrestricted harvest or manual removal)`;
644
+ try {
645
+ enqueueMemoryAnnouncement(this.controlPlaneRoot, { kind: "external", at: this.now(), items: item === undefined ? [] : [item] }, { supersedesPrefix: UNBOUND_DISCLOSURE_PREFIX });
646
+ }
647
+ catch {
648
+ }
649
+ }
645
650
  bindingAbsPath(binding) {
646
651
  return join(scopeDirFor(this.directoryRoot, this.controlPlaneRoot, binding.scope), `${binding.slug}.md`);
647
652
  }
@@ -2033,12 +2038,8 @@ export class FileMemoryEngineBackend {
2033
2038
  if (this.persistedSchemaVersion === "v1" || hasUnboundRows(this.loadLedger()))
2034
2039
  this.tryOpportunisticMigrate();
2035
2040
  const rows = { ...this.loadLedger() };
2036
- if (this.persistedSchemaVersion === "v2" && hasUnboundRows(rows)) {
2037
- const unboundIds = unboundRowIds(rows);
2038
- this.enqueueExternalItems([
2039
- `memory ledger has ${unboundIds.length} unbound row(s) (no derivable committed projection): ${unboundIds.slice(0, 8).join(", ")}${unboundIds.length > 8 ? ", …" : ""} — v2 binding guarantees do not cover them until they converge`,
2040
- ]);
2041
- }
2041
+ if (this.persistedSchemaVersion === "v2")
2042
+ this.announceUnboundRows(rows);
2042
2043
  const present = new Map();
2043
2044
  let complete = true;
2044
2045
  for (const scope of Object.keys(registeredScopes(this.controlPlaneRoot))) {
@@ -56,6 +56,27 @@ export declare function extraOriginCarriers(extra: readonly string[] | undefined
56
56
  * so a committed multi-carrier shape always answers the one agreed value.
57
57
  */
58
58
  export declare function committedOriginOf(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): MemoryEntryOrigin | undefined;
59
+ /**
60
+ * design/383 §1.6 — the CONSOLIDATION exposure predicate: is this entry's content untrusted for
61
+ * the purpose of the distillation fold? TWO axes, not one:
62
+ * - a committed external-origin marker ({@link committedOriginOf}) — the design/336 axis;
63
+ * - `trust: "untrusted"` — the design-A C-5 repo-ingest marker, which an ingested repo file can
64
+ * carry with NO origin marker at all. Such an entry is untrusted content by the same law that
65
+ * makes it un-whitewashable (the single-value marker has no `"trusted"` spelling to escalate
66
+ * to), and the origin-only predicate sorted it into the CLEAN arm — its bytes then authored
67
+ * clean products, and those products committed unmarked. That is exactly the marked-ingress
68
+ * laundering window the exposure partition exists to close.
69
+ *
70
+ * The direction is deliberately conservative (over-marking): an entry judged marked here is
71
+ * withheld from the clean arm and its products carry `external/derived`. Nothing here proves an
72
+ * unmarked entry clean — absence of both markers means "never judged exposed", as always.
73
+ *
74
+ * This is the consolidation lane's judgment only. The READ-face treatment (opaque handles, band
75
+ * ordering, the `MemoryEntryHeader.exposure` backend fact) stays on the origin axis alone: those
76
+ * faces answer "does the store carry an origin marker", a data question a third-party backend
77
+ * already answers, and widening them is a backend-contract change this predicate does not make.
78
+ */
79
+ export declare function consolidationExposedFrontmatter(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra" | "trust">): boolean;
59
80
  /** Deep equality over the origin marker (every member, absence included) — the immutability law's
60
81
  * comparator: a downgraded cause or a rewritten `at` is as refused as a strip. */
61
82
  export declare function originEquals(a: MemoryEntryOrigin | undefined, b: MemoryEntryOrigin | undefined): boolean;
@@ -106,7 +127,7 @@ export declare function distilledEquals(a: MemoryEntryDistilled | undefined, b:
106
127
  * design/339 §1.4.2-1 — the harvest-side STRIP of model-written distilled bytes (typed field AND
107
128
  * distilled-form `extra` lines): the block is minted by the consolidation plan freeze EXCLUSIVELY —
108
129
  * whatever a file on the model-writable plane says about its own lineage/supersession is not
109
- * evidence (a model-writable edge would be the delete-attack channel add-only exists to close).
130
+ * evidence (a model-writable edge would be the covert-delete channel add-only exists to close).
110
131
  * Same seat and same disclosure discipline as {@link stripModelWrittenOrigin}. MUTATES the given
111
132
  * (already-copied) frontmatter; returns true ⇔ anything was stripped.
112
133
  */
@@ -326,6 +326,9 @@ export function committedOriginOf(fm) {
326
326
  return { taint: fm.origin.taint, ...(fm.origin.cause !== undefined ? { cause: fm.origin.cause } : {}), at: fm.origin.at };
327
327
  return extraOriginCarriers(fm.extra)[0];
328
328
  }
329
+ export function consolidationExposedFrontmatter(fm) {
330
+ return committedOriginOf(fm) !== undefined || fm.trust === "untrusted";
331
+ }
329
332
  export function originEquals(a, b) {
330
333
  if (a === undefined || b === undefined)
331
334
  return a === b;
@@ -37,6 +37,11 @@ export interface V2HeaderHints {
37
37
  * privilege AND walks through the exposure hard gate with it. ONE exported predicate on purpose:
38
38
  * the write side (harvest hard gate) and any read-side consumer must judge through the same
39
39
  * function, never through re-implementations that can drift.
40
+ *
41
+ * NAME COLLISION, not concept parity (test B12): `pinned` here is a
42
+ * WRITE-SIDE safety-privilege declaration (instruction-form ⇒ exposure hard gate / quarantine),
43
+ * while Claude Code's `pinned` is a RECALL-side display priority (up to 4 entries surfaced in a
44
+ * "# Pinned memories" block). Same word, different machines — do not read one as the other.
40
45
  */
41
46
  export declare function isInstructionEntry(fm: {
42
47
  type?: string;
@@ -1,15 +1,16 @@
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, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, } 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";
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, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, } from "./engine.js";
2
+ export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, type MemoryGetDetails, type MemoryIndexDetails, type MemoryIndexRow, type CleanMemoryIndexRow, type ExposedMemoryIndexRow, } from "./tools.js";
3
3
  export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
4
4
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
5
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";
6
6
  export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, type MemoryExportBundle, type MemoryImportReport, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, } from "./export-bundle.js";
7
- export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
7
+ export { 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, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, type SessionCaptureOptOutRecord, type SessionCaptureOptOutMarkOutcome, type SessionCaptureRecordStore, } from "./layout.js";
8
8
  export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
10
+ export { consolidationExposedFrontmatter } from "./frontmatter.js";
10
11
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
11
12
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
12
- export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
13
+ export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
13
14
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
14
15
  export type { OriginClearanceRow, OriginClearanceEvent, OriginClearanceShadow } from "./origin-clearance.js";
15
16
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
@@ -1,15 +1,16 @@
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, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, } from "./engine.js";
2
- export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } 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, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, } from "./engine.js";
2
+ export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
3
3
  export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
4
4
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
5
5
  export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, erasureSelectHash, } from "./file-backend.js";
6
6
  export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
7
- export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
7
+ export { 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, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, } from "./layout.js";
8
8
  export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation } from "./frontmatter.js";
10
+ export { consolidationExposedFrontmatter } from "./frontmatter.js";
10
11
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
11
12
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, CONSOLIDATION_RUN_STOP_REASONS, } from "./consolidation.js";
12
- export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
13
+ export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
13
14
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, } from "./consolidation-driver.js";
14
15
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
15
16
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
@@ -268,8 +268,31 @@ interface AnnouncementsRecord {
268
268
  folded: number;
269
269
  queue: MemoryAnnouncement[];
270
270
  }
271
- /** S2-B — enqueue one announcement (journaled + locked; bounded with overflow folding). */
272
- export declare function enqueueMemoryAnnouncement(controlDir: string, ann: MemoryAnnouncement): void;
271
+ /**
272
+ * S2-B — enqueue one announcement (journaled + locked; bounded with overflow folding).
273
+ *
274
+ * `opts.supersedesPrefix` (#495) declares this announcement to be a STANDING CONDITION — one whose
275
+ * sentence is re-derived at every entry point until the condition clears — and names the family it
276
+ * belongs to by line prefix. Every queued line of that family is a STALE READING of the same
277
+ * condition, so the queue is made to hold exactly one of them, always the current one:
278
+ * · the identical line already queued unread ⇒ no-op, and no write at all;
279
+ * · otherwise every queued item with that prefix is dropped and this one appended.
280
+ * Merely SKIPPING a repeat is not enough, and that was the first form of this: a condition that goes
281
+ * A → B → A inside one unread window would leave `A, B` in the queue and a reader looking at B as the
282
+ * latest state — the fix for a flood turning into a wrong answer. Replacement makes the last word the
283
+ * true one.
284
+ *
285
+ * All of it runs INSIDE the locked read-modify-write, which is the point: a caller that peeked first
286
+ * and enqueued after would race a concurrent drain, and two processes could both peek-miss.
287
+ * Deliberately keyed on the LINE, not on a structural id — two states that render the same sentence
288
+ * are, to every consumer, the same statement. Deliberately opt-in: an ordinary event repeating IS
289
+ * news, and only its author knows which kind it is. Callers using it pass ONE item (a standing
290
+ * condition is one sentence), and the prefix must name the family precisely — anything else sharing
291
+ * it would be dropped as a stale sibling.
292
+ */
293
+ export declare function enqueueMemoryAnnouncement(controlDir: string, ann: MemoryAnnouncement, opts?: {
294
+ supersedesPrefix?: string;
295
+ }): void;
273
296
  /** S2-B — drain the queue (session-first inject, 时机①). Returns what was queued and resets it. */
274
297
  export declare function drainMemoryAnnouncements(controlDir: string): AnnouncementsRecord;
275
298
  /** Test/observability peek (journal-aware, non-destructive). */
@@ -330,6 +353,69 @@ export declare function listSessionPollution(controlDir: string): Record<string,
330
353
  * write failure THROWS (the import aborts with its latch standing — §1-2⑤).
331
354
  */
332
355
  export declare function importSessionPollution(controlDir: string, sessionId: string, record: SessionPollutionRecord): "written" | "already" | "divergent";
356
+ /** Directory (under the control plane) holding one record file per capture-opted-out session. */
357
+ export declare const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
358
+ /** The durable capture opt-out record. `reason` is host/engine-authored (the declaring channel),
359
+ * never model text. */
360
+ export interface SessionCaptureOptOutRecord {
361
+ at: number;
362
+ reason: string;
363
+ }
364
+ /** Outcome vocabulary shared with the pollution marker (same `wx` one-way form): `"created"` ⇔ THIS
365
+ * call minted the record; `"existed"` ⇔ already recorded (a resumed session / an idempotent
366
+ * re-declaration); `"unpersisted"` ⇔ the record could not be written — the CALLER must refuse to
367
+ * run (design/383 §2.8: an opt-out that only holds in-process is broken by the first resume, so
368
+ * "could not persist" is a named refusal, never a silent in-process-only degrade — the deliberate
369
+ * divergence from the pollution marker's best-effort arm). */
370
+ export type SessionCaptureOptOutMarkOutcome = "created" | "existed" | "unpersisted";
371
+ /** Mark `sessionId` capture-opted-out (idempotent; the FIRST record wins — `wx` refuses overwrite). */
372
+ export declare function markSessionCaptureOptOut(controlDir: string, sessionId: string, reason: string, now: () => number): SessionCaptureOptOutMarkOutcome;
373
+ /** Read the durable capture opt-out record for `sessionId` (undefined = capture is on). Fail-closed
374
+ * exactly like the pollution read: a record that EXISTS but cannot be read or parsed still answers
375
+ * opted-out (a synthesized record stands in) — corruption must not launder a privacy request back
376
+ * into capture. Only ABSENCE reads as "capture on". A store state where existence itself cannot be
377
+ * established (dir-level IO fault) THROWS — the engine's `sessionCaptureOptOutOrFault` catch turns
378
+ * it into the INDETERMINATE fault axis (same fail-closed suppression, honest attribution — see the
379
+ * catch below). */
380
+ export declare function readSessionCaptureOptOut(controlDir: string, sessionId: string): SessionCaptureOptOutRecord | undefined;
381
+ /**
382
+ * Enumerate EVERY durable capture opt-out record (the consolidation eligibility arm's roster read —
383
+ * design/383 §2.4). Same fail-closed posture as {@link listSessionPollution}: a record that exists
384
+ * but cannot be read answers synthesized (never silently omitted); an absent directory is the
385
+ * genuine empty state (ENOENT only); any OTHER enumeration failure THROWS — a listing that silently
386
+ * dropped records would let the distiller amplify content its author asked to withhold.
387
+ */
388
+ export declare function listSessionCaptureOptOut(controlDir: string): Record<string, SessionCaptureOptOutRecord>;
389
+ /**
390
+ * design/383 §2.1b — the control-plane carrier a HOST may substitute for the capture opt-out
391
+ * record's read/write/list (server F1: on a stateless multi-replica deployment the file form is
392
+ * invisible to the resume replica — "record not found" would break exactly toward capture). The
393
+ * core default is the file trio above, byte-identical for every deployment that supplies nothing.
394
+ * CONTRACT (each leg mirrors the file form's law):
395
+ * - `mark` is create-only/first-record-wins and must answer `"unpersisted"` (never throw) when the
396
+ * carrier cannot durably land the record;
397
+ * - `read` is fail-closed: a record that exists but cannot be decoded answers a synthesized
398
+ * record, never `undefined`; a store that cannot establish whether a record EXISTS (an outage)
399
+ * may THROW — the engine treats a throwing read as the INDETERMINATE fault axis (commit
400
+ * boundaries suppress fail-closed), which is the honest answer there: it must not fabricate a
401
+ * record ("a user request that never happened") and must not answer `undefined` ("no record"
402
+ * breaks exactly toward capture);
403
+ * - `list` throws on an enumeration failure (an unreadable roster is not an empty one).
404
+ * REGISTERED COMPANION (named, not silently absent): the session-POLLUTION marker has the same
405
+ * cloud-form invisibility (server F1 names both). It is NOT routed through this seat in this
406
+ * slice because the delegation-settlement lane's consumers are contractually PURE-DATA
407
+ * (`controlDir` + `sessionId` coordinates that outlive any live object — see the settlement handle
408
+ * doc in prepare-task.ts), and a live store object cannot ride that lane; its migration needs its
409
+ * own seam design and is registered here rather than half-built.
410
+ */
411
+ export interface SessionCaptureRecordStore {
412
+ mark(sessionId: string, record: SessionCaptureOptOutRecord): SessionCaptureOptOutMarkOutcome;
413
+ read(sessionId: string): SessionCaptureOptOutRecord | undefined;
414
+ list(): Record<string, SessionCaptureOptOutRecord>;
415
+ }
416
+ /** The core-default {@link SessionCaptureRecordStore}: the file trio over `controlDir` (single-
417
+ * process / single-host deployments keep today's carrier byte-identical). */
418
+ export declare function fileSessionCaptureRecordStore(controlDir: string): SessionCaptureRecordStore;
333
419
  /** The retrieved-account sidecar (control plane): entry id → { count, lastAt }. */
334
420
  export declare const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
335
421
  /** Bound on tracked ids — over the cap, the entries with the OLDEST `lastAt` are evicted first
@@ -668,17 +668,34 @@ function coerceAnnouncements(raw) {
668
668
  typeof a.at === "number" &&
669
669
  Array.isArray(a.items) &&
670
670
  a.items.every((i) => typeof i === "string")) {
671
- rec.queue.push({ kind: a.kind, at: a.at, ...(typeof a.sessionId === "string" ? { sessionId: a.sessionId } : {}), items: a.items });
671
+ rec.queue.push({
672
+ kind: a.kind,
673
+ at: a.at,
674
+ ...(typeof a.sessionId === "string" ? { sessionId: a.sessionId } : {}),
675
+ ...(typeof a.reportId === "string" ? { reportId: a.reportId } : {}),
676
+ items: a.items,
677
+ });
672
678
  }
673
679
  }
674
680
  }
675
681
  return rec;
676
682
  }
677
- export function enqueueMemoryAnnouncement(controlDir, ann) {
678
- if (ann.items.length === 0)
683
+ export function enqueueMemoryAnnouncement(controlDir, ann, opts) {
684
+ const family = opts?.supersedesPrefix;
685
+ if (ann.items.length === 0 && family === undefined)
679
686
  return;
680
687
  lockedJournaledUpdate(controlDir, ANNOUNCEMENTS_FILE, (current) => {
681
688
  const rec = coerceAnnouncements(current);
689
+ if (family !== undefined) {
690
+ const queued = rec.queue.flatMap((a) => a.items.filter((i) => i.startsWith(family)));
691
+ if (queued.length === ann.items.length && ann.items.every((i) => queued.includes(i)))
692
+ return undefined;
693
+ for (const a of rec.queue)
694
+ a.items = a.items.filter((i) => !i.startsWith(family));
695
+ rec.queue = rec.queue.filter((a) => a.items.length > 0);
696
+ if (ann.items.length === 0)
697
+ return rec;
698
+ }
682
699
  rec.queue.push(ann);
683
700
  if (rec.queue.length > MEMORY_ANNOUNCEMENTS_MAX) {
684
701
  rec.folded += rec.queue.length - MEMORY_ANNOUNCEMENTS_MAX;
@@ -831,6 +848,98 @@ export function importSessionPollution(controlDir, sessionId, record) {
831
848
  const standing = readSessionPollution(controlDir, sessionId);
832
849
  return standing !== undefined && standing.at === record.at && standing.reason === record.reason ? "already" : "divergent";
833
850
  }
851
+ export const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
852
+ function captureOptOutPath(controlDir, sessionId) {
853
+ return join(controlDir, SESSION_CAPTURE_OPTOUT_DIR, `${encodeURIComponent(sessionId)}.json`);
854
+ }
855
+ export function markSessionCaptureOptOut(controlDir, sessionId, reason, now) {
856
+ const path = captureOptOutPath(controlDir, sessionId);
857
+ try {
858
+ ensureDirExists(dirname(path));
859
+ const record = { at: now(), reason };
860
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
861
+ return "created";
862
+ }
863
+ catch (err) {
864
+ if (err instanceof Error && "code" in err && err.code === "EEXIST")
865
+ return "existed";
866
+ return "unpersisted";
867
+ }
868
+ }
869
+ export function readSessionCaptureOptOut(controlDir, sessionId) {
870
+ const path = captureOptOutPath(controlDir, sessionId);
871
+ let raw;
872
+ try {
873
+ raw = readFileSync(path, "utf8");
874
+ }
875
+ catch (err) {
876
+ if (err.code === "ENOENT") {
877
+ try {
878
+ lstatSync(path);
879
+ }
880
+ catch (lerr) {
881
+ if (lerr.code === "ENOENT")
882
+ return undefined;
883
+ throw new ControlPlaneCorruptError(`session capture opt-out record state is INDETERMINATE (${lerr.code ?? "io error"}) at ${path} — the store cannot establish whether a record exists; consumers must suppress on the fault axis (an outage is never reported as a user request)`, { cause: lerr });
884
+ }
885
+ return { at: 0, reason: "capture opt-out record present but unreadable (kept fail-closed)" };
886
+ }
887
+ try {
888
+ statSync(path);
889
+ }
890
+ catch {
891
+ throw new ControlPlaneCorruptError(`session capture opt-out record state is INDETERMINATE (${err.code ?? "io error"}) at ${path} — the store cannot establish whether a record exists; consumers must suppress on the fault axis (an outage is never reported as a user request)`, { cause: err });
892
+ }
893
+ return { at: 0, reason: "capture opt-out record present but unreadable (kept fail-closed)" };
894
+ }
895
+ try {
896
+ const parsed = JSON.parse(raw);
897
+ if (parsed !== null && typeof parsed === "object") {
898
+ const at = parsed.at;
899
+ const reason = parsed.reason;
900
+ if (typeof at === "number" && typeof reason === "string")
901
+ return { at, reason };
902
+ }
903
+ }
904
+ catch {
905
+ }
906
+ return { at: 0, reason: "capture opt-out record present but unreadable (kept fail-closed)" };
907
+ }
908
+ export function listSessionCaptureOptOut(controlDir) {
909
+ const dir = join(controlDir, SESSION_CAPTURE_OPTOUT_DIR);
910
+ let names;
911
+ try {
912
+ names = readdirSync(dir);
913
+ }
914
+ catch (err) {
915
+ if (err.code === "ENOENT")
916
+ return {};
917
+ throw new ControlPlaneCorruptError(`session capture opt-out records could not be enumerated (${err.code ?? "io error"}) at ${dir} — an enumeration failure is not an empty set (fail-closed)`, { cause: err });
918
+ }
919
+ const out = Object.create(null);
920
+ for (const name of names.sort()) {
921
+ if (!name.endsWith(".json"))
922
+ continue;
923
+ let sessionId;
924
+ try {
925
+ sessionId = decodeURIComponent(name.slice(0, -".json".length));
926
+ }
927
+ catch {
928
+ continue;
929
+ }
930
+ const rec = readSessionCaptureOptOut(controlDir, sessionId);
931
+ if (rec !== undefined)
932
+ out[sessionId] = rec;
933
+ }
934
+ return out;
935
+ }
936
+ export function fileSessionCaptureRecordStore(controlDir) {
937
+ return {
938
+ mark: (sessionId, record) => markSessionCaptureOptOut(controlDir, sessionId, record.reason, () => record.at),
939
+ read: (sessionId) => readSessionCaptureOptOut(controlDir, sessionId),
940
+ list: () => listSessionCaptureOptOut(controlDir),
941
+ };
942
+ }
834
943
  export const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
835
944
  export const USAGE_RETRIEVED_MAX_IDS = 4096;
836
945
  function coerceRetrievedAccount(raw) {
@@ -62,3 +62,10 @@ export declare const MEMORY_SEARCH_BAND_ORDER_HEADER = "entries without the [ext
62
62
  * memory_get instead of treating an id-only line as an empty result.
63
63
  */
64
64
  export declare const MEMORY_PROVENANCE_SEARCH_SENTENCE: string;
65
+ /**
66
+ * #493 件① — the memory_index counterpart of {@link MEMORY_PROVENANCE_SEARCH_SENTENCE} (appended to
67
+ * the listing tool's description under "carry" only): same teaching, "rows" instead of "hits" —
68
+ * the enumeration face renders a marked entry as the same opaque id-only handle the search face
69
+ * does (§5.2's passive-face law: a marked entry's model-authored strings never ride untaxed).
70
+ */
71
+ export declare const MEMORY_PROVENANCE_INDEX_SENTENCE: string;
@@ -14,3 +14,6 @@ export const MEMORY_SEARCH_BAND_ORDER_HEADER = `entries without the ${MEMORY_EXP
14
14
  export const MEMORY_PROVENANCE_SEARCH_SENTENCE = "Hits tagged [external-origin] list only the entry id (their content came from a session that was exposed to " +
15
15
  "external content): read them with memory_get by id as usual, then verify what they say against current sources " +
16
16
  "before acting on it.";
17
+ export const MEMORY_PROVENANCE_INDEX_SENTENCE = "Rows tagged [external-origin] list only the entry id (their content came from a session that was exposed to " +
18
+ "external content): read them with memory_get by id as usual, then verify what they say against current sources " +
19
+ "before acting on it.";
@@ -1,17 +1,22 @@
1
1
  /**
2
- * design/178 v1 — the two engine-memory retrieval tools (`memory_search` / `memory_get`).
2
+ * design/178 v1 — the engine-memory retrieval tools (`memory_search` / `memory_get`, and since
3
+ * #493 the `memory_index` enumeration face).
3
4
  *
4
5
  * These are the first runner-wired consumers of the `MemoryBackend.search()`/`getByIds()` contract:
5
- * a deterministic keyword lookup over the session's mounted scopes plus a paged full-content read.
6
- * Zero contract changes the tools sit entirely ON TOP of the backend seam.
6
+ * a deterministic keyword lookup over the session's mounted scopes plus a paged full-content read,
7
+ * plus (#493) a paged enumeration of EVERY mounted entry the completeness floor under both: any
8
+ * committed entry is reachable through `memory_index` regardless of the injected index's truncation
9
+ * window and of whether its words match the question (chaos live S2-B: out-of-window ∧ lexically
10
+ * disjoint was structurally unreachable). Zero contract changes — the tools sit entirely ON TOP of
11
+ * the backend seam.
7
12
  *
8
- * READ-ONLY BY STRUCTURE. Neither tool can write; the write path stays the file channel + harvest
13
+ * READ-ONLY BY STRUCTURE. No tool here can write; the write path stays the file channel + harvest
9
14
  * gate. Entry content returned to the model is DATA, not instructions: every body fragment rides an
10
15
  * untrusted fence (`delimitUntrusted`) and every header field a model could have authored
11
16
  * (name/description/slug/scope) is neutralized inline. Results are byte-bounded.
12
17
  *
13
- * THE PAIR IS ATOMIC. Each tool names the other in its description, so a half-mount would teach a
14
- * tool that is not there — the mount site (runner/prepare-task.ts) mounts both or neither.
18
+ * THE TRIO IS ATOMIC. Each tool names the others in its description, so a partial mount would teach
19
+ * a tool that is not there — the mount site (runner/prepare-task.ts) mounts all or none.
15
20
  *
16
21
  * RESULT ORDER IS THE BACKEND CONTRACT ORDER: ascending cosine-distance score with the
17
22
  * deterministic id tie-break, exactly what `MemoryBackend.search()` promises. Multi-plane sessions
@@ -30,11 +35,21 @@ import type { ToolSpec } from "../types.js";
30
35
  import type { MemoryBackend } from "./types.js";
31
36
  export declare const MEMORY_SEARCH_TOOL_NAME = "memory_search";
32
37
  export declare const MEMORY_GET_TOOL_NAME = "memory_get";
33
- /** The atomic pair, in mount order (same shape as SHARED_MEMORY_TOOL_NAMES). */
34
- export declare const MEMORY_ENGINE_TOOL_NAMES: readonly ["memory_search", "memory_get"];
38
+ /** #493 件① the enumeration face. NOT `memory_list`: that wire name (and the `core.memory_list@1`
39
+ * contract id) belongs to the design/177 shared-memory store pair, and both families can mount in
40
+ * one session. */
41
+ export declare const MEMORY_INDEX_TOOL_NAME = "memory_index";
42
+ /** The atomic trio, in mount order (same shape as SHARED_MEMORY_TOOL_NAMES). */
43
+ export declare const MEMORY_ENGINE_TOOL_NAMES: readonly ["memory_search", "memory_get", "memory_index"];
35
44
  /** Default / maximum hit counts for one search call. */
36
45
  export declare const MEMORY_SEARCH_DEFAULT_LIMIT = 8;
37
46
  export declare const MEMORY_SEARCH_MAX_LIMIT = 20;
47
+ /** Default / maximum rows of one memory_index page (a row is ≲600 bytes by the inline caps, so the
48
+ * extremes stay well under one tool-result's budget; the per-page byte bound below is the backstop). */
49
+ export declare const MEMORY_INDEX_PAGE_ROWS = 100;
50
+ export declare const MEMORY_INDEX_MAX_PAGE_ROWS = 500;
51
+ /** Byte bound of one memory_index page (rows past it defer to the next page, disclosed). */
52
+ export declare const MEMORY_INDEX_PAGE_CAP_BYTES: number;
38
53
  /** Per-hit body-fragment bound (code points, enforced by the fence itself). */
39
54
  export declare const MEMORY_SEARCH_SNIPPET_CAP = 600;
40
55
  /** Default / maximum lines of one memory_get page. */
@@ -78,6 +93,21 @@ export interface MemoryEnginePlane {
78
93
  * settlement account's record-failure arm.
79
94
  */
80
95
  onTaintedDelivery?: (ids: readonly string[]) => void;
96
+ /**
97
+ * design/383 §1.7 — the provenance mode of the WRITE-SIDE engine that owns this plane, read off
98
+ * the engine itself (`MemoryEngine.provenanceMode`), never re-declared by hand. Present ⇒ the
99
+ * factory ASSERTS it against its own {@link MemoryEngineToolsOptions.provenance}: a wiring that
100
+ * writes under `"carry"` while reading under `"off"` mints origin markers no read face will ever
101
+ * honour (marked entries served through the clean shape, no banner, no recall-taint propagation)
102
+ * — the two halves are one deployment posture and a split between them is a configuration fault,
103
+ * refused at construction (`config.memory_provenance`) rather than discovered as a quiet leak.
104
+ *
105
+ * Scope, stated honestly: this kills CONFIGURATION divergence only. It proves nothing about
106
+ * whether any individual entry is classified correctly — that is the 178 fail-closed default's
107
+ * job, not this assertion's. Absent ⇒ no engine mode is declared for this plane (engine-direct
108
+ * hosts and tests) and nothing is asserted: the factory's behaviour is byte-identical to before.
109
+ */
110
+ enginePlaneProvenance?: "off" | "carry";
81
111
  }
82
112
  export interface MemoryEngineToolsOptions {
83
113
  planes: ReadonlyArray<MemoryEnginePlane>;
@@ -191,6 +221,57 @@ export interface MemoryGetDetails {
191
221
  * supersession is disclosure/ordering, never withholding. */
192
222
  supersededBy?: string;
193
223
  }
224
+ /**
225
+ * #493 件① — one row of the memory_index enumeration. The same discriminated shape law as the
226
+ * search hits (design/336 B5): a clean row carries the text seats, a marked row (carry mode) is an
227
+ * opaque id-only handle — a consumer must branch on `exposure` to touch the text seats. No score
228
+ * seat: the enumeration is structural, not ranked.
229
+ */
230
+ export interface CleanMemoryIndexRow {
231
+ id: string;
232
+ scope: string;
233
+ slug: string;
234
+ name?: string;
235
+ description?: string;
236
+ mtimeMs: number;
237
+ sizeBytes: number;
238
+ exposure?: never;
239
+ /** Present ⇔ this row is SUPERSEDED (listed via `includeSuperseded`; the default face filters
240
+ * such rows out) — the value is the standing carrier entry's id. */
241
+ supersededBy?: string;
242
+ }
243
+ /** The opaque marked-row handle (carry mode) — same posture as {@link ExposedMemorySearchHit}. */
244
+ export interface ExposedMemoryIndexRow {
245
+ exposure: "external";
246
+ id: string;
247
+ scope: string;
248
+ mtimeMs: number;
249
+ sizeBytes: number;
250
+ slug?: never;
251
+ name?: never;
252
+ description?: never;
253
+ supersededBy?: string;
254
+ }
255
+ export type MemoryIndexRow = CleanMemoryIndexRow | ExposedMemoryIndexRow;
256
+ export interface MemoryIndexDetails {
257
+ outcome: "ok" | "refused" | "failed";
258
+ reason?: string;
259
+ entries?: MemoryIndexRow[];
260
+ /** Zero-based row offset this page started at (the enumeration order is deterministic). */
261
+ offset?: number;
262
+ /** Rows on THIS page. */
263
+ shown?: number;
264
+ /** Total rows the enumeration serves (after the exclusion/superseded filters). */
265
+ total?: number;
266
+ /** Superseded rows the default face hid (absent when zero or when `includeSuperseded` was passed). */
267
+ supersededHidden?: number;
268
+ /** Present ⇔ more rows follow — pass it as the next call's `offset`. */
269
+ nextOffset?: number;
270
+ /** The listing revision this page was served against (a stateless digest of the ordered id
271
+ * sequence the offsets address). Pass it back on continuation pages of a complete pass; a
272
+ * mismatch refuses with reason `listing_changed` instead of silently skipping shifted rows. */
273
+ listingRev?: string;
274
+ }
194
275
  /** Cut `text` to at most `maxBytes` UTF-8 bytes on a CODE POINT boundary (a byte-wise slice would
195
276
  * strand half a character), reporting how many bytes were dropped. Unchanged text reports 0.
196
277
  * Exported for the truncation-invariant registry — a bound-taking function has to be provable. */