@remnic/core 10.0.94 → 10.0.95
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.js +1 -1
- package/dist/access-cli.js +1 -1
- package/dist/access-extraction-force-flush.js +1 -1
- package/dist/access-http-briefing.js +1 -1
- package/dist/access-http-query.js +1 -1
- package/dist/access-http.js +1 -1
- package/dist/access-identity-continuity-surface.js +1 -1
- package/dist/access-lcm-surface.js +1 -1
- package/dist/access-mcp.js +1 -1
- package/dist/access-observe-write-surface.js +1 -1
- package/dist/access-operations-batch.js +1 -1
- package/dist/access-operations.js +1 -1
- package/dist/access-recall-concurrency.js +1 -1
- package/dist/access-recall-response.js +1 -1
- package/dist/access-recall-surface.js +1 -1
- package/dist/access-service.js +1 -1
- package/dist/{chunk-D5S6RL32.js → chunk-DDATG2SF.js} +40 -20
- package/dist/{chunk-D5S6RL32.js.map → chunk-DDATG2SF.js.map} +1 -1
- package/dist/{chunk-RUTN5MMC.js → chunk-LS7PT3YD.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/explicit-capture.js +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/backlog-scheduler.js +2 -2
- package/dist/orchestration/buffer-drain-runner.js +2 -2
- package/dist/orchestration/extraction-persist-failure.d.ts +25 -1
- package/dist/orchestration/extraction-persist-failure.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts +3 -6
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestrator.js +1 -1
- package/dist/recall-navigation.js +1 -1
- package/package.json +2 -2
- package/src/orchestration/extraction-persist-failure.test.ts +90 -0
- package/src/orchestration/extraction-persist-failure.ts +39 -1
- package/src/orchestration/extraction-persist.ts +29 -21
- package/src/orchestrator-persist-branch-failure.test.ts +271 -0
- /package/dist/{chunk-RUTN5MMC.js.map → chunk-LS7PT3YD.js.map} +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
import { resetExtractionLlmHealthForTests } from "./extraction-llm-health.js";
|
|
7
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
8
|
+
import type { StorageManager } from "./storage.js";
|
|
9
|
+
import {
|
|
10
|
+
cleanupDir,
|
|
11
|
+
harmonicNodeStoreStatus,
|
|
12
|
+
makeHarmonicLifecycleConfig,
|
|
13
|
+
mkTempMemoryDir,
|
|
14
|
+
} from "./testing/orchestrator-lite.js";
|
|
15
|
+
import type { ExtractionResult } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* #3288: a branch that throws after an earlier fact in the same batch is
|
|
19
|
+
* durable must still land that fact's batch-end effects (behavior signal,
|
|
20
|
+
* harmonic node, temporal/tag index entry, QMD request) before the error
|
|
21
|
+
* reaches the caller. A retry exact-dedupes the earlier fact, so this failure
|
|
22
|
+
* path is the only chance those effects get.
|
|
23
|
+
*/
|
|
24
|
+
const EARLIER = "Prefers terse queue names for background jobs in Project Gamma.";
|
|
25
|
+
|
|
26
|
+
type BranchCase = {
|
|
27
|
+
label: string;
|
|
28
|
+
config?: Parameters<typeof makeHarmonicLifecycleConfig>[1];
|
|
29
|
+
/** Outputs persisted after the EARLIER fact; one of them hits the injected failure. */
|
|
30
|
+
later: Partial<ExtractionResult>;
|
|
31
|
+
/** Installs the failure and returns the error the pass must rethrow. */
|
|
32
|
+
inject: (orchestrator: Orchestrator, storage: StorageManager, memoryDir: string) => Promise<Error>;
|
|
33
|
+
/**
|
|
34
|
+
* Proves the failing branch had already committed its own write when it
|
|
35
|
+
* threw and returns that write's id. It is durable too, so batch-end
|
|
36
|
+
* finalization must cover it as well as the earlier fact.
|
|
37
|
+
*/
|
|
38
|
+
committedBeforeFailure?: (storage: StorageManager) => Promise<string>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function assertEarlierFactFinalized(branch: BranchCase): Promise<void> {
|
|
42
|
+
const memoryDir = await mkTempMemoryDir(`persist-branch-${branch.label}`);
|
|
43
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
44
|
+
chunkingEnabled: false,
|
|
45
|
+
multiGraphMemoryEnabled: false,
|
|
46
|
+
factDeduplicationEnabled: true,
|
|
47
|
+
extractionMinImportanceLevel: "trivial",
|
|
48
|
+
entityActivityLogEnabled: false,
|
|
49
|
+
queryAwareIndexingEnabled: true,
|
|
50
|
+
...branch.config,
|
|
51
|
+
});
|
|
52
|
+
const orchestrator = new Orchestrator(config);
|
|
53
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
54
|
+
const temporalIdBatches: string[][] = [];
|
|
55
|
+
// Finalization starts the temporal/tag update without awaiting it; cleanup
|
|
56
|
+
// waits for it so the temp dir is not removed under its lock.
|
|
57
|
+
const temporalUpdates: Promise<void>[] = [];
|
|
58
|
+
// Bracket access is TypeScript's typed escape hatch for the private method; the spy only wraps it.
|
|
59
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
60
|
+
orchestrator["updateTemporalTagIndexes"] = (target, persistedIds) => {
|
|
61
|
+
temporalIdBatches.push([...persistedIds]);
|
|
62
|
+
const update = originalTemporal(target, persistedIds);
|
|
63
|
+
temporalUpdates.push(update);
|
|
64
|
+
return update;
|
|
65
|
+
};
|
|
66
|
+
let qmdMaintenanceCalls = 0;
|
|
67
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
68
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
69
|
+
qmdMaintenanceCalls += 1;
|
|
70
|
+
originalQmd();
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
await storage.ensureDirectories();
|
|
74
|
+
const failure = await branch.inject(orchestrator, storage, memoryDir);
|
|
75
|
+
const result: ExtractionResult = {
|
|
76
|
+
entities: [],
|
|
77
|
+
questions: [],
|
|
78
|
+
profileUpdates: [],
|
|
79
|
+
...branch.later,
|
|
80
|
+
facts: [
|
|
81
|
+
{ content: EARLIER, category: "preference", confidence: 0.95, tags: ["queue-naming"] },
|
|
82
|
+
...(branch.later.facts ?? []),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
orchestrator.persistExtraction(result, storage, null, { sessionKey: `session:${branch.label}` }),
|
|
88
|
+
(error: Error) => error === failure,
|
|
89
|
+
"the branch failure must reach the caller unchanged",
|
|
90
|
+
);
|
|
91
|
+
const branchWriteId = await branch.committedBeforeFailure?.(storage);
|
|
92
|
+
|
|
93
|
+
const earlier = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "preference");
|
|
94
|
+
assert.equal(earlier.length, 1, "the earlier fact must stay durable");
|
|
95
|
+
const factId = earlier[0]!.frontmatter.id;
|
|
96
|
+
assert.ok(
|
|
97
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
98
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
99
|
+
),
|
|
100
|
+
"the earlier fact's behavior signal must land before the rethrow",
|
|
101
|
+
);
|
|
102
|
+
const harmonicSources = (await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? [];
|
|
103
|
+
for (const id of branchWriteId ? [factId, branchWriteId] : [factId]) {
|
|
104
|
+
assert.ok(harmonicSources.includes(id), `the harmonic node must cover durable write ${id} before the rethrow`);
|
|
105
|
+
assert.ok(
|
|
106
|
+
temporalIdBatches.some((ids) => ids.includes(id)),
|
|
107
|
+
`the temporal/tag index update must cover durable write ${id} before the rethrow`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
assert.ok(qmdMaintenanceCalls > 0, "the partial batch must still schedule QMD maintenance");
|
|
111
|
+
} finally {
|
|
112
|
+
await Promise.allSettled(temporalUpdates);
|
|
113
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
114
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
115
|
+
try {
|
|
116
|
+
await orchestrator.destroy();
|
|
117
|
+
} finally {
|
|
118
|
+
resetExtractionLlmHealthForTests();
|
|
119
|
+
await cleanupDir(memoryDir);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test("#3288: a chunk write failure after the chunked parent is durable finalizes the earlier fact", async () => {
|
|
125
|
+
await assertEarlierFactFinalized({
|
|
126
|
+
label: "chunk-write",
|
|
127
|
+
config: {
|
|
128
|
+
chunkingEnabled: true,
|
|
129
|
+
chunkingTargetTokens: 12,
|
|
130
|
+
chunkingMinTokens: 10,
|
|
131
|
+
chunkingOverlapSentences: 0,
|
|
132
|
+
},
|
|
133
|
+
later: {
|
|
134
|
+
facts: [
|
|
135
|
+
{
|
|
136
|
+
content:
|
|
137
|
+
"Project Gamma runs its nightly export on a dedicated worker pool. " +
|
|
138
|
+
"The export writes compressed archives to the cold storage bucket. " +
|
|
139
|
+
"Each archive is verified against a checksum manifest before upload. " +
|
|
140
|
+
"Failed uploads are retried three times with exponential backoff.",
|
|
141
|
+
category: "fact",
|
|
142
|
+
confidence: 0.9,
|
|
143
|
+
tags: ["export"],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
inject: async (_orchestrator, storage) => {
|
|
148
|
+
const failure = new Error("synthetic chunk write failure");
|
|
149
|
+
storage.writeChunk = async () => {
|
|
150
|
+
throw failure;
|
|
151
|
+
};
|
|
152
|
+
return failure;
|
|
153
|
+
},
|
|
154
|
+
committedBeforeFailure: async (storage) => {
|
|
155
|
+
const parents = (await storage.readAllMemories()).filter((m) => m.frontmatter.tags?.includes("chunked"));
|
|
156
|
+
assert.equal(parents.length, 1, "the chunked parent must be durable before the chunk write throws");
|
|
157
|
+
return parents[0]!.frontmatter.id;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const MERGE_TARGET_ID = "merge-target-gamma";
|
|
163
|
+
const MERGE_SEED = "The Project Gamma billing service deploys on a Tuesday cadence.";
|
|
164
|
+
const MERGE_INCOMING = "The Project Gamma billing service deploys at 09:00 UTC sharp.";
|
|
165
|
+
|
|
166
|
+
/** Seed an active merge target and a deterministic merge judge for MERGE_INCOMING only. */
|
|
167
|
+
async function armSemanticMerge(orchestrator: Orchestrator, memoryDir: string): Promise<void> {
|
|
168
|
+
const created = new Date(Date.now() - 3600_000).toISOString();
|
|
169
|
+
const seededDir = path.join(memoryDir, "facts", created.slice(0, 10));
|
|
170
|
+
await mkdir(seededDir, { recursive: true });
|
|
171
|
+
await writeFile(
|
|
172
|
+
path.join(seededDir, `${MERGE_TARGET_ID}.md`),
|
|
173
|
+
[
|
|
174
|
+
"---",
|
|
175
|
+
`id: ${MERGE_TARGET_ID}`,
|
|
176
|
+
"category: fact",
|
|
177
|
+
`created: ${created}`,
|
|
178
|
+
`updated: ${created}`,
|
|
179
|
+
"source: extraction",
|
|
180
|
+
"confidence: 0.9",
|
|
181
|
+
"confidenceTier: explicit",
|
|
182
|
+
"status: active",
|
|
183
|
+
"importanceScore: 0.9",
|
|
184
|
+
"importanceLevel: high",
|
|
185
|
+
"---",
|
|
186
|
+
"",
|
|
187
|
+
MERGE_SEED,
|
|
188
|
+
"",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
// Band neighbor for the incoming fact only ([minSimilarity, threshold)).
|
|
193
|
+
const embeddings = orchestrator.embeddingFallback;
|
|
194
|
+
embeddings.isAvailable = async () => true;
|
|
195
|
+
embeddings.search = async (query) => (query === MERGE_INCOMING ? [{ id: MERGE_TARGET_ID, score: 0.85, path: "" }] : []);
|
|
196
|
+
embeddings.indexFile = async () => {};
|
|
197
|
+
embeddings.removeFromIndex = async () => {};
|
|
198
|
+
// Deterministic merge judge; every other local-LLM prompt gets no answer.
|
|
199
|
+
orchestrator.localLlm.chatCompletion = async (messages) =>
|
|
200
|
+
messages[0]?.role === "system" && messages[0].content.startsWith("You maintain a long-term memory store")
|
|
201
|
+
? {
|
|
202
|
+
content: JSON.stringify({
|
|
203
|
+
decision: "merge",
|
|
204
|
+
targetId: MERGE_TARGET_ID,
|
|
205
|
+
mergedContent: `${MERGE_SEED} ${MERGE_INCOMING}`,
|
|
206
|
+
reason: "deterministic judge for the merge-branch failure test",
|
|
207
|
+
}),
|
|
208
|
+
}
|
|
209
|
+
: null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test("#3288: a semantic-merge post-effect failure after the merge commits finalizes the earlier fact", async () => {
|
|
213
|
+
await assertEarlierFactFinalized({
|
|
214
|
+
label: "semantic-merge",
|
|
215
|
+
config: {
|
|
216
|
+
embeddingFallbackEnabled: true,
|
|
217
|
+
versioningEnabled: true,
|
|
218
|
+
semanticMerge: { enabled: true },
|
|
219
|
+
multiGraphMemoryEnabled: true,
|
|
220
|
+
},
|
|
221
|
+
later: {
|
|
222
|
+
facts: [{ content: MERGE_INCOMING, category: "fact", confidence: 0.9, tags: [] }],
|
|
223
|
+
},
|
|
224
|
+
inject: async (orchestrator, _storage, memoryDir) => {
|
|
225
|
+
await armSemanticMerge(orchestrator, memoryDir);
|
|
226
|
+
// The merged-target graph rebuild fails open, but the edge-cache
|
|
227
|
+
// invalidation that follows it is not wrapped: an unavailable graph
|
|
228
|
+
// index escapes runMergedTargetPostEffects after the merge committed.
|
|
229
|
+
const failure = new Error("synthetic graph index failure");
|
|
230
|
+
orchestrator["graphIndexFor"] = () => {
|
|
231
|
+
throw failure;
|
|
232
|
+
};
|
|
233
|
+
return failure;
|
|
234
|
+
},
|
|
235
|
+
committedBeforeFailure: async (storage) => {
|
|
236
|
+
const target = await storage.getMemoryByIdIncludingArchived(MERGE_TARGET_ID);
|
|
237
|
+
assert.ok(target?.content.includes(MERGE_INCOMING), "the merge must be committed before the post effect throws");
|
|
238
|
+
return MERGE_TARGET_ID;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("#3288: a profile append failure after an earlier fact is durable finalizes that fact", async () => {
|
|
244
|
+
await assertEarlierFactFinalized({
|
|
245
|
+
label: "profile-append",
|
|
246
|
+
later: { profileUpdates: ["Works mostly on the Project Gamma export pipeline."] },
|
|
247
|
+
inject: async (_orchestrator, storage) => {
|
|
248
|
+
const failure = new Error("synthetic profile append failure");
|
|
249
|
+
storage.appendToProfile = async () => {
|
|
250
|
+
throw failure;
|
|
251
|
+
};
|
|
252
|
+
return failure;
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("#3288: a question write failure after an earlier fact is durable finalizes that fact", async () => {
|
|
258
|
+
await assertEarlierFactFinalized({
|
|
259
|
+
label: "question-write",
|
|
260
|
+
later: {
|
|
261
|
+
questions: [{ question: "Which bucket holds the Project Gamma archives?", context: "export pipeline", priority: 0.5 }],
|
|
262
|
+
},
|
|
263
|
+
inject: async (_orchestrator, storage) => {
|
|
264
|
+
const failure = new Error("synthetic question write failure");
|
|
265
|
+
storage.writeQuestion = async () => {
|
|
266
|
+
throw failure;
|
|
267
|
+
};
|
|
268
|
+
return failure;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
});
|
|
File without changes
|