@remnic/core 9.3.717 → 9.3.719

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 (250) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +9 -9
  3. package/dist/access-cli.js +23 -23
  4. package/dist/access-http.d.ts +5 -5
  5. package/dist/access-http.js +13 -13
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +12 -12
  8. package/dist/access-operations-batch.js +10 -10
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +11 -11
  11. package/dist/{access-service-d4Tk-Dlr.d.ts → access-service-CZbnt0t2.d.ts} +3 -3
  12. package/dist/access-service.d.ts +5 -5
  13. package/dist/access-service.js +8 -8
  14. package/dist/access-surface-catalog.d.ts +5 -5
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-recall.d.ts +1 -1
  18. package/dist/active-recall.js +1 -1
  19. package/dist/behavior-learner.d.ts +1 -1
  20. package/dist/behavior-signals.d.ts +1 -1
  21. package/dist/bootstrap.d.ts +4 -4
  22. package/dist/briefing.d.ts +3 -1
  23. package/dist/briefing.js +3 -3
  24. package/dist/buffer-surprise-report.d.ts +1 -1
  25. package/dist/buffer.d.ts +1 -1
  26. package/dist/calibration.d.ts +1 -1
  27. package/dist/capabilities.d.ts +1 -1
  28. package/dist/{catalog-CbR0CsUw.d.ts → catalog-CEYlMs9X.d.ts} +1 -1
  29. package/dist/causal-behavior.d.ts +1 -1
  30. package/dist/causal-consolidation.d.ts +1 -1
  31. package/dist/causal-consolidation.js +4 -4
  32. package/dist/{chunk-JEIROMLX.js → chunk-2COL35PK.js} +95 -9
  33. package/dist/chunk-2COL35PK.js.map +1 -0
  34. package/dist/{chunk-XZTUMWKQ.js → chunk-2PTBLS2S.js} +2 -2
  35. package/dist/{chunk-2Y6LMOQQ.js → chunk-2VHMVWPU.js} +2 -2
  36. package/dist/{chunk-6UU43ZGD.js → chunk-4KZGLDJJ.js} +2 -2
  37. package/dist/{chunk-K44MIN6D.js → chunk-5RHM43BJ.js} +3 -3
  38. package/dist/{chunk-2B43BYYZ.js → chunk-72VDBALG.js} +18 -18
  39. package/dist/{chunk-UY4JMEAK.js → chunk-7EEVXW7X.js} +2 -2
  40. package/dist/{chunk-XCTDHF7U.js → chunk-A5TUPZ3N.js} +33 -1
  41. package/dist/chunk-A5TUPZ3N.js.map +1 -0
  42. package/dist/{chunk-WI4QX7GH.js → chunk-A66YRSOS.js} +2 -2
  43. package/dist/{chunk-EM22PCQR.js → chunk-ANI5DUAE.js} +8 -6
  44. package/dist/{chunk-EM22PCQR.js.map → chunk-ANI5DUAE.js.map} +1 -1
  45. package/dist/{chunk-VZQXJNBD.js → chunk-APUJIMDM.js} +2 -2
  46. package/dist/{chunk-MPHDVHIB.js → chunk-DIFU33GU.js} +2 -2
  47. package/dist/{chunk-DXGS5YOQ.js → chunk-EH27LHML.js} +2 -2
  48. package/dist/{chunk-YAINYEM3.js → chunk-F3PVXKHP.js} +25 -1398
  49. package/dist/chunk-F3PVXKHP.js.map +1 -0
  50. package/dist/{chunk-DZYARLJ7.js → chunk-HLIR3RJ7.js} +2 -2
  51. package/dist/{chunk-6FUJ2VYY.js → chunk-JAKYZGV4.js} +3 -3
  52. package/dist/{chunk-ZAVTEQFM.js → chunk-JIZGYQYH.js} +3 -3
  53. package/dist/{chunk-QOIONSOI.js → chunk-KRGC4EDX.js} +2 -2
  54. package/dist/{chunk-AJOIM6UR.js → chunk-L5ASZA2F.js} +2 -2
  55. package/dist/{chunk-I56LKMF7.js → chunk-LXSG4L55.js} +2 -2
  56. package/dist/{chunk-MSVSP5VO.js → chunk-N3DUH6OO.js} +1 -1
  57. package/dist/chunk-N3DUH6OO.js.map +1 -0
  58. package/dist/{chunk-CTUWGJRI.js → chunk-NFRBADNR.js} +166 -28
  59. package/dist/chunk-NFRBADNR.js.map +1 -0
  60. package/dist/chunk-OJU73BUX.js +1688 -0
  61. package/dist/chunk-OJU73BUX.js.map +1 -0
  62. package/dist/{chunk-JVEMUX6L.js → chunk-OZ2KQ7HF.js} +2 -2
  63. package/dist/{chunk-OUDAU3T5.js → chunk-P5IPFZVH.js} +4 -4
  64. package/dist/{chunk-KHQ433G5.js → chunk-REWQAFSA.js} +2 -2
  65. package/dist/{chunk-V7GDTQ6I.js → chunk-S35FX6MU.js} +2 -2
  66. package/dist/{chunk-MTHNJCOJ.js → chunk-S3OPGGZB.js} +2 -2
  67. package/dist/{chunk-ZXLSNG3T.js → chunk-U5H7WHTS.js} +6 -4
  68. package/dist/{chunk-ZXLSNG3T.js.map → chunk-U5H7WHTS.js.map} +1 -1
  69. package/dist/{chunk-BZAK3RBN.js → chunk-UCJ5CDTK.js} +6 -6
  70. package/dist/{chunk-BF2426TN.js → chunk-URM3HW2R.js} +2 -2
  71. package/dist/{chunk-VO4UICII.js → chunk-YTZLBBEB.js} +2 -2
  72. package/dist/{chunk-OC56UWV5.js → chunk-YYOK4GVF.js} +2 -2
  73. package/dist/{cli-insNLYs6.d.ts → cli-1orcdXOY.d.ts} +3 -3
  74. package/dist/cli.d.ts +6 -6
  75. package/dist/cli.js +27 -27
  76. package/dist/compounding/engine.d.ts +1 -1
  77. package/dist/compounding/engine.js +3 -3
  78. package/dist/compounding/preference-consolidator.d.ts +1 -1
  79. package/dist/compression-optimizer.d.ts +1 -1
  80. package/dist/config.d.ts +1 -1
  81. package/dist/config.js +1 -1
  82. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  83. package/dist/connectors/codex-materialize-runner.js +3 -3
  84. package/dist/connectors/codex-materialize.d.ts +1 -1
  85. package/dist/connectors/index.d.ts +1 -1
  86. package/dist/connectors/index.js +3 -3
  87. package/dist/consolidation-provenance-check.d.ts +1 -1
  88. package/dist/consolidation-undo.d.ts +1 -1
  89. package/dist/contradiction/index.d.ts +1 -1
  90. package/dist/conversation-index/backend.d.ts +1 -1
  91. package/dist/conversation-index/chunker.d.ts +1 -1
  92. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  93. package/dist/conversation-index/indexer.d.ts +1 -1
  94. package/dist/conversation-index/search.d.ts +1 -1
  95. package/dist/day-summary.d.ts +1 -1
  96. package/dist/delinearize.d.ts +1 -1
  97. package/dist/direct-answer-wiring.d.ts +1 -1
  98. package/dist/direct-answer.d.ts +1 -1
  99. package/dist/embedding-fallback.d.ts +1 -1
  100. package/dist/enrichment/index.d.ts +1 -1
  101. package/dist/entity-retrieval.d.ts +1 -1
  102. package/dist/entity-retrieval.js +3 -3
  103. package/dist/entity-schema.d.ts +1 -1
  104. package/dist/explicit-capture.d.ts +4 -4
  105. package/dist/extraction-faithfulness.d.ts +1 -1
  106. package/dist/extraction-judge-telemetry.d.ts +1 -1
  107. package/dist/extraction-judge-training.d.ts +1 -1
  108. package/dist/extraction-judge.d.ts +1 -1
  109. package/dist/extraction.d.ts +1 -1
  110. package/dist/fallback-llm.d.ts +1 -1
  111. package/dist/identity-continuity.d.ts +1 -1
  112. package/dist/importance.d.ts +1 -1
  113. package/dist/index.d.ts +9 -9
  114. package/dist/index.js +33 -33
  115. package/dist/intent.d.ts +1 -1
  116. package/dist/lcm/engine.d.ts +1 -1
  117. package/dist/lcm/index.d.ts +1 -1
  118. package/dist/lcm/tools.d.ts +1 -1
  119. package/dist/lifecycle.d.ts +1 -1
  120. package/dist/live-connectors-runner.d.ts +1 -1
  121. package/dist/local-llm.d.ts +1 -1
  122. package/dist/maintenance/memory-governance.d.ts +1 -1
  123. package/dist/maintenance/memory-governance.js +3 -3
  124. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  125. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  126. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  127. package/dist/memory-action-policy.d.ts +1 -1
  128. package/dist/memory-cache.d.ts +1 -1
  129. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  130. package/dist/memory-projection-store.d.ts +1 -1
  131. package/dist/memory-provenance.d.ts +1 -1
  132. package/dist/memory-worth-outcomes.d.ts +1 -1
  133. package/dist/models-json.d.ts +1 -1
  134. package/dist/namespaces/migrate.d.ts +2 -2
  135. package/dist/namespaces/migrate.js +4 -4
  136. package/dist/namespaces/principal.d.ts +1 -1
  137. package/dist/namespaces/search.d.ts +1 -1
  138. package/dist/namespaces/storage.d.ts +2 -2
  139. package/dist/namespaces/storage.js +3 -3
  140. package/dist/native-knowledge.d.ts +1 -1
  141. package/dist/operator-toolkit.d.ts +1 -1
  142. package/dist/operator-toolkit.js +9 -9
  143. package/dist/orchestration/maintenance.d.ts +2 -2
  144. package/dist/orchestration/maintenance.js +5 -5
  145. package/dist/{orchestrator-XPKhY2OV.d.ts → orchestrator-kOxredSr.d.ts} +20 -3
  146. package/dist/orchestrator.d.ts +4 -4
  147. package/dist/orchestrator.js +16 -16
  148. package/dist/patterns-cli.d.ts +1 -1
  149. package/dist/policy-runtime.d.ts +1 -1
  150. package/dist/provenance.d.ts +1 -1
  151. package/dist/qmd-recall-cache.d.ts +1 -1
  152. package/dist/qmd.d.ts +1 -1
  153. package/dist/recall-disclosure-escalation.d.ts +1 -1
  154. package/dist/recall-explain-renderer.d.ts +1 -1
  155. package/dist/recall-explain-renderer.js +3 -3
  156. package/dist/recall-planner-llm.d.ts +1 -1
  157. package/dist/recall-state.d.ts +1 -1
  158. package/dist/recall-tag-filter.d.ts +1 -1
  159. package/dist/recall-xray-cli.d.ts +1 -1
  160. package/dist/recall-xray-cli.js +4 -4
  161. package/dist/recall-xray-renderer.d.ts +1 -1
  162. package/dist/recall-xray-renderer.js +3 -3
  163. package/dist/recall-xray.d.ts +1 -1
  164. package/dist/recall-xray.js +2 -2
  165. package/dist/resolve-auth-token.d.ts +1 -1
  166. package/dist/resume-bundles.js +2 -2
  167. package/dist/retrieval-agents.d.ts +1 -1
  168. package/dist/retrieval-tiers.d.ts +1 -1
  169. package/dist/routing/engine.d.ts +1 -1
  170. package/dist/routing/store.d.ts +1 -1
  171. package/dist/schemas.d.ts +22 -22
  172. package/dist/search/embed-helper.d.ts +1 -1
  173. package/dist/search/factory.d.ts +1 -1
  174. package/dist/search/index.d.ts +1 -1
  175. package/dist/search/lancedb-backend.d.ts +1 -1
  176. package/dist/search/meilisearch-backend.d.ts +1 -1
  177. package/dist/search/noop-backend.d.ts +1 -1
  178. package/dist/search/orama-backend.d.ts +1 -1
  179. package/dist/search/port.d.ts +1 -1
  180. package/dist/search/remote-backend.d.ts +1 -1
  181. package/dist/{semantic-consolidation-DSjMDZ1u.d.ts → semantic-consolidation-B58v3S-a.d.ts} +1 -1
  182. package/dist/semantic-consolidation.d.ts +2 -2
  183. package/dist/semantic-consolidation.js +4 -4
  184. package/dist/semantic-rule-promotion.js +3 -3
  185. package/dist/semantic-rule-verifier.d.ts +1 -1
  186. package/dist/semantic-rule-verifier.js +3 -3
  187. package/dist/session-observer-bands.d.ts +1 -1
  188. package/dist/session-observer-state.d.ts +1 -1
  189. package/dist/shared-context/manager.d.ts +1 -1
  190. package/dist/signal.d.ts +1 -1
  191. package/dist/storage.d.ts +1 -1
  192. package/dist/storage.js +2 -2
  193. package/dist/summarizer.d.ts +1 -1
  194. package/dist/summary-snapshot.d.ts +1 -1
  195. package/dist/temporal-supersession.d.ts +1 -1
  196. package/dist/temporal-validity.d.ts +1 -1
  197. package/dist/threading.d.ts +1 -1
  198. package/dist/tier-migration.d.ts +1 -1
  199. package/dist/tier-routing.d.ts +1 -1
  200. package/dist/topics.d.ts +1 -1
  201. package/dist/transcript.d.ts +1 -1
  202. package/dist/transfer/types.d.ts +12 -12
  203. package/dist/{types-D9dwzU_x.d.ts → types-ACmDOScJ.d.ts} +36 -0
  204. package/dist/types.d.ts +1 -1
  205. package/dist/types.js +1 -1
  206. package/dist/utility-runtime.d.ts +1 -1
  207. package/dist/verified-recall.js +3 -3
  208. package/package.json +2 -2
  209. package/src/briefing.ts +32 -0
  210. package/src/config.ts +32 -0
  211. package/src/correction/index.ts +24 -0
  212. package/src/correction/passive-capture.test.ts +500 -0
  213. package/src/correction/passive-capture.ts +335 -0
  214. package/src/correction/passive-correction-detector.test.ts +263 -0
  215. package/src/correction/passive-correction-detector.ts +325 -0
  216. package/src/correction/passive-correction-notifications.ts +123 -0
  217. package/src/orchestrator.ts +196 -12
  218. package/src/types.ts +36 -0
  219. package/dist/chunk-2KF4HDG2.js +0 -24
  220. package/dist/chunk-2KF4HDG2.js.map +0 -1
  221. package/dist/chunk-CTUWGJRI.js.map +0 -1
  222. package/dist/chunk-JEIROMLX.js.map +0 -1
  223. package/dist/chunk-MSVSP5VO.js.map +0 -1
  224. package/dist/chunk-XCTDHF7U.js.map +0 -1
  225. package/dist/chunk-YAINYEM3.js.map +0 -1
  226. /package/dist/{chunk-XZTUMWKQ.js.map → chunk-2PTBLS2S.js.map} +0 -0
  227. /package/dist/{chunk-2Y6LMOQQ.js.map → chunk-2VHMVWPU.js.map} +0 -0
  228. /package/dist/{chunk-6UU43ZGD.js.map → chunk-4KZGLDJJ.js.map} +0 -0
  229. /package/dist/{chunk-K44MIN6D.js.map → chunk-5RHM43BJ.js.map} +0 -0
  230. /package/dist/{chunk-2B43BYYZ.js.map → chunk-72VDBALG.js.map} +0 -0
  231. /package/dist/{chunk-UY4JMEAK.js.map → chunk-7EEVXW7X.js.map} +0 -0
  232. /package/dist/{chunk-WI4QX7GH.js.map → chunk-A66YRSOS.js.map} +0 -0
  233. /package/dist/{chunk-VZQXJNBD.js.map → chunk-APUJIMDM.js.map} +0 -0
  234. /package/dist/{chunk-MPHDVHIB.js.map → chunk-DIFU33GU.js.map} +0 -0
  235. /package/dist/{chunk-DXGS5YOQ.js.map → chunk-EH27LHML.js.map} +0 -0
  236. /package/dist/{chunk-DZYARLJ7.js.map → chunk-HLIR3RJ7.js.map} +0 -0
  237. /package/dist/{chunk-6FUJ2VYY.js.map → chunk-JAKYZGV4.js.map} +0 -0
  238. /package/dist/{chunk-ZAVTEQFM.js.map → chunk-JIZGYQYH.js.map} +0 -0
  239. /package/dist/{chunk-QOIONSOI.js.map → chunk-KRGC4EDX.js.map} +0 -0
  240. /package/dist/{chunk-AJOIM6UR.js.map → chunk-L5ASZA2F.js.map} +0 -0
  241. /package/dist/{chunk-I56LKMF7.js.map → chunk-LXSG4L55.js.map} +0 -0
  242. /package/dist/{chunk-JVEMUX6L.js.map → chunk-OZ2KQ7HF.js.map} +0 -0
  243. /package/dist/{chunk-OUDAU3T5.js.map → chunk-P5IPFZVH.js.map} +0 -0
  244. /package/dist/{chunk-KHQ433G5.js.map → chunk-REWQAFSA.js.map} +0 -0
  245. /package/dist/{chunk-V7GDTQ6I.js.map → chunk-S35FX6MU.js.map} +0 -0
  246. /package/dist/{chunk-MTHNJCOJ.js.map → chunk-S3OPGGZB.js.map} +0 -0
  247. /package/dist/{chunk-BZAK3RBN.js.map → chunk-UCJ5CDTK.js.map} +0 -0
  248. /package/dist/{chunk-BF2426TN.js.map → chunk-URM3HW2R.js.map} +0 -0
  249. /package/dist/{chunk-VO4UICII.js.map → chunk-YTZLBBEB.js.map} +0 -0
  250. /package/dist/{chunk-OC56UWV5.js.map → chunk-YYOK4GVF.js.map} +0 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * correction/passive-capture.ts — routes detected passive corrections to the
