@remnic/core 9.38.2 → 9.39.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 (189) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +13 -13
  3. package/dist/access-authorization-probe.d.ts +5 -5
  4. package/dist/access-authorization-probe.js +14 -14
  5. package/dist/access-boundary.d.ts +5 -5
  6. package/dist/access-boundary.js +13 -13
  7. package/dist/access-cli.js +35 -35
  8. package/dist/access-extraction-force-flush.d.ts +5 -5
  9. package/dist/access-extraction-force-flush.js +13 -13
  10. package/dist/access-http-lcm-compaction.d.ts +5 -5
  11. package/dist/access-http-lifecycle-flush.d.ts +5 -5
  12. package/dist/access-http.d.ts +5 -5
  13. package/dist/access-http.js +18 -18
  14. package/dist/access-identity-continuity-surface.d.ts +4 -4
  15. package/dist/access-identity-continuity-surface.js +13 -13
  16. package/dist/access-lcm-surface.d.ts +5 -5
  17. package/dist/access-lcm-surface.js +13 -13
  18. package/dist/access-mcp.d.ts +5 -5
  19. package/dist/access-mcp.js +16 -16
  20. package/dist/access-namespace-preflight.js +13 -13
  21. package/dist/access-observe-write-surface.d.ts +5 -5
  22. package/dist/access-observe-write-surface.js +13 -13
  23. package/dist/access-operations-batch.js +14 -14
  24. package/dist/access-operations.d.ts +5 -5
  25. package/dist/access-operations.js +15 -15
  26. package/dist/access-recall-concurrency.d.ts +5 -5
  27. package/dist/access-recall-concurrency.js +13 -13
  28. package/dist/access-recall-response.d.ts +5 -5
  29. package/dist/access-recall-response.js +13 -13
  30. package/dist/access-recall-surface.d.ts +5 -5
  31. package/dist/access-recall-surface.js +13 -13
  32. package/dist/{access-service-C2WYT1pO.d.ts → access-service-C6QuKBCG.d.ts} +3 -3
  33. package/dist/access-service-helpers.d.ts +5 -5
  34. package/dist/access-service.d.ts +5 -5
  35. package/dist/access-service.js +13 -13
  36. package/dist/access-surface-catalog.d.ts +5 -5
  37. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  38. package/dist/bootstrap.d.ts +4 -4
  39. package/dist/briefing.d.ts +1 -1
  40. package/dist/briefing.js +5 -5
  41. package/dist/buffer.d.ts +1 -1
  42. package/dist/buffer.js +2 -2
  43. package/dist/causal-consolidation.js +6 -6
  44. package/dist/{chunk-2YJPCRUM.js → chunk-4CSTEPFA.js} +2 -2
  45. package/dist/{chunk-T3FVAHM7.js → chunk-4VXGRIH6.js} +286 -7
  46. package/dist/chunk-4VXGRIH6.js.map +1 -0
  47. package/dist/{chunk-A4P6H25D.js → chunk-5JUYBWJD.js} +19 -19
  48. package/dist/{chunk-JYLN6KHH.js → chunk-5TRIAPT4.js} +7 -9
  49. package/dist/{chunk-JYLN6KHH.js.map → chunk-5TRIAPT4.js.map} +1 -1
  50. package/dist/{chunk-6D5D5A4M.js → chunk-7ERHAARG.js} +2 -2
  51. package/dist/{chunk-GORHSN5C.js → chunk-7OCTH7AJ.js} +2 -2
  52. package/dist/{chunk-ILGC6JH6.js → chunk-7RJUVTVF.js} +16 -1
  53. package/dist/chunk-7RJUVTVF.js.map +1 -0
  54. package/dist/{chunk-FE7MUX7M.js → chunk-AL73FWSS.js} +2 -2
  55. package/dist/{chunk-6TF4M2LB.js → chunk-AVGTOOYP.js} +2 -2
  56. package/dist/{chunk-B6XHIBIB.js → chunk-AX2J2LC4.js} +2 -2
  57. package/dist/{chunk-ZB3YODID.js → chunk-BVJIAIB3.js} +8 -8
  58. package/dist/{chunk-XN3EW6P2.js → chunk-CMENJQ6I.js} +2 -2
  59. package/dist/{chunk-PRP3M5Y3.js → chunk-CMFUYOYB.js} +2 -2
  60. package/dist/{chunk-M3DBB2ND.js → chunk-CNZLKQEL.js} +2 -2
  61. package/dist/{chunk-YVMVVE4B.js → chunk-CPD6C6K6.js} +2 -2
  62. package/dist/{chunk-VU7732DU.js → chunk-CYM2PHLB.js} +5 -5
  63. package/dist/{chunk-RY2XZTFN.js → chunk-DJH35IVS.js} +2 -2
  64. package/dist/{chunk-Q3CVY7LV.js → chunk-ENLZOWYK.js} +201 -231
  65. package/dist/chunk-ENLZOWYK.js.map +1 -0
  66. package/dist/{chunk-IJAQCBGV.js → chunk-F5EVTFQK.js} +2 -2
  67. package/dist/{chunk-RY7CHVGE.js → chunk-G57BGHQ2.js} +2 -2
  68. package/dist/{chunk-F3BNEYX2.js → chunk-GUQYMOPU.js} +2 -2
  69. package/dist/{chunk-IMEXE4XS.js → chunk-GYGXKATH.js} +6 -6
  70. package/dist/{chunk-CKBKHZKZ.js → chunk-JKTHT4KX.js} +3 -3
  71. package/dist/{chunk-HQYYJRU7.js → chunk-KJP2R5MJ.js} +15 -15
  72. package/dist/{chunk-UBWOCQ3I.js → chunk-MHCRASVK.js} +2 -2
  73. package/dist/{chunk-RYMPLSIT.js → chunk-NF3U3MPG.js} +41 -8
  74. package/dist/chunk-NF3U3MPG.js.map +1 -0
  75. package/dist/{chunk-DKKZ6SO2.js → chunk-SHTPVI47.js} +22 -45
  76. package/dist/chunk-SHTPVI47.js.map +1 -0
  77. package/dist/{chunk-PYTUBWAS.js → chunk-UGAZILZG.js} +2 -2
  78. package/dist/{chunk-4TAD3B56.js → chunk-VGK33D3K.js} +2 -2
  79. package/dist/{chunk-BGLDKAYY.js → chunk-VK33JSBR.js} +2 -2
  80. package/dist/{chunk-NBAEGFXT.js → chunk-VQTRSX4R.js} +11 -14
  81. package/dist/chunk-VQTRSX4R.js.map +1 -0
  82. package/dist/{chunk-WET4M3R6.js → chunk-XSPD6KQL.js} +2 -2
  83. package/dist/{chunk-HUS2XN3T.js → chunk-YFHMW37V.js} +2 -2
  84. package/dist/{chunk-HKF5VASQ.js → chunk-ZA53AZND.js} +17 -6
  85. package/dist/chunk-ZA53AZND.js.map +1 -0
  86. package/dist/{chunk-5ZGHUWTT.js → chunk-ZBDORYWM.js} +2 -2
  87. package/dist/{cli-jkmzdT8P.d.ts → cli-ClXRCu08.d.ts} +3 -3
  88. package/dist/cli.d.ts +6 -6
  89. package/dist/cli.js +27 -27
  90. package/dist/compounding/engine.d.ts +1 -1
  91. package/dist/compounding/engine.js +5 -5
  92. package/dist/connectors/codex-materialize-runner.js +5 -5
  93. package/dist/connectors/index.js +5 -5
  94. package/dist/consolidation-provenance-check.d.ts +1 -1
  95. package/dist/consolidation-undo.d.ts +1 -1
  96. package/dist/contradiction/index.d.ts +1 -1
  97. package/dist/corpus-watermark.js +7 -7
  98. package/dist/entity-retrieval.d.ts +1 -1
  99. package/dist/entity-retrieval.js +5 -5
  100. package/dist/explicit-capture.d.ts +4 -4
  101. package/dist/extraction.js +8 -8
  102. package/dist/index.d.ts +10 -10
  103. package/dist/index.js +35 -35
  104. package/dist/maintenance/memory-governance.js +5 -5
  105. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  106. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  107. package/dist/maintenance/rebuild-memory-projection.d.ts +17 -10
  108. package/dist/maintenance/rebuild-memory-projection.js +6 -6
  109. package/dist/{maintenance-Ha9Dg93M.d.ts → maintenance-Dsvprbod.d.ts} +1 -1
  110. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  111. package/dist/memory-lifecycle-ledger-utils.d.ts +8 -1
  112. package/dist/memory-lifecycle-ledger-utils.js +5 -1
  113. package/dist/memory-worth-outcomes.d.ts +1 -1
  114. package/dist/namespaces/migrate.d.ts +1 -1
  115. package/dist/namespaces/migrate.js +6 -6
  116. package/dist/namespaces/storage.d.ts +1 -1
  117. package/dist/namespaces/storage.js +5 -5
  118. package/dist/offline-sync-impression-drain.js +5 -3
  119. package/dist/operator-doctor-corpus.js +8 -8
  120. package/dist/operator-toolkit.d.ts +1 -1
  121. package/dist/operator-toolkit.js +11 -11
  122. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  123. package/dist/orchestration/maintenance.d.ts +2 -2
  124. package/dist/orchestration/maintenance.js +10 -10
  125. package/dist/{orchestrator-DV_u9bfx.d.ts → orchestrator-CEhod0MJ.d.ts} +3 -3
  126. package/dist/orchestrator.d.ts +4 -4
  127. package/dist/orchestrator.js +35 -35
  128. package/dist/semantic-consolidation.js +6 -6
  129. package/dist/semantic-rule-promotion.js +5 -5
  130. package/dist/semantic-rule-verifier.js +5 -5
  131. package/dist/{service-JxXXY-Eh.d.ts → service-BlOfGid8.d.ts} +1 -1
  132. package/dist/source-agent-qualifier.js +7 -7
  133. package/dist/{storage-BbH4HDUS.d.ts → storage-CNKZocJF.d.ts} +1 -1
  134. package/dist/storage-guards.d.ts +9 -2
  135. package/dist/storage-guards.js +3 -1
  136. package/dist/storage.d.ts +1 -1
  137. package/dist/storage.js +4 -4
  138. package/dist/temporal-supersession.d.ts +1 -1
  139. package/dist/tier-migration.d.ts +1 -1
  140. package/dist/verified-recall.js +5 -5
  141. package/package.json +2 -2
  142. package/src/maintenance/projection-support.ts +192 -1
  143. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +5 -16
  144. package/src/maintenance/rebuild-memory-projection.ts +28 -60
  145. package/src/memory-lifecycle-ledger-utils.ts +21 -0
  146. package/src/orchestration/extraction-run-helpers.ts +47 -0
  147. package/src/orchestration/extraction-run.test.ts +133 -0
  148. package/src/orchestration/extraction-run.ts +11 -7
  149. package/src/orchestration/maintenance.test.ts +30 -0
  150. package/src/orchestrator-flush.test.ts +51 -0
  151. package/src/storage/memory-lifecycle-ledger-access.ts +198 -1
  152. package/src/storage/secure-line-reader.ts +3 -1
  153. package/src/storage-guards.ts +28 -7
  154. package/src/storage-memory-lifecycle-ledger.test.ts +291 -1
  155. package/src/storage.ts +11 -21
  156. package/dist/chunk-DKKZ6SO2.js.map +0 -1
  157. package/dist/chunk-HKF5VASQ.js.map +0 -1
  158. package/dist/chunk-ILGC6JH6.js.map +0 -1
  159. package/dist/chunk-NBAEGFXT.js.map +0 -1
  160. package/dist/chunk-Q3CVY7LV.js.map +0 -1
  161. package/dist/chunk-RYMPLSIT.js.map +0 -1
  162. package/dist/chunk-T3FVAHM7.js.map +0 -1
  163. /package/dist/{chunk-2YJPCRUM.js.map → chunk-4CSTEPFA.js.map} +0 -0
  164. /package/dist/{chunk-A4P6H25D.js.map → chunk-5JUYBWJD.js.map} +0 -0
  165. /package/dist/{chunk-6D5D5A4M.js.map → chunk-7ERHAARG.js.map} +0 -0
  166. /package/dist/{chunk-GORHSN5C.js.map → chunk-7OCTH7AJ.js.map} +0 -0
  167. /package/dist/{chunk-FE7MUX7M.js.map → chunk-AL73FWSS.js.map} +0 -0
  168. /package/dist/{chunk-6TF4M2LB.js.map → chunk-AVGTOOYP.js.map} +0 -0
  169. /package/dist/{chunk-B6XHIBIB.js.map → chunk-AX2J2LC4.js.map} +0 -0
  170. /package/dist/{chunk-ZB3YODID.js.map → chunk-BVJIAIB3.js.map} +0 -0
  171. /package/dist/{chunk-XN3EW6P2.js.map → chunk-CMENJQ6I.js.map} +0 -0
  172. /package/dist/{chunk-PRP3M5Y3.js.map → chunk-CMFUYOYB.js.map} +0 -0
  173. /package/dist/{chunk-M3DBB2ND.js.map → chunk-CNZLKQEL.js.map} +0 -0
  174. /package/dist/{chunk-YVMVVE4B.js.map → chunk-CPD6C6K6.js.map} +0 -0
  175. /package/dist/{chunk-VU7732DU.js.map → chunk-CYM2PHLB.js.map} +0 -0
  176. /package/dist/{chunk-RY2XZTFN.js.map → chunk-DJH35IVS.js.map} +0 -0
  177. /package/dist/{chunk-IJAQCBGV.js.map → chunk-F5EVTFQK.js.map} +0 -0
  178. /package/dist/{chunk-RY7CHVGE.js.map → chunk-G57BGHQ2.js.map} +0 -0
  179. /package/dist/{chunk-F3BNEYX2.js.map → chunk-GUQYMOPU.js.map} +0 -0
  180. /package/dist/{chunk-IMEXE4XS.js.map → chunk-GYGXKATH.js.map} +0 -0
  181. /package/dist/{chunk-CKBKHZKZ.js.map → chunk-JKTHT4KX.js.map} +0 -0
  182. /package/dist/{chunk-HQYYJRU7.js.map → chunk-KJP2R5MJ.js.map} +0 -0
  183. /package/dist/{chunk-UBWOCQ3I.js.map → chunk-MHCRASVK.js.map} +0 -0
  184. /package/dist/{chunk-PYTUBWAS.js.map → chunk-UGAZILZG.js.map} +0 -0
  185. /package/dist/{chunk-4TAD3B56.js.map → chunk-VGK33D3K.js.map} +0 -0
  186. /package/dist/{chunk-BGLDKAYY.js.map → chunk-VK33JSBR.js.map} +0 -0
  187. /package/dist/{chunk-WET4M3R6.js.map → chunk-XSPD6KQL.js.map} +0 -0
  188. /package/dist/{chunk-HUS2XN3T.js.map → chunk-YFHMW37V.js.map} +0 -0
  189. /package/dist/{chunk-5ZGHUWTT.js.map → chunk-ZBDORYWM.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.38.2",
3
+ "version": "9.39.0",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3086,7 +3086,7 @@
3086
3086
  "core"
3087
3087
  ],
3088
3088
  "peerDependencies": {
3089
- "@remnic/coding-graph": "^9.38.2"
3089
+ "@remnic/coding-graph": "^9.39.0"
3090
3090
  },
3091
3091
  "peerDependenciesMeta": {
3092
3092
  "@remnic/coding-graph": {
@@ -6,7 +6,22 @@
6
6
  */
7
7
 
8
8
  import path from "node:path";
9
- import { openProjectionReadonly } from "../memory-projection-store.js";
9
+ import {
10
+ openProjectionReadonly,
11
+ readProjectedEntityMentions,
12
+ readProjectedGovernanceRecord,
13
+ readProjectedNativeKnowledgeChunks,
14
+ type MemoryProjectionGovernanceAppliedActionRow,
15
+ type MemoryProjectionGovernanceReviewQueueRow,
16
+ type ProjectedEntityMentionRow,
17
+ type ProjectedNativeKnowledgeChunkRow,
18
+ } from "../memory-projection-store.js";
19
+ import {
20
+ isFrontmatterDerivedLifecycleEventType,
21
+ memoryLifecycleEventProjectionIdentity,
22
+ } from "../memory-lifecycle-ledger-utils.js";
23
+ import type { MemoryLifecycleEvent } from "../types.js";
24
+ import type { BetterSqlite3Database } from "../runtime/better-sqlite.js";
10
25
 
11
26
  /** Read the projection meta rebuiltAt timestamp; null when absent/unopenable. */
12
27
  export function readProjectionRebuiltAt(memoryDir: string): string | null {
@@ -24,6 +39,172 @@ export function readProjectionRebuiltAt(memoryDir: string): string | null {
24
39
  }
25
40
  }
26
41
 
42
+ /** Format projection age description string for fallback warning telemetry. */
43
+ export function formatProjectionAge(rebuiltAt: string | null): string {
44
+ if (!rebuiltAt) return "projection never rebuilt";
45
+ const ageMs = Date.now() - Date.parse(rebuiltAt);
46
+ if (!Number.isFinite(ageMs)) return `projection rebuiltAt=${rebuiltAt}`;
47
+ const ageMin = Math.max(0, Math.round(ageMs / 60_000));
48
+ const age = ageMin >= 120 ? `${Math.round(ageMin / 60)}h` : `${ageMin}m`;
49
+ return `projection ${age} stale, last rebuilt ${rebuiltAt}`;
50
+ }
51
+
52
+ export interface ProjectionLifecycleLedgerHighWater {
53
+ eventCount: number;
54
+ sourceEventCount: number;
55
+ }
56
+
57
+ function readProjectionHighWaterFromDb(
58
+ db: BetterSqlite3Database,
59
+ ): ProjectionLifecycleLedgerHighWater | null {
60
+ const sourceRow = db.prepare(
61
+ "SELECT value FROM meta WHERE key = 'sourceLifecycleLedgerEventCount'",
62
+ ).get() as { value?: unknown } | undefined;
63
+ const projectedRow = db.prepare(
64
+ "SELECT value FROM meta WHERE key = 'projectedLifecycleLedgerEventCount'",
65
+ ).get() as { value?: unknown } | undefined;
66
+ if (
67
+ typeof sourceRow?.value !== "string"
68
+ || !/^\d+$/.test(sourceRow.value)
69
+ || typeof projectedRow?.value !== "string"
70
+ || !/^\d+$/.test(projectedRow.value)
71
+ ) return null;
72
+ const sourceEventCount = Number(sourceRow.value);
73
+ const eventCount = Number(projectedRow.value);
74
+ if (!Number.isSafeInteger(sourceEventCount) || !Number.isSafeInteger(eventCount)) return null;
75
+ return { eventCount, sourceEventCount };
76
+ }
77
+
78
+ export function readProjectionLifecycleLedgerHighWater(
79
+ memoryDir: string,
80
+ ): ProjectionLifecycleLedgerHighWater | null {
81
+ const db = openProjectionReadonly(memoryDir);
82
+ if (!db) return null;
83
+ try {
84
+ return readProjectionHighWaterFromDb(db);
85
+ } catch {
86
+ return null;
87
+ } finally {
88
+ db.close();
89
+ }
90
+ }
91
+
92
+ export interface ProjectionLifecycleLedgerLagTracker {
93
+ highWater: ProjectionLifecycleLedgerHighWater;
94
+ record(event: MemoryLifecycleEvent): { unique: boolean; projected: boolean };
95
+ close(): void;
96
+ }
97
+
98
+ export function openProjectionLifecycleLedgerLagTracker(
99
+ memoryDir: string,
100
+ ): ProjectionLifecycleLedgerLagTracker | null {
101
+ const db = openProjectionReadonly(memoryDir);
102
+ if (!db) return null;
103
+ try {
104
+ const highWater = readProjectionHighWaterFromDb(db);
105
+ if (!highWater) {
106
+ db.close();
107
+ return null;
108
+ }
109
+ db.pragma("temp_store = FILE");
110
+ db.exec(`
111
+ CREATE TEMP TABLE projection_lag_current_identity (
112
+ identity TEXT PRIMARY KEY
113
+ ) WITHOUT ROWID;
114
+ `);
115
+ db.pragma("temp.cache_size = -1024");
116
+ const insertCurrent = db.prepare(
117
+ "INSERT OR IGNORE INTO projection_lag_current_identity(identity) VALUES (?)",
118
+ );
119
+ const findProjectedEvent = db.prepare(
120
+ "SELECT 1 FROM memory_timeline WHERE event_id = ? LIMIT 1",
121
+ );
122
+ const findProjectedFrontmatterEvent = db.prepare(`
123
+ SELECT 1
124
+ FROM memory_timeline
125
+ WHERE memory_id = ? AND event_type = ? AND timestamp = ?
126
+ LIMIT 1
127
+ `);
128
+ return {
129
+ highWater,
130
+ record(event) {
131
+ if (!event.memoryId.trim()) return { unique: false, projected: false };
132
+ const identity = memoryLifecycleEventProjectionIdentity(event);
133
+ const inserted = insertCurrent.run(identity);
134
+ if (inserted.changes === 0) return { unique: false, projected: false };
135
+ const projected = isFrontmatterDerivedLifecycleEventType(event.eventType)
136
+ ? findProjectedFrontmatterEvent.get(event.memoryId, event.eventType, event.timestamp)
137
+ : findProjectedEvent.get(event.eventId);
138
+ return { unique: true, projected: projected !== undefined };
139
+ },
140
+ close: () => db.close(),
141
+ };
142
+ } catch {
143
+ db.close();
144
+ return null;
145
+ }
146
+ }
147
+ /** Write the high-water ledger marker counts to projection meta store. */
148
+ export function writeProjectionMetaHighWater(
149
+ insertMeta: { run: (key: string, value: string) => void },
150
+ sourceLifecycleLedgerEventCount: number,
151
+ timelineRows: MemoryLifecycleEvent[],
152
+ ): void {
153
+ insertMeta.run("sourceLifecycleLedgerEventCount", String(sourceLifecycleLedgerEventCount));
154
+ insertMeta.run(
155
+ "projectedLifecycleLedgerEventCount",
156
+ String(new Set(timelineRows.map(memoryLifecycleEventProjectionIdentity)).size),
157
+ );
158
+ }
159
+
160
+ export function readProjectedEntityMentionRows(
161
+ memoryDir: string,
162
+ ): { projectionExists: boolean; rows: ProjectedEntityMentionRow[] } {
163
+ const rows = readProjectedEntityMentions(memoryDir);
164
+ if (rows === null) return { projectionExists: false, rows: [] };
165
+ return { projectionExists: true, rows };
166
+ }
167
+
168
+ export function readProjectedNativeKnowledgeRows(
169
+ memoryDir: string,
170
+ ): { projectionExists: boolean; rows: ProjectedNativeKnowledgeChunkRow[] } {
171
+ const rows = readProjectedNativeKnowledgeChunks(memoryDir);
172
+ if (rows === null) return { projectionExists: false, rows: [] };
173
+ return { projectionExists: true, rows };
174
+ }
175
+
176
+ export function readProjectedGovernanceRows(memoryDir: string): {
177
+ projectionExists: boolean;
178
+ runId: string | null;
179
+ summary: unknown;
180
+ metrics: unknown;
181
+ reviewQueueRows: MemoryProjectionGovernanceReviewQueueRow[];
182
+ appliedActionRows: MemoryProjectionGovernanceAppliedActionRow[];
183
+ report: string;
184
+ } {
185
+ const record = readProjectedGovernanceRecord(memoryDir);
186
+ if (record === null) {
187
+ return {
188
+ projectionExists: false,
189
+ runId: null,
190
+ summary: undefined,
191
+ metrics: undefined,
192
+ reviewQueueRows: [],
193
+ appliedActionRows: [],
194
+ report: "",
195
+ };
196
+ }
197
+ return {
198
+ projectionExists: true,
199
+ runId: record.runId,
200
+ summary: record.summary,
201
+ metrics: record.metrics,
202
+ reviewQueueRows: record.reviewQueueRows,
203
+ appliedActionRows: record.appliedActionRows,
204
+ report: record.report,
205
+ };
206
+ }
207
+
27
208
  /**
28
209
  * Assert an optionally injected storage (e.g. the daemon's live unlocked
29
210
  * secure-store instance) is rooted at `memoryDir`, and hand it back. Shared
@@ -54,3 +235,13 @@ export interface SkippedDuplicateMemory {
54
235
  export interface SkippedBlankIdMemory {
55
236
  path: string;
56
237
  }
238
+ export interface SkippedDuplicateTimelineEvent {
239
+ eventId: string;
240
+ keptPath: string;
241
+ skippedPath: string;
242
+ }
243
+
244
+ export interface SkippedBlankIdTimelineEvent {
245
+ eventId: string;
246
+ path: string;
247
+ }
@@ -11,23 +11,14 @@ import {
11
11
  buildLifecycleEventsForMemory,
12
12
  compareMemoryLifecycleEvents,
13
13
  sortMemoryLifecycleEvents,
14
+ isFrontmatterDerivedLifecycleEventType,
15
+ memoryLifecycleEventProjectionIdentity,
14
16
  memoryLifecycleLedgerLockPath,
15
17
  MEMORY_LIFECYCLE_LEDGER_LOCK_STALE_MS,
16
18
  } from "../memory-lifecycle-ledger-utils.js";
17
19
  import { withHeldFileLock, type HeldFileLockOptions, type HeldFileLockController } from "../utils/serialize-mutations.js";
18
20
  import { probeEncryptedRegularFileHeader } from "../secure-store/secure-fs.js";
19
21
 
20
- /**
21
- * Event types `buildLifecycleEventsForMemory` reconstructs from frontmatter.
22
- * Everything else in the ledger is append-only history with no frontmatter
23
- * equivalent and must be carried over by a preserving rebuild (issue #1910).
24
- */
25
- const FRONTMATTER_DERIVED_EVENT_TYPES: Record<string, true> = {
26
- created: true,
27
- updated: true,
28
- superseded: true,
29
- archived: true,
30
- };
31
22
 
32
23
  export interface RebuildMemoryLifecycleLedgerOptions {
33
24
  memoryDir: string;
@@ -322,17 +313,15 @@ export async function rebuildMemoryLifecycleLedger(
322
313
  // retried append of the same logical event still collapses to one. Rows
323
314
  // that are not frontmatter-derived are append-only history with no
324
315
  // reconstruction — always carried over, deduped by eventId in the merge.
325
- const contentKey = (event: MemoryLifecycleEvent): string =>
326
- `${event.memoryId}\u0000${event.eventType}\u0000${event.timestamp}`;
327
- const reconstructedKeys = new Set(events.map(contentKey));
316
+ const reconstructedKeys = new Set(events.map(memoryLifecycleEventProjectionIdentity));
328
317
  const racedFrontmatterByKey = new Map<string, MemoryLifecycleEvent>();
329
318
  const appendOnly: MemoryLifecycleEvent[] = [];
330
319
  for (const event of existing) {
331
- if (!FRONTMATTER_DERIVED_EVENT_TYPES[event.eventType]) {
320
+ if (!isFrontmatterDerivedLifecycleEventType(event.eventType)) {
332
321
  appendOnly.push(event);
333
322
  continue;
334
323
  }
335
- const key = contentKey(event);
324
+ const key = memoryLifecycleEventProjectionIdentity(event);
336
325
  if (reconstructedKeys.has(key)) continue; // collapsed into reconstruction
337
326
  if (!racedFrontmatterByKey.has(key)) racedFrontmatterByKey.set(key, event);
338
327
  }
@@ -18,6 +18,7 @@ import {
18
18
  buildLifecycleEventsForMemory,
19
19
  inferMemoryStatus,
20
20
  MEMORY_LIFECYCLE_EVENT_SORT_ORDER,
21
+ memoryLifecycleEventProjectionIdentity,
21
22
  sortMemoryLifecycleEvents,
22
23
  toMemoryPathRel,
23
24
  } from "../memory-lifecycle-ledger-utils.js";
@@ -55,20 +56,26 @@ export interface RebuildMemoryProjectionOptions {
55
56
  updatedBefore?: string;
56
57
  }
57
58
 
58
- export type { SkippedBlankIdMemory, SkippedDuplicateMemory } from "./projection-support.js";
59
- import { assertInjectedStorageRooted } from "./projection-support.js";
60
- import type { SkippedBlankIdMemory, SkippedDuplicateMemory } from "./projection-support.js";
61
-
62
- export interface SkippedDuplicateTimelineEvent {
63
- eventId: string;
64
- keptPath: string;
65
- skippedPath: string;
66
- }
59
+ export type {
60
+ SkippedBlankIdMemory,
61
+ SkippedBlankIdTimelineEvent,
62
+ SkippedDuplicateMemory,
63
+ SkippedDuplicateTimelineEvent,
64
+ } from "./projection-support.js";
65
+ import {
66
+ assertInjectedStorageRooted,
67
+ readProjectedEntityMentionRows,
68
+ readProjectedGovernanceRows,
69
+ readProjectedNativeKnowledgeRows,
70
+ writeProjectionMetaHighWater,
71
+ } from "./projection-support.js";
72
+ import type {
73
+ SkippedBlankIdMemory,
74
+ SkippedBlankIdTimelineEvent,
75
+ SkippedDuplicateMemory,
76
+ SkippedDuplicateTimelineEvent,
77
+ } from "./projection-support.js";
67
78
 
68
- export interface SkippedBlankIdTimelineEvent {
69
- eventId: string;
70
- path: string;
71
- }
72
79
 
73
80
  export interface RebuildMemoryProjectionResult {
74
81
  dryRun: boolean;
@@ -569,6 +576,7 @@ async function loadAuthoritativeProjectionSnapshot(options: {
569
576
  }
570
577
  | null;
571
578
  usedLifecycleLedger: boolean;
579
+ sourceLifecycleLedgerEventCount: number;
572
580
  scope: {
573
581
  updatedAfter: string | null;
574
582
  updatedBefore: string | null;
@@ -607,6 +615,9 @@ async function loadAuthoritativeProjectionSnapshot(options: {
607
615
  );
608
616
  const events = deduplicatedTimeline.events;
609
617
  const usedLifecycleLedger = timeline.usedLifecycleLedger;
618
+ const sourceLifecycleLedgerEventCount = new Set(
619
+ lifecycleEvents.map(memoryLifecycleEventProjectionIdentity),
620
+ ).size;
610
621
  const currentRows = projectionMemories.map((memory) => toCurrentStateRow(options.memoryDir, memory));
611
622
  const scope = normalizeProjectionScope(options);
612
623
  const scopedMemories = filterMemoriesForProjectionScope(projectionMemories, scope);
@@ -645,6 +656,7 @@ async function loadAuthoritativeProjectionSnapshot(options: {
645
656
  nativeKnowledgeRows,
646
657
  governance,
647
658
  usedLifecycleLedger,
659
+ sourceLifecycleLedgerEventCount,
648
660
  scope,
649
661
  skippedDuplicateMemories: selected.skippedDuplicateMemories,
650
662
  skippedBlankIdMemories: selected.skippedBlankIdMemories,
@@ -733,53 +745,6 @@ function readProjectedTimelineRows(
733
745
  }
734
746
  }
735
747
 
736
- function readProjectedEntityMentionRows(
737
- memoryDir: string,
738
- ): { projectionExists: boolean; rows: ProjectedEntityMentionRow[] } {
739
- const rows = readProjectedEntityMentions(memoryDir);
740
- if (rows === null) return { projectionExists: false, rows: [] };
741
- return { projectionExists: true, rows };
742
- }
743
-
744
- function readProjectedNativeKnowledgeRows(
745
- memoryDir: string,
746
- ): { projectionExists: boolean; rows: ProjectedNativeKnowledgeChunkRow[] } {
747
- const rows = readProjectedNativeKnowledgeChunks(memoryDir);
748
- if (rows === null) return { projectionExists: false, rows: [] };
749
- return { projectionExists: true, rows };
750
- }
751
-
752
- function readProjectedGovernanceRows(memoryDir: string): {
753
- projectionExists: boolean;
754
- runId: string | null;
755
- summary: unknown;
756
- metrics: unknown;
757
- reviewQueueRows: MemoryProjectionGovernanceReviewQueueRow[];
758
- appliedActionRows: MemoryProjectionGovernanceAppliedActionRow[];
759
- report: string;
760
- } {
761
- const record = readProjectedGovernanceRecord(memoryDir);
762
- if (record === null) {
763
- return {
764
- projectionExists: false,
765
- runId: null,
766
- summary: undefined,
767
- metrics: undefined,
768
- reviewQueueRows: [],
769
- appliedActionRows: [],
770
- report: "",
771
- };
772
- }
773
- return {
774
- projectionExists: true,
775
- runId: record.runId,
776
- summary: record.summary,
777
- metrics: record.metrics,
778
- reviewQueueRows: record.reviewQueueRows,
779
- appliedActionRows: record.appliedActionRows,
780
- report: record.report,
781
- };
782
- }
783
748
 
784
749
  function writeProjectionDb(
785
750
  dbPath: string,
@@ -790,6 +755,7 @@ function writeProjectionDb(
790
755
  nativeKnowledgeRows: ProjectedNativeKnowledgeChunkRow[],
791
756
  governance: Awaited<ReturnType<typeof loadLatestGovernanceProjection>>,
792
757
  usedLifecycleLedger: boolean,
758
+ sourceLifecycleLedgerEventCount: number,
793
759
  ): void {
794
760
  const db = openBetterSqlite3(dbPath);
795
761
  const memoryDir = path.dirname(path.dirname(dbPath));
@@ -800,6 +766,7 @@ function writeProjectionDb(
800
766
  insertMeta.run("schemaVersion", String(MEMORY_PROJECTION_SCHEMA_VERSION));
801
767
  insertMeta.run("rebuiltAt", nowIso);
802
768
  insertMeta.run("usedLifecycleLedger", usedLifecycleLedger ? "true" : "false");
769
+ writeProjectionMetaHighWater(insertMeta, sourceLifecycleLedgerEventCount, timelineRows);
803
770
  insertMeta.run("latestGovernanceRunId", governance?.runId ?? "");
804
771
 
805
772
  const insertCurrent = db.prepare(`
@@ -1161,6 +1128,7 @@ export async function rebuildMemoryProjection(
1161
1128
  nextNativeKnowledgeRows,
1162
1129
  nextGovernance,
1163
1130
  snapshot.usedLifecycleLedger,
1131
+ snapshot.sourceLifecycleLedgerEventCount,
1164
1132
  );
1165
1133
  backupPath = await backupExistingProjection(options.memoryDir, outputPath, now);
1166
1134
  backupPath = await commitPreparedFileAtomically(tempPath, outputPath, backupPath);
@@ -24,6 +24,27 @@ export const MEMORY_LIFECYCLE_EVENT_SORT_ORDER: Record<MemoryLifecycleEventType,
24
24
  archived: 10,
25
25
  };
26
26
 
27
+ const FRONTMATTER_DERIVED_EVENT_TYPES: Record<string, true> = {
28
+ created: true,
29
+ updated: true,
30
+ superseded: true,
31
+ archived: true,
32
+ };
33
+
34
+ export function isFrontmatterDerivedLifecycleEventType(eventType: string): boolean {
35
+ return FRONTMATTER_DERIVED_EVENT_TYPES[eventType] === true;
36
+ }
37
+
38
+ export function memoryLifecycleEventProjectionIdentity(event: {
39
+ eventId: string;
40
+ memoryId: string;
41
+ eventType: string;
42
+ timestamp: string;
43
+ }): string {
44
+ if (!isFrontmatterDerivedLifecycleEventType(event.eventType)) return `event\u0000${event.eventId}`;
45
+ return `frontmatter\u0000${event.memoryId}\u0000${event.eventType}\u0000${event.timestamp}`;
46
+ }
47
+
27
48
  /**
28
49
  * Deterministic sort rank for a lifecycle event type. The ledger readers admit
29
50
  * any structurally valid row (permissive `eventType`, issue #1910), so an
@@ -307,3 +307,50 @@ export async function runExtractionPostPersist(
307
307
  if (deadlineError) throw deadlineError;
308
308
  return { meta, metadataFailed };
309
309
  }
310
+
311
+ /**
312
+ * Advance the extraction liveness watermark for a successfully parsed empty
313
+ * extraction (#2223) and record its processed fingerprint when applicable.
314
+ * Fingerprinted/import runs treat the metadata save as load-bearing (dedupe
315
+ * state) and propagate failures; normal live runs treat the stamp as
316
+ * diagnostic liveness and fail open. Deadline errors always propagate.
317
+ */
318
+ export async function commitEmptySuccessExtraction(args: {
319
+ storage: StorageManager;
320
+ meta: MetaState | null;
321
+ extractionFingerprint: string | null;
322
+ shouldPersistProcessedFingerprint: boolean;
323
+ hasExtractionFailure: boolean;
324
+ recordProcessedExtractionFingerprint: (storage: StorageManager, fingerprint: string, meta: MetaState) => Promise<unknown>;
325
+ runDeadlineAware: <T>(operation: () => Promise<T>, phase: string, clearTimerOnError?: boolean) => Promise<T>;
326
+ isDeadlineError: (error: unknown) => boolean;
327
+ }): Promise<void> {
328
+ const {
329
+ storage,
330
+ meta: existingMeta,
331
+ extractionFingerprint,
332
+ shouldPersistProcessedFingerprint,
333
+ hasExtractionFailure,
334
+ recordProcessedExtractionFingerprint,
335
+ runDeadlineAware,
336
+ isDeadlineError,
337
+ } = args;
338
+ let meta = existingMeta;
339
+ if (extractionFingerprint && shouldPersistProcessedFingerprint && !hasExtractionFailure) {
340
+ meta ??= await storage.loadMeta();
341
+ await recordProcessedExtractionFingerprint(storage, extractionFingerprint, meta);
342
+ }
343
+ if (!hasExtractionFailure) {
344
+ const livenessStampFatal = shouldPersistProcessedFingerprint;
345
+ try {
346
+ meta ??= await runDeadlineAware(() => storage.loadMeta(), "during_empty_success_liveness_load", false);
347
+ meta.extractionCount += 1;
348
+ meta.lastExtractionAt = new Date().toISOString();
349
+ await runDeadlineAware(() => storage.saveMeta(meta!), "during_empty_success_liveness_save", false);
350
+ } catch (err) {
351
+ if (isDeadlineError(err)) throw err;
352
+ if (livenessStampFatal) throw err;
353
+ log.warn("runExtraction: failed to stamp empty-success liveness watermark (non-fatal)", err);
354
+ }
355
+ }
356
+ }
@@ -980,3 +980,136 @@ test("namespace isolation: the provider breaker suppresses extraction across nam
980
980
  await h.cleanup();
981
981
  }
982
982
  });
983
+
984
+
985
+ // ---------------------------------------------------------------------------
986
+ // Extraction liveness watermark (#2223): a successfully parsed extraction must
987
+ // advance lastExtractionAt even when it emits no durable objects. Normal live
988
+ // turns do not set persistProcessedFingerprint, which is the gap case.
989
+ // ---------------------------------------------------------------------------
990
+
991
+ function emptySuccessResult(): ExtractionResult {
992
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
993
+ }
994
+
995
+ // Normal live turns (TurnIngestionCoordinator.processTurn path) do NOT set
996
+ // persistProcessedFingerprint — the case the pre-#2223 watermark stamp skipped.
997
+ function liveTurns(content: string): BufferTurn[] {
998
+ return [
999
+ { role: "user", content: `u:${content}`, timestamp: "2026-07-15T00:00:00Z" },
1000
+ { role: "assistant", content: `a:${content}`, timestamp: "2026-07-15T00:00:01Z" },
1001
+ ] as BufferTurn[];
1002
+ }
1003
+
1004
+ test("extraction liveness (#2223): a normal live empty success advances lastExtractionAt without persisting memories", async () => {
1005
+ const h = await makeHarness();
1006
+ try {
1007
+ const coord = h.newCoordinator();
1008
+ h.setRespond(() => emptySuccessResult());
1009
+ const storage = await h.storageForNs("default");
1010
+ const before = await storage.loadMeta();
1011
+ const baselineCount = before.extractionCount;
1012
+
1013
+ const result = await coord.runExtraction(liveTurns("nothing-durable-here"), {
1014
+ skipCharThreshold: true,
1015
+ skipUserTurnThreshold: true,
1016
+ clearBufferAfterExtraction: false,
1017
+ bufferKey: "nothing-durable-here",
1018
+ });
1019
+
1020
+ assert.equal(result.status, "skipped");
1021
+ assert.equal(result.reason, "empty_extraction_result");
1022
+ assert.equal(h.persistCalls(), 0, "no durable memories persisted for an empty result");
1023
+
1024
+ const after = await storage.loadMeta();
1025
+ assert.equal(after.extractionCount, baselineCount + 1, "empty success increments extractionCount");
1026
+ assert.ok(after.lastExtractionAt, "empty success stamps lastExtractionAt");
1027
+ assert.equal(h.recordedProcessedCount(), 0, "normal live turns never record a processed fingerprint");
1028
+ } finally {
1029
+ await h.cleanup();
1030
+ }
1031
+ });
1032
+
1033
+ test("extraction liveness (#2223): a fingerprinted empty success advances the watermark once and records the fingerprint once", async () => {
1034
+ const h = await makeHarness();
1035
+ try {
1036
+ const coord = h.newCoordinator();
1037
+ h.setRespond(() => emptySuccessResult());
1038
+ const storage = await h.storageForNs("default");
1039
+ const before = await storage.loadMeta();
1040
+ const baselineCount = before.extractionCount;
1041
+
1042
+ const result = await coord.runExtraction(makeTurns("fingerprinted-empty"), {
1043
+ skipCharThreshold: true,
1044
+ skipUserTurnThreshold: true,
1045
+ clearBufferAfterExtraction: false,
1046
+ bufferKey: "fingerprinted-empty",
1047
+ });
1048
+
1049
+ assert.equal(result.status, "skipped");
1050
+ assert.equal(result.reason, "empty_extraction_result");
1051
+ assert.equal(h.persistCalls(), 0);
1052
+
1053
+ const after = await storage.loadMeta();
1054
+ assert.equal(after.extractionCount, baselineCount + 1, "fingerprinted empty success increments extractionCount exactly once");
1055
+ assert.ok(after.lastExtractionAt, "fingerprinted empty success stamps lastExtractionAt");
1056
+ assert.equal(h.recordedProcessedCount(), 1, "processed fingerprint recorded exactly once");
1057
+ } finally {
1058
+ await h.cleanup();
1059
+ }
1060
+ });
1061
+
1062
+ test("extraction liveness (#2223): a non-empty success advances lastExtractionAt", async () => {
1063
+ const h = await makeHarness();
1064
+ try {
1065
+ const coord = h.newCoordinator();
1066
+ h.setRespond(() => successResult());
1067
+ const storage = await h.storageForNs("default");
1068
+ const before = await storage.loadMeta();
1069
+ const baselineCount = before.extractionCount;
1070
+
1071
+ const result = await coord.runExtraction(makeTurns("durable-fact"), {
1072
+ skipCharThreshold: true,
1073
+ skipUserTurnThreshold: true,
1074
+ clearBufferAfterExtraction: false,
1075
+ bufferKey: "durable-fact",
1076
+ });
1077
+
1078
+ assert.equal(result.status, "completed");
1079
+ assert.ok(h.persistCalls() >= 1);
1080
+
1081
+ const after = await storage.loadMeta();
1082
+ assert.equal(after.extractionCount, baselineCount + 1, "non-empty success increments extractionCount");
1083
+ assert.ok(after.lastExtractionAt, "non-empty success stamps lastExtractionAt");
1084
+ } finally {
1085
+ await h.cleanup();
1086
+ }
1087
+ });
1088
+
1089
+ test("extraction liveness (#2223): a provider failure does not advance lastExtractionAt", async () => {
1090
+ const h = await makeHarness({ extractionBreakerFailureThreshold: 100 });
1091
+ try {
1092
+ const coord = h.newCoordinator();
1093
+ h.setRespond(() => failureResult("provider_retryable"));
1094
+ const storage = await h.storageForNs("default");
1095
+ const before = await storage.loadMeta();
1096
+ const baselineWatermark = before.lastExtractionAt;
1097
+ const baselineCount = before.extractionCount;
1098
+
1099
+ const result = await coord.runExtraction(makeTurns("failing-extraction"), {
1100
+ skipCharThreshold: true,
1101
+ skipUserTurnThreshold: true,
1102
+ clearBufferAfterExtraction: false,
1103
+ bufferKey: "failing-extraction",
1104
+ });
1105
+
1106
+ assert.equal(result.status, "skipped");
1107
+ assert.equal(result.reason, "empty_extraction_result");
1108
+
1109
+ const after = await storage.loadMeta();
1110
+ assert.equal(after.lastExtractionAt, baselineWatermark, "failure must not stamp lastExtractionAt");
1111
+ assert.equal(after.extractionCount, baselineCount, "failure must not increment extractionCount");
1112
+ } finally {
1113
+ await h.cleanup();
1114
+ }
1115
+ });
@@ -127,6 +127,7 @@ import { raceRecallAbort, throwIfRecallAborted } from "./orchestrator-helpers.js
127
127
  import {
128
128
  capExtractionRetryStateEntries,
129
129
  combineExtractionAbortSignals,
130
+ commitEmptySuccessExtraction,
130
131
  computeExtractionRetryNextEligibleMs,
131
132
  deriveSourceConnector,
132
133
  deriveTopicsFromExtraction,
@@ -1015,13 +1016,16 @@ export class ExtractionRunCoordinator {
1015
1016
  { extractionFailure }
1016
1017
  );
1017
1018
  }
1018
- if (extractionFingerprint && shouldPersistProcessedFingerprint && !extractionFailure) {
1019
- meta ??= await storage.loadMeta();
1020
- await this.deps.recordProcessedExtractionFingerprint(storage, extractionFingerprint, meta);
1021
- meta.extractionCount += 1;
1022
- meta.lastExtractionAt = new Date().toISOString();
1023
- await storage.saveMeta(meta);
1024
- }
1019
+ await commitEmptySuccessExtraction({
1020
+ storage,
1021
+ meta,
1022
+ extractionFingerprint,
1023
+ shouldPersistProcessedFingerprint,
1024
+ hasExtractionFailure: Boolean(extractionFailure),
1025
+ recordProcessedExtractionFingerprint: this.deps.recordProcessedExtractionFingerprint,
1026
+ runDeadlineAware,
1027
+ isDeadlineError: (error) => error instanceof ExtractionDeadlineError,
1028
+ });
1025
1029
  // Correction-only turns that meet char/user-turn thresholds but yield
1026
1030
  // zero facts still need passive capture (review: "empty extraction skips
1027
1031
  // capture"). selfNamespace/principal already resolved above.