@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.
- package/dist/access-admin-ops-surface.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +13 -13
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +14 -14
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +13 -13
- package/dist/access-cli.js +35 -35
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +13 -13
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +18 -18
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +13 -13
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +13 -13
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +16 -16
- package/dist/access-namespace-preflight.js +13 -13
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +13 -13
- package/dist/access-operations-batch.js +14 -14
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +15 -15
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +13 -13
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +13 -13
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +13 -13
- package/dist/{access-service-C2WYT1pO.d.ts → access-service-C6QuKBCG.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +13 -13
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/buffer.d.ts +1 -1
- package/dist/buffer.js +2 -2
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-2YJPCRUM.js → chunk-4CSTEPFA.js} +2 -2
- package/dist/{chunk-T3FVAHM7.js → chunk-4VXGRIH6.js} +286 -7
- package/dist/chunk-4VXGRIH6.js.map +1 -0
- package/dist/{chunk-A4P6H25D.js → chunk-5JUYBWJD.js} +19 -19
- package/dist/{chunk-JYLN6KHH.js → chunk-5TRIAPT4.js} +7 -9
- package/dist/{chunk-JYLN6KHH.js.map → chunk-5TRIAPT4.js.map} +1 -1
- package/dist/{chunk-6D5D5A4M.js → chunk-7ERHAARG.js} +2 -2
- package/dist/{chunk-GORHSN5C.js → chunk-7OCTH7AJ.js} +2 -2
- package/dist/{chunk-ILGC6JH6.js → chunk-7RJUVTVF.js} +16 -1
- package/dist/chunk-7RJUVTVF.js.map +1 -0
- package/dist/{chunk-FE7MUX7M.js → chunk-AL73FWSS.js} +2 -2
- package/dist/{chunk-6TF4M2LB.js → chunk-AVGTOOYP.js} +2 -2
- package/dist/{chunk-B6XHIBIB.js → chunk-AX2J2LC4.js} +2 -2
- package/dist/{chunk-ZB3YODID.js → chunk-BVJIAIB3.js} +8 -8
- package/dist/{chunk-XN3EW6P2.js → chunk-CMENJQ6I.js} +2 -2
- package/dist/{chunk-PRP3M5Y3.js → chunk-CMFUYOYB.js} +2 -2
- package/dist/{chunk-M3DBB2ND.js → chunk-CNZLKQEL.js} +2 -2
- package/dist/{chunk-YVMVVE4B.js → chunk-CPD6C6K6.js} +2 -2
- package/dist/{chunk-VU7732DU.js → chunk-CYM2PHLB.js} +5 -5
- package/dist/{chunk-RY2XZTFN.js → chunk-DJH35IVS.js} +2 -2
- package/dist/{chunk-Q3CVY7LV.js → chunk-ENLZOWYK.js} +201 -231
- package/dist/chunk-ENLZOWYK.js.map +1 -0
- package/dist/{chunk-IJAQCBGV.js → chunk-F5EVTFQK.js} +2 -2
- package/dist/{chunk-RY7CHVGE.js → chunk-G57BGHQ2.js} +2 -2
- package/dist/{chunk-F3BNEYX2.js → chunk-GUQYMOPU.js} +2 -2
- package/dist/{chunk-IMEXE4XS.js → chunk-GYGXKATH.js} +6 -6
- package/dist/{chunk-CKBKHZKZ.js → chunk-JKTHT4KX.js} +3 -3
- package/dist/{chunk-HQYYJRU7.js → chunk-KJP2R5MJ.js} +15 -15
- package/dist/{chunk-UBWOCQ3I.js → chunk-MHCRASVK.js} +2 -2
- package/dist/{chunk-RYMPLSIT.js → chunk-NF3U3MPG.js} +41 -8
- package/dist/chunk-NF3U3MPG.js.map +1 -0
- package/dist/{chunk-DKKZ6SO2.js → chunk-SHTPVI47.js} +22 -45
- package/dist/chunk-SHTPVI47.js.map +1 -0
- package/dist/{chunk-PYTUBWAS.js → chunk-UGAZILZG.js} +2 -2
- package/dist/{chunk-4TAD3B56.js → chunk-VGK33D3K.js} +2 -2
- package/dist/{chunk-BGLDKAYY.js → chunk-VK33JSBR.js} +2 -2
- package/dist/{chunk-NBAEGFXT.js → chunk-VQTRSX4R.js} +11 -14
- package/dist/chunk-VQTRSX4R.js.map +1 -0
- package/dist/{chunk-WET4M3R6.js → chunk-XSPD6KQL.js} +2 -2
- package/dist/{chunk-HUS2XN3T.js → chunk-YFHMW37V.js} +2 -2
- package/dist/{chunk-HKF5VASQ.js → chunk-ZA53AZND.js} +17 -6
- package/dist/chunk-ZA53AZND.js.map +1 -0
- package/dist/{chunk-5ZGHUWTT.js → chunk-ZBDORYWM.js} +2 -2
- package/dist/{cli-jkmzdT8P.d.ts → cli-ClXRCu08.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +27 -27
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +5 -5
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/corpus-watermark.js +7 -7
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction.js +8 -8
- package/dist/index.d.ts +10 -10
- package/dist/index.js +35 -35
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.d.ts +17 -10
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/{maintenance-Ha9Dg93M.d.ts → maintenance-Dsvprbod.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-lifecycle-ledger-utils.d.ts +8 -1
- package/dist/memory-lifecycle-ledger-utils.js +5 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +5 -5
- package/dist/offline-sync-impression-drain.js +5 -3
- package/dist/operator-doctor-corpus.js +8 -8
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +11 -11
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -10
- package/dist/{orchestrator-DV_u9bfx.d.ts → orchestrator-CEhod0MJ.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +35 -35
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/{service-JxXXY-Eh.d.ts → service-BlOfGid8.d.ts} +1 -1
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/{storage-BbH4HDUS.d.ts → storage-CNKZocJF.d.ts} +1 -1
- package/dist/storage-guards.d.ts +9 -2
- package/dist/storage-guards.js +3 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/maintenance/projection-support.ts +192 -1
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +5 -16
- package/src/maintenance/rebuild-memory-projection.ts +28 -60
- package/src/memory-lifecycle-ledger-utils.ts +21 -0
- package/src/orchestration/extraction-run-helpers.ts +47 -0
- package/src/orchestration/extraction-run.test.ts +133 -0
- package/src/orchestration/extraction-run.ts +11 -7
- package/src/orchestration/maintenance.test.ts +30 -0
- package/src/orchestrator-flush.test.ts +51 -0
- package/src/storage/memory-lifecycle-ledger-access.ts +198 -1
- package/src/storage/secure-line-reader.ts +3 -1
- package/src/storage-guards.ts +28 -7
- package/src/storage-memory-lifecycle-ledger.test.ts +291 -1
- package/src/storage.ts +11 -21
- package/dist/chunk-DKKZ6SO2.js.map +0 -1
- package/dist/chunk-HKF5VASQ.js.map +0 -1
- package/dist/chunk-ILGC6JH6.js.map +0 -1
- package/dist/chunk-NBAEGFXT.js.map +0 -1
- package/dist/chunk-Q3CVY7LV.js.map +0 -1
- package/dist/chunk-RYMPLSIT.js.map +0 -1
- package/dist/chunk-T3FVAHM7.js.map +0 -1
- /package/dist/{chunk-2YJPCRUM.js.map → chunk-4CSTEPFA.js.map} +0 -0
- /package/dist/{chunk-A4P6H25D.js.map → chunk-5JUYBWJD.js.map} +0 -0
- /package/dist/{chunk-6D5D5A4M.js.map → chunk-7ERHAARG.js.map} +0 -0
- /package/dist/{chunk-GORHSN5C.js.map → chunk-7OCTH7AJ.js.map} +0 -0
- /package/dist/{chunk-FE7MUX7M.js.map → chunk-AL73FWSS.js.map} +0 -0
- /package/dist/{chunk-6TF4M2LB.js.map → chunk-AVGTOOYP.js.map} +0 -0
- /package/dist/{chunk-B6XHIBIB.js.map → chunk-AX2J2LC4.js.map} +0 -0
- /package/dist/{chunk-ZB3YODID.js.map → chunk-BVJIAIB3.js.map} +0 -0
- /package/dist/{chunk-XN3EW6P2.js.map → chunk-CMENJQ6I.js.map} +0 -0
- /package/dist/{chunk-PRP3M5Y3.js.map → chunk-CMFUYOYB.js.map} +0 -0
- /package/dist/{chunk-M3DBB2ND.js.map → chunk-CNZLKQEL.js.map} +0 -0
- /package/dist/{chunk-YVMVVE4B.js.map → chunk-CPD6C6K6.js.map} +0 -0
- /package/dist/{chunk-VU7732DU.js.map → chunk-CYM2PHLB.js.map} +0 -0
- /package/dist/{chunk-RY2XZTFN.js.map → chunk-DJH35IVS.js.map} +0 -0
- /package/dist/{chunk-IJAQCBGV.js.map → chunk-F5EVTFQK.js.map} +0 -0
- /package/dist/{chunk-RY7CHVGE.js.map → chunk-G57BGHQ2.js.map} +0 -0
- /package/dist/{chunk-F3BNEYX2.js.map → chunk-GUQYMOPU.js.map} +0 -0
- /package/dist/{chunk-IMEXE4XS.js.map → chunk-GYGXKATH.js.map} +0 -0
- /package/dist/{chunk-CKBKHZKZ.js.map → chunk-JKTHT4KX.js.map} +0 -0
- /package/dist/{chunk-HQYYJRU7.js.map → chunk-KJP2R5MJ.js.map} +0 -0
- /package/dist/{chunk-UBWOCQ3I.js.map → chunk-MHCRASVK.js.map} +0 -0
- /package/dist/{chunk-PYTUBWAS.js.map → chunk-UGAZILZG.js.map} +0 -0
- /package/dist/{chunk-4TAD3B56.js.map → chunk-VGK33D3K.js.map} +0 -0
- /package/dist/{chunk-BGLDKAYY.js.map → chunk-VK33JSBR.js.map} +0 -0
- /package/dist/{chunk-WET4M3R6.js.map → chunk-XSPD6KQL.js.map} +0 -0
- /package/dist/{chunk-HUS2XN3T.js.map → chunk-YFHMW37V.js.map} +0 -0
- /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.
|
|
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.
|
|
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 {
|
|
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
|
|
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 (!
|
|
320
|
+
if (!isFrontmatterDerivedLifecycleEventType(event.eventType)) {
|
|
332
321
|
appendOnly.push(event);
|
|
333
322
|
continue;
|
|
334
323
|
}
|
|
335
|
-
const key =
|
|
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 {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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.
|