3
+ * Correction Contract (#1580) by mode: queue, auto, or off (issue #1581).
4
+ *
5
+ * This module is the thin handoff between the extraction post-processing step
6
+ * (which calls `detectPassiveCorrections`) and the CorrectionService's
7
+ * `plan()` / `apply()` pipeline. It adds NO parallel correction logic (rule 22)
8
+ * — the planner does all the locating/classifying; this module only decides
9
+ * whether to queue the resulting plan for review or auto-apply it.
10
+ *
11
+ * Guards (auto mode):
12
+ * - plan confidence ≥ confidenceFloor
13
+ * - classification ∈ {outdated, wrong}
14
+ * - affected.length ≤ autoApplyMaxAffected
15
+ * - every action ∈ {supersede, edit, retract} (never auto-apply
16
+ * rescope/redaction_rule/never_store — those are policy changes)
17
+ * If ANY guard fails, the plan falls back to queue.
18
+ *
19
+ * Session scoping (rule 42): a correction detected in session S plans only
20
+ * against S's readable namespaces. The CorrectionService already enforces this
21
+ * through its namespace policy; this module passes the session-scoped context.
22
+ *
23
+ * Dedup: fingerprint detected corrections by `bufferKey + normalizedCorrectionText`
24
+ * so parallel sessions / re-flushes don't double-plan.
25
+ */
26
+
27
+ import { log } from "../logger.js";
28
+ import type { PassiveCorrection } from "./passive-correction-detector.js";
29
+ import type {
30
+ CorrectionClassification,
31
+ CorrectionOutcome,
32
+ CorrectionPlan,
33
+ CorrectionRequest,
34
+ CorrectionAction,
35
+ } from "./correction-contract.js";
36
+ import {
37
+ enqueuePassiveCorrectionNotification,
38
+ } from "./passive-correction-notifications.js";
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Config + context
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export type CorrectionCaptureMode = "off" | "queue" | "auto";
45
+
46
+ export interface PassiveCaptureConfig {
47
+ mode: CorrectionCaptureMode;
48
+ confidenceFloor: number;
49
+ autoApplyMaxAffected: number;
50
+ }
51
+
52
+ export interface PassiveCaptureContext {
53
+ /** The correction feature must be enabled for capture to run. */
54
+ correctionEnabled: boolean;
55
+ /** Whether this is a live session (not replay/import). Auto-apply is
56
+ * live-session only — replayed history may contain corrections the user
57
+ * later re-reverted; queue those. */
58
+ isLiveSession: boolean;
59
+ /** Buffer key for dedup fingerprinting. */
60
+ bufferKey: string;
61
+ /** Session key for namespace/principal resolution. */
62
+ sessionKey?: string;
63
+ /** Authenticated principal. */
64
+ principal?: string;
65
+ /** Authorized namespace the extraction wrote to. */
66
+ namespace: string;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Deps (injected — the orchestrator wires these)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export interface PassiveCaptureDeps {
74
+ /** Plan a correction through the Correction Contract. */
75
+ planCorrection(request: CorrectionRequest): Promise<CorrectionPlan>;
76
+ /** Apply a planned correction (auto mode only). */
77
+ applyCorrection(
78
+ planId: string,
79
+ opts: { confirm: true; namespace?: string; sessionKey?: string; principal?: string },
80
+ ): Promise<CorrectionOutcome>;
81
+ /** Resolve the storage dir for notification enqueue (per namespace). */
82
+ storageDir(namespace: string): Promise<string>;
83
+ /**
84
+ * Resolve a memory handle (`[m:xxxx]`) to a concrete memory id, returning
85
+ * null when the handle is unresolvable in the session (graceful — the
86
+ * planner then falls back to text search). Wired to the orchestrator single
87
+ * shared `resolveMemoryIdOrHandle` helper (#1582) so handle resolution has
88
+ * one path (rule 22). Review: "memory handles not resolved".
89
+ */
90
+ resolveHandle?(ref: string, sessionKey?: string): string | null;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Telemetry
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export interface PassiveCaptureTelemetry {
98
+ detected: number;
99
+ queued: number;
100
+ autoApplied: number;
101
+ /** Reason → count for auto-apply suppressions. */
102
+ suppressedReasons: Record<string, number>;
103
+ }
104
+
105
+ export function emptyTelemetry(): PassiveCaptureTelemetry {
106
+ return { detected: 0, queued: 0, autoApplied: 0, suppressedReasons: {} };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Dedup fingerprint
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /** Normalize correction text for fingerprinting (case + whitespace + punctuation). */
114
+ function normalizeForFingerprint(text: string): string {
115
+ return text
116
+ .toLowerCase()
117
+ .replace(/[^\w\s]/g, "")
118
+ .replace(/\s+/g, " ")
119
+ .trim()
120
+ .slice(0, 200);
121
+ }
122
+
123
+ function correctionFingerprint(bufferKey: string, correction: PassiveCorrection): string {
124
+ return `${bufferKey}::${correction.polarity}::${normalizeForFingerprint(correction.targetHint)}`;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Auto-apply guards
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /** Action kinds that are safe to auto-apply (state-preserving, not policy changes). */
132
+ const AUTO_APPLICABLE_ACTION_KINDS = new Set(["supersede", "edit", "retract"]);
133
+
134
+ /** Classifications safe for auto-apply. */
135
+ const AUTO_APPLICABLE_CLASSIFICATIONS = new Set<CorrectionClassification>(["outdated", "wrong"]);
136
+
137
+ export type AutoApplySuppressionReason =
138
+ | "non_live_session"
139
+ | "confidence_below_floor"
140
+ | "classification_not_allowed"
141
+ | "affected_too_large"
142
+ | "disallowed_action_kind"
143
+ | "empty_plan";
144
+
145
+ /**
146
+ * Evaluate all auto-apply guards. Returns the suppression reason (if any), or
147
+ * null if auto-apply may proceed.
148
+ */
149
+ export function evaluateAutoApplyGuards(
150
+ plan: CorrectionPlan,
151
+ config: PassiveCaptureConfig,
152
+ isLiveSession: boolean,
153
+ ): AutoApplySuppressionReason | null {
154
+ if (!isLiveSession) return "non_live_session";
155
+ if (plan.confidence < config.confidenceFloor) return "confidence_below_floor";
156
+ if (!AUTO_APPLICABLE_CLASSIFICATIONS.has(plan.classification)) {
157
+ return "classification_not_allowed";
158
+ }
159
+ if (plan.affected.length > config.autoApplyMaxAffected) {
160
+ return "affected_too_large";
161
+ }
162
+ // An empty action list is a no-op plan (review: "queue plans that contain no
163
+ // actions"). Auto-applying it would mark a correction as applied, write an
164
+ // audit record + notification, and record the dedup fingerprint — yet nothing
165
+ // changed. Suppress so it queues for human review instead.
166
+ if (plan.actions.length === 0) {
167
+ return "empty_plan";
168
+ }
169
+ for (const action of plan.actions) {
170
+ if (!AUTO_APPLICABLE_ACTION_KINDS.has(action.kind)) {
171
+ return "disallowed_action_kind";
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Main entry: capture passive corrections
179
+ // ---------------------------------------------------------------------------
180
+
181
+ export interface PassiveCaptureResult {
182
+ telemetry: PassiveCaptureTelemetry;
183
+ /** Plans created (both queued and auto-applied). */
184
+ plans: CorrectionPlan[];
185
+ }
186
+
187
+ /**
188
+ * Process a batch of detected passive corrections. For each correction:
189
+ * 1. Check dedup fingerprint — skip if already processed for this bufferKey.
190
+ * 2. Build a CorrectionRequest and call the planner.
191
+ * 3. Dispatch by mode: queue → plan is left pending; auto → apply if all
192
+ * guards pass, else fall back to queue.
193
+ * 4. Record telemetry.
194
+ *
195
+ * Fail-open: a planning or apply error for ONE correction never blocks the
196
+ * others. The error is logged and the correction is counted as suppressed.
197
+ */
198
+ export async function capturePassiveCorrections(
199
+ corrections: readonly PassiveCorrection[],
200
+ ctx: PassiveCaptureContext,
201
+ config: PassiveCaptureConfig,
202
+ deps: PassiveCaptureDeps,
203
+ dedupState: Set<string>,
204
+ ): Promise<PassiveCaptureResult> {
205
+ const telemetry = emptyTelemetry();
206
+ const plans: CorrectionPlan[] = [];
207
+ telemetry.detected = corrections.length;
208
+
209
+ for (const correction of corrections) {
210
+ // 1. Dedup — checked before planning, but the fingerprint is only
211
+ // recorded AFTER a successful plan so a transient planning failure
212
+ // can be retried on a later flush (review: "dedup blocks retry
213
+ // after failure").
214
+ const fp = correctionFingerprint(ctx.bufferKey, correction);
215
+ if (dedupState.has(fp)) {
216
+ continue;
217
+ }
218
+
219
+ // 2. Resolve `[m:xxxx]` handles to concrete memory ids (review:
220
+ // "memory handles not resolved"). The planner resolveTargetMemories
221
+ // expects memory ids, not raw handle tokens — passing a handle
222
+ // string would throw "target memory not found". Unresolvable handles
223
+ // are dropped so the planner falls back to text search (graceful
224
+ // degradation; a correction without a resolved target is still valid).
225
+ let targetIds: string[] | undefined;
226
+ if (correction.handles.length > 0 && deps.resolveHandle && ctx.sessionKey) {
227
+ const resolved: string[] = [];
228
+ for (const handle of correction.handles) {
229
+ const id = deps.resolveHandle(handle, ctx.sessionKey);
230
+ if (id) resolved.push(id);
231
+ }
232
+ if (resolved.length > 0) targetIds = resolved;
233
+ }
234
+
235
+ // 3. Build + plan
236
+ const request: CorrectionRequest = {
237
+ text: correction.correctedAssertion || correction.sourceExcerpt,
238
+ ...(targetIds ? { targetIds } : {}),
239
+ ...(ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}),
240
+ ...(ctx.principal ? { principal: ctx.principal } : {}),
241
+ namespace: ctx.namespace,
242
+ };
243
+
244
+ let plan: CorrectionPlan;
245
+ try {
246
+ plan = await deps.planCorrection(request);
247
+ } catch (err) {
248
+ log.warn(
249
+ `passive-correction: planning failed for "${correction.targetHint.slice(0, 60)}": ${err instanceof Error ? err.message : String(err)}`,
250
+ );
251
+ continue;
252
+ }
253
+ plans.push(plan);
254
+ // Record the fingerprint only after a successful plan.
255
+ dedupState.add(fp);
256
+
257
+ // 3. Dispatch by mode
258
+ if (config.mode === "queue") {
259
+ telemetry.queued += 1;
260
+ continue;
261
+ }
262
+
263
+ // mode === "auto"
264
+ const suppression = evaluateAutoApplyGuards(plan, config, ctx.isLiveSession);
265
+ if (suppression) {
266
+ telemetry.suppressedReasons[suppression] =
267
+ (telemetry.suppressedReasons[suppression] ?? 0) + 1;
268
+ telemetry.queued += 1;
269
+ continue;
270
+ }
271
+
272
+ // Guards passed — auto-apply
273
+ try {
274
+ const outcome = await deps.applyCorrection(plan.planId, {
275
+ confirm: true,
276
+ namespace: ctx.namespace,
277
+ ...(ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}),
278
+ ...(ctx.principal ? { principal: ctx.principal } : {}),
279
+ });
280
+ // A partial outcome means some actions failed (per-action races or
281
+ // storage failures). Don't count it as auto-applied or notify — queue
282
+ // for human review instead (review: "partial outcomes as failures").
283
+ if (outcome.status !== "applied") {
284
+ // Partial outcome: some actions applied, some failed. The plan is
285
+ // consumed (status 'partial', not 'pending'), so it won't appear in
286
+ // listPending — do NOT increment queued (review: "partial not
287
+ // re-queued"). The audit record captures which actions failed.
288
+ log.warn(
289
+ `passive-correction: plan ${plan.planId} applied partially — some actions failed (see audit record)`,
290
+ );
291
+ continue;
292
+ }
293
+ telemetry.autoApplied += 1;
294
+
295
+ // Enqueue notification for the next briefing
296
+ try {
297
+ const dir = await deps.storageDir(ctx.namespace);
298
+ await enqueuePassiveCorrectionNotification(dir, {
299
+ planId: plan.planId,
300
+ summary: buildNotificationSummary(plan),
301
+ undoCommand: `auto-applied (plan ${plan.planId})`,
302
+ createdAt: new Date().toISOString(),
303
+ });
304
+ } catch (err) {
305
+ // Notification is best-effort — never block on it.
306
+ log.debug(`passive-correction: notification enqueue failed (non-fatal): ${err}`);
307
+ }
308
+ } catch (err) {
309
+ log.warn(
310
+ `passive-correction: auto-apply failed for plan ${plan.planId}: ${err instanceof Error ? err.message : String(err)} — plan left in queue`,
311
+ );
312
+ telemetry.queued += 1;
313
+ }
314
+ }
315
+
316
+ return { telemetry, plans };
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Notification summary builder
321
+ // ---------------------------------------------------------------------------
322
+
323
+ function buildNotificationSummary(plan: CorrectionPlan): string {
324
+ const action = plan.actions[0];
325
+ if (!action) {
326
+ return plan.request.text.slice(0, 80);
327
+ }
328
+ const target =
329
+ action.kind === "supersede"
330
+ ? action.loserId
331
+ : action.kind === "edit" || action.kind === "retract" || action.kind === "rescope"
332
+ ? action.memoryId
333
+ : "pattern";
334
+ return `${plan.classification}: ${plan.request.text.slice(0, 60)}${plan.request.text.length > 60 ? "..." : ""} (${target})`;
335
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * passive-correction-detector.test.ts — fixture matrix for passive correction
3
+ * detection (issue #1581 PR 1).
4
+ *
5
+ * Positive fixtures (≥12): direct update, retraction, stop-storing, tense/
6
+ * morphology variants, correction embedded mid-paragraph, handle-referenced.
7
+ * Anti-fixtures (≥8): quoting someone else's correction; correcting a third
8
+ * party; hypothetical; self-correction within the same turn that resolves
9
+ * itself; the agent being corrected about a tool output, not stored memory.
10
+ */
11
+
12
+ import { strict as assert } from "node:assert";
13
+ import { test } from "node:test";
14
+ import {
15
+ detectPassiveCorrections,
16
+ extractHandles,
17
+ type DetectorTurn,
18
+ } from "./passive-correction-detector.js";
19
+
20
+ function user(content: string): DetectorTurn[] {
21
+ return [{ role: "user", content }];
22
+ }
23
+
24
+ test("positive: direct update — actually we use PostgreSQL now", () => {
25
+ const results = detectPassiveCorrections(user("actually we use PostgreSQL now, not MySQL"));
26
+ assert.ok(results.length >= 1, `expected >=1 correction, got ${results.length}`);
27
+ assert.ok(results.some((r) => r.polarity === "update"));
28
+ });
29
+
30
+ test("positive: direct update — no, we renamed that service", () => {
31
+ const results = detectPassiveCorrections(user("no, we renamed that service to auth-svc"));
32
+ assert.ok(results.length >= 1);
33
+ assert.ok(results.some((r) => r.polarity === "update"));
34
+ });
35
+
36
+ test("positive: retraction — we don't use Redis anymore", () => {
37
+ const results = detectPassiveCorrections(user("we don't use Redis anymore"));
38
+ assert.ok(results.length >= 1);
39
+ assert.ok(results.some((r) => r.polarity === "retract"));
40
+ });
41
+
42
+ test("positive: retraction — that's wrong", () => {
43
+ const results = detectPassiveCorrections(user("that's wrong, I never said that"));
44
+ assert.ok(results.length >= 1);
45
+ assert.ok(results.some((r) => r.polarity === "retract"));
46
+ });
47
+
48
+ test("positive: stop-storing — stop suggesting Vim", () => {
49
+ const results = detectPassiveCorrections(user("stop suggesting Vim, I switched to Helix months ago"));
50
+ assert.ok(results.length >= 1);
51
+ assert.ok(results.some((r) => r.polarity === "stop_storing"));
52
+ });
53
+
54
+ test("positive: stop-storing — don't bring up that project", () => {
55
+ const results = detectPassiveCorrections(user("don't bring up that project anymore"));
56
+ assert.ok(results.length >= 1);
57
+ assert.ok(results.some((r) => r.polarity === "stop_storing"));
58
+ });
59
+
60
+ test("positive: tense variant — we switched to (past)", () => {
61
+ const results = detectPassiveCorrections(user("we switched to GitHub Actions for CI"));
62
+ assert.ok(results.length >= 1);
63
+ assert.ok(results.some((r) => r.polarity === "update"));
64
+ });
65
+
66
+ test("positive: tense variant — we're switching to (progressive)", () => {
67
+ const results = detectPassiveCorrections(user("we're switching to pnpm from npm"));
68
+ assert.ok(results.length >= 1, `expected >=1, got ${results.length}`);
69
+ assert.ok(results.some((r) => r.polarity === "update"));
70
+ });
71
+
72
+ test("positive: tense variant — we've migrated to (present perfect)", () => {
73
+ const results = detectPassiveCorrections(user("we've migrated to PostgreSQL"));
74
+ assert.ok(results.length >= 1, `expected >=1, got ${results.length}`);
75
+ assert.ok(results.some((r) => r.polarity === "update"));
76
+ });
77
+
78
+ test("positive: no longer phrasing", () => {
79
+ const results = detectPassiveCorrections(user("I no longer work on that project"));
80
+ assert.ok(results.length >= 1);
81
+ assert.ok(results.some((r) => r.polarity === "update"));
82
+ });
83
+
84
+ test("positive: that's outdated phrasing", () => {
85
+ const results = detectPassiveCorrections(user("that's outdated, the API changed last week"));
86
+ assert.ok(results.length >= 1);
87
+ assert.ok(results.some((r) => r.polarity === "update"));
88
+ });
89
+
90
+ test("positive: correction embedded mid-paragraph", () => {
91
+ const results = detectPassiveCorrections(
92
+ user("I was reviewing the architecture and noticed the docs say we use AWS. Actually, we moved to GCP last quarter, so those docs need updating."),
93
+ );
94
+ assert.ok(results.length >= 1);
95
+ assert.ok(results.some((r) => r.polarity === "update"));
96
+ });
97
+
98
+ test("positive: deadline moved to Friday", () => {
99
+ const results = detectPassiveCorrections(user("the deadline moved to Friday"));
100
+ assert.ok(results.length >= 1);
101
+ assert.ok(results.some((r) => r.polarity === "update"));
102
+ });
103
+
104
+ test("positive: handle-referenced [m:4f2a] is wrong", () => {
105
+ const results = detectPassiveCorrections(user("that's wrong, [m:4f2a] is incorrect — the real value is 42"));
106
+ assert.ok(results.length >= 1);
107
+ assert.ok(results[0].handles.includes("[m:4f2a]"));
108
+ });
109
+
110
+ test("positive: multiple handles extracted", () => {
111
+ const results = detectPassiveCorrections(
112
+ user("actually, [m:4f2a] and [m:8b1c] are both outdated"),
113
+ );
114
+ assert.ok(results.length >= 1, `expected >=1, got ${results.length}`);
115
+ assert.ok(results[0].handles.includes("[m:4f2a]"));
116
+ assert.ok(results[0].handles.includes("[m:8b1c]"));
117
+ });
118
+
119
+ // ── Anti-fixtures ──────────────────────────────────────────────────────────
120
+
121
+ test("anti-fixture: quoting someone else's correction", () => {
122
+ const results = detectPassiveCorrections(
123
+ user('Bob told me "that\'s wrong" about the deployment process'),
124
+ );
125
+ const retracts = results.filter((r) => r.polarity === "retract");
126
+ assert.strictEqual(retracts.length, 0, `expected 0 retract corrections, got ${retracts.length}`);
127
+ });
128
+
129
+ test("anti-fixture: correcting a third party", () => {
130
+ const results = detectPassiveCorrections(
131
+ user("tell Bob he's wrong about the API design"),
132
+ );
133
+ assert.strictEqual(results.length, 0);
134
+ });
135
+
136
+ test("anti-fixture: hypothetical — if we ever moved to MySQL", () => {
137
+ const results = detectPassiveCorrections(
138
+ user("if we ever moved to MySQL, we'd need to rewrite the migrations"),
139
+ );
140
+ assert.strictEqual(results.length, 0);
141
+ });
142
+
143
+ test("anti-fixture: hypothetical — what if we switch", () => {
144
+ const results = detectPassiveCorrections(
145
+ user("what if we switch to a different framework?"),
146
+ );
147
+ assert.strictEqual(results.length, 0);
148
+ });
149
+
150
+ test("anti-fixture: self-resolving — actually wait, the original was right", () => {
151
+ const results = detectPassiveCorrections(
152
+ user("actually, wait, never mind, the original was right"),
153
+ );
154
+ assert.strictEqual(results.length, 0);
155
+ });
156
+
157
+ test("anti-fixture: self-resolving — scratch that", () => {
158
+ const results = detectPassiveCorrections(
159
+ user("actually we moved to... no, scratch that, we're still on the old system"),
160
+ );
161
+ assert.strictEqual(results.length, 0);
162
+ });
163
+
164
+ test("anti-fixture: tool output correction", () => {
165
+ const results = detectPassiveCorrections(
166
+ user("the output is wrong, the test results don't match"),
167
+ );
168
+ assert.strictEqual(results.length, 0);
169
+ });
170
+
171
+ test("anti-fixture: agent corrected about code, not stored memory", () => {
172
+ const results = detectPassiveCorrections(
173
+ user("your code has a bug in the error handler"),
174
+ );
175
+ assert.strictEqual(results.length, 0);
176
+ });
177
+
178
+ test("anti-fixture: suppose / hypothetical scenario", () => {
179
+ const results = detectPassiveCorrections(
180
+ user("suppose we changed the database — what would break?"),
181
+ );
182
+ assert.strictEqual(results.length, 0);
183
+ });
184
+
185
+ // ── Characterization ───────────────────────────────────────────────────────
186
+
187
+ test("characterization: assistant turns are not scanned", () => {
188
+ const results = detectPassiveCorrections([
189
+ { role: "assistant", content: "actually, we moved to PostgreSQL" },
190
+ ]);
191
+ assert.strictEqual(results.length, 0);
192
+ });
193
+
194
+ test("characterization: empty content produces no corrections", () => {
195
+ const results = detectPassiveCorrections([{ role: "user", content: "" }]);
196
+ assert.strictEqual(results.length, 0);
197
+ });
198
+
199
+ test("characterization: non-corrective user text produces no corrections", () => {
200
+ const results = detectPassiveCorrections(
201
+ user("Can you help me write a function to parse JSON?"),
202
+ );
203
+ assert.strictEqual(results.length, 0);
204
+ });
205
+
206
+ test("characterization: confidence is in [0, 1]", () => {
207
+ const results = detectPassiveCorrections(user("we switched to Go"));
208
+ for (const r of results) {
209
+ assert.ok(r.confidence >= 0 && r.confidence <= 1);
210
+ }
211
+ });
212
+
213
+ test("characterization: sourceExcerpt is populated", () => {
214
+ const results = detectPassiveCorrections(user("we switched to Go"));
215
+ for (const r of results) {
216
+ assert.ok(r.sourceExcerpt.length > 0);
217
+ }
218
+ });
219
+
220
+ // ── extractHandles ─────────────────────────────────────────────────────────
221
+
222
+ test("extractHandles: extracts [m:xxxx] handles", () => {
223
+ assert.deepStrictEqual(extractHandles("see [m:4f2a] and [m:8b1c]"), ["[m:4f2a]", "[m:8b1c]"]);
224
+ });
225
+
226
+ test("extractHandles: returns empty for no handles", () => {
227
+ assert.deepStrictEqual(extractHandles("no handles here"), []);
228
+ });
229
+
230
+ test("contraction does not suppress a correction phrase (review: apostrophe quote detection)", () => {
231
+ // OLD isWithinQuotes treated the ASCII apostrophe as a quote opener. A single
232
+ // contraction with no closing apostrophe ("We've switched to Postgres") left
233
+ // the scanner in quoted mode and suppressed the correction — a false negative.
234
+ // Only double quotes delimit quoted speech; apostrophes are contractions.
235
+ const results = detectPassiveCorrections(user("We've switched to Postgres"));
236
+ assert.ok(results.length > 0, "contraction-led correction must be detected");
237
+ assert.ok(results.some((r) => r.polarity === "update"));
238
+ });
239
+
240
+ test("double-quoted correction is still suppressed (isWithinQuotes uses double quotes only)", () => {
241
+ // After dropping single-quote handling, real double-quoted speech must STILL
242
+ // be suppressed — only the delimiter set changed, not the suppression itself.
243
+ const results = detectPassiveCorrections(
244
+ user('Bob told me "that\'s wrong" about the deployment process'),
245
+ );
246
+ const retracts = results.filter((r) => r.polarity === "retract");
247
+ assert.strictEqual(retracts.length, 0, "double-quoted speech must still be suppressed");
248
+ });
249
+
250
+ test("overlapping cues dedup: 'actually, we don't use Redis anymore' emits one correction (review)", () => {
251
+ // Both the "actually" (update) and "don't use X anymore" (retract) patterns
252
+ // match the same targetHint. Polarity-agnostic dedup keeps one correction.
253
+ const results = detectPassiveCorrections(
254
+ user("actually, we don't use Redis anymore for caching"),
255
+ );
256
+ const redisCorrections = results.filter((r) =>
257
+ r.targetHint.toLowerCase().includes("redis"),
258
+ );
259
+ assert.ok(
260
+ redisCorrections.length <= 1,
261
+ `overlapping cues for same target must emit at most one correction, got ${redisCorrections.length}`,
262
+ );
263
+ });