@remnic/core 10.0.67 → 10.0.68
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-cli.js +1 -1
- package/dist/{chunk-LFYEQK45.js → chunk-FAF4A76Q.js} +35 -36
- package/dist/chunk-FAF4A76Q.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/orchestration/buffer-drain-runner.js +1 -1
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestrator.js +1 -1
- package/package.json +2 -2
- package/src/extraction-parallel-dedup.test.ts +185 -0
- package/src/orchestration/extraction-persist.ts +18 -26
- package/src/orchestration/provider-breaker.test.ts +22 -0
- package/dist/chunk-LFYEQK45.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extraction-persist.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAgBxG,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,OAAO,EAUgE,KAAK,8BAA8B,EACxG,KAAK,4BAA4B,EAClC,MAAM,oBAAoB,CAAC;AAqD5B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF,OAAO,KAAK,EAEV,gBAAgB,EAQjB,MAAM,aAAa,CAAC;AAWrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAmB1E,6FAA6F;AAC7F,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAE9C,OAAO,KAAK,MAAM,GAEjB;IACD,OAAO,KAAK,6BAA6B,GAGxC;IAEK,iBAAiB,CACrB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,cAAc,EACvB,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,EACrC,aAAa,CAAC,EAAE,uBAAuB,EACvC,aAAa,CAAC,EAAE,MAAM,EACtB,qBAAqB,CAAC,EAAE,wBAAwB,GAAG,IAAI,EACvD,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,GAAE,8BAAuF,EAClG,aAAa,GAAE,4BAAmF,GACjG,OAAO,CAAC,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"extraction-persist.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAgBxG,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,OAAO,EAUgE,KAAK,8BAA8B,EACxG,KAAK,4BAA4B,EAClC,MAAM,oBAAoB,CAAC;AAqD5B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF,OAAO,KAAK,EAEV,gBAAgB,EAQjB,MAAM,aAAa,CAAC;AAWrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAmB1E,6FAA6F;AAC7F,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAE9C,OAAO,KAAK,MAAM,GAEjB;IACD,OAAO,KAAK,6BAA6B,GAGxC;IAEK,iBAAiB,CACrB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,cAAc,EACvB,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,EACrC,aAAa,CAAC,EAAE,uBAAuB,EACvC,aAAa,CAAC,EAAE,MAAM,EACtB,qBAAqB,CAAC,EAAE,wBAAwB,GAAG,IAAI,EACvD,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,GAAE,8BAAuF,EAClG,aAAa,GAAE,4BAAmF,GACjG,OAAO,CAAC,uBAAuB,CAAC;CA0wFpC"}
|
package/dist/orchestrator.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.68",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3376,7 +3376,7 @@
|
|
|
3376
3376
|
"core"
|
|
3377
3377
|
],
|
|
3378
3378
|
"peerDependencies": {
|
|
3379
|
-
"@remnic/coding-graph": "^10.0.
|
|
3379
|
+
"@remnic/coding-graph": "^10.0.68"
|
|
3380
3380
|
},
|
|
3381
3381
|
"peerDependenciesMeta": {
|
|
3382
3382
|
"@remnic/coding-graph": {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #3312 — non-fact dedup registration must share the fact-dedup commit
|
|
3
|
+
* window.
|
|
4
|
+
*
|
|
5
|
+
* writeMemory indexes content hashes for facts only (rule 44 #1579); every
|
|
6
|
+
* other category used to register its hash in a post-write block that ran
|
|
7
|
+
* after persistUnderFactDedupWindow had already released the per-dir commit
|
|
8
|
+
* window. With extractionQueueConcurrency > 1 a second session's in-window
|
|
9
|
+
* recheck could then miss and both sessions wrote the same preference —
|
|
10
|
+
* duplicate active copies. The fix registers the hash inside the window
|
|
11
|
+
* callback, so any later window holder observes the committed content.
|
|
12
|
+
*
|
|
13
|
+
* These tests drive the real persistExtraction path (no LLM network; every
|
|
14
|
+
* gate that would call one is off) and assert the durable outcome: exactly
|
|
15
|
+
* one active copy survives a concurrent same-content write pair.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
|
|
24
|
+
import { parseConfig } from "./config.js";
|
|
25
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
26
|
+
import type { ExtractionResult, PluginConfig } from "./types.js";
|
|
27
|
+
import type { LocalLlmClient } from "./local-llm.js";
|
|
28
|
+
import type { PersistExtractionFn } from "./testing/orchestrator-lite.js";
|
|
29
|
+
import type { StorageManager } from "./storage.js";
|
|
30
|
+
|
|
31
|
+
// Fixture contract for the private Orchestrator surface this suite drives.
|
|
32
|
+
interface OrchestratorFixture {
|
|
33
|
+
localLlm?: unknown;
|
|
34
|
+
chatCompletion?: unknown;
|
|
35
|
+
getStorage?: unknown;
|
|
36
|
+
persistExtraction?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Named owner contract for the private internals this suite drives.
|
|
40
|
+
interface OrchestratorInternals {
|
|
41
|
+
localLlm: LocalLlmClient;
|
|
42
|
+
getStorage: (ns: string) => Promise<StorageManager>;
|
|
43
|
+
persistExtraction: PersistExtractionFn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function asInternals<T extends object>(value: OrchestratorFixture): T {
|
|
47
|
+
// SAFETY: the fixture provides the members the persist path reads; the
|
|
48
|
+
// remaining contract members are unused by these tests.
|
|
49
|
+
return value as T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function makeHarness(): Promise<{
|
|
53
|
+
orchestrator: Orchestrator;
|
|
54
|
+
persist: OrchestratorInternals;
|
|
55
|
+
memoryDir: string;
|
|
56
|
+
}> {
|
|
57
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-parallel-dedup-"));
|
|
58
|
+
const config: PluginConfig = parseConfig({
|
|
59
|
+
openaiApiKey: "sk-test",
|
|
60
|
+
memoryDir,
|
|
61
|
+
workspaceDir: path.join(memoryDir, "workspace"),
|
|
62
|
+
// Minimize every gate that needs a model or corpus-scale machinery so the
|
|
63
|
+
// preference reaches the normal (non-chunked) write path.
|
|
64
|
+
extractionJudgeEnabled: false,
|
|
65
|
+
temporalSupersessionEnabled: false,
|
|
66
|
+
contradictionDetectionEnabled: false,
|
|
67
|
+
chunkingEnabled: false,
|
|
68
|
+
embeddingFallbackEnabled: false,
|
|
69
|
+
qmdEnabled: false,
|
|
70
|
+
extractionMinChars: 0,
|
|
71
|
+
extractionMinImportanceLevel: "trivial",
|
|
72
|
+
inlineSourceAttributionEnabled: false,
|
|
73
|
+
// Real deployment profile: the artifact write sits between the window
|
|
74
|
+
// release and hash registration, widening the exact gap under test.
|
|
75
|
+
verbatimArtifactsEnabled: true,
|
|
76
|
+
verbatimArtifactCategories: ["preference"],
|
|
77
|
+
});
|
|
78
|
+
const orchestrator = new Orchestrator(config);
|
|
79
|
+
const internals = asInternals<OrchestratorInternals>(orchestrator);
|
|
80
|
+
internals.localLlm = asInternals<LocalLlmClient>({ chatCompletion: async () => null });
|
|
81
|
+
return { orchestrator, persist: internals, memoryDir };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function preferenceResult(content: string): ExtractionResult {
|
|
85
|
+
const result = {
|
|
86
|
+
facts: [
|
|
87
|
+
{
|
|
88
|
+
content,
|
|
89
|
+
category: "preference",
|
|
90
|
+
tags: [],
|
|
91
|
+
confidence: 0.9,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
entities: [],
|
|
95
|
+
relationships: [],
|
|
96
|
+
questions: [],
|
|
97
|
+
profileUpdates: [],
|
|
98
|
+
};
|
|
99
|
+
// SAFETY: literal carries the full extraction-result shape persistExtraction
|
|
100
|
+
// consumes for a single preference candidate.
|
|
101
|
+
return result as ExtractionResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function activePreferenceCopies(storage: StorageManager): Promise<number> {
|
|
105
|
+
const memories = await storage.readAllMemories();
|
|
106
|
+
return memories.filter(
|
|
107
|
+
(m) => m.frontmatter.category === "preference" && (m.frontmatter.status ?? "active") === "active",
|
|
108
|
+
).length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PREFERENCE = "Always answer with bullet points.";
|
|
112
|
+
|
|
113
|
+
test("a repeated identical preference persists once (serial baseline)", async () => {
|
|
114
|
+
const { persist, memoryDir } = await makeHarness();
|
|
115
|
+
try {
|
|
116
|
+
const storage = await persist.getStorage("default");
|
|
117
|
+
await storage.ensureDirectories();
|
|
118
|
+
|
|
119
|
+
const first = await persist.persistExtraction(
|
|
120
|
+
preferenceResult(PREFERENCE),
|
|
121
|
+
storage,
|
|
122
|
+
null,
|
|
123
|
+
{ sessionKey: "agent:test:a", principal: "test" },
|
|
124
|
+
undefined,
|
|
125
|
+
null,
|
|
126
|
+
"User said they always want bullet points.",
|
|
127
|
+
);
|
|
128
|
+
const second = await persist.persistExtraction(
|
|
129
|
+
preferenceResult(PREFERENCE),
|
|
130
|
+
storage,
|
|
131
|
+
null,
|
|
132
|
+
{ sessionKey: "agent:test:b", principal: "test" },
|
|
133
|
+
undefined,
|
|
134
|
+
null,
|
|
135
|
+
"User said they always want bullet points.",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
assert.equal(first.persistedIds.length, 1, "first write persists");
|
|
139
|
+
assert.equal(second.persistedIds.length, 0, "second identical write is deduped");
|
|
140
|
+
assert.equal(await activePreferenceCopies(storage), 1);
|
|
141
|
+
} finally {
|
|
142
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("concurrent identical preferences leave exactly one active copy", async () => {
|
|
147
|
+
const { persist, memoryDir } = await makeHarness();
|
|
148
|
+
try {
|
|
149
|
+
const storage = await persist.getStorage("default");
|
|
150
|
+
await storage.ensureDirectories();
|
|
151
|
+
|
|
152
|
+
const sourceText = "User said they always want bullet points.";
|
|
153
|
+
// The race was probabilistic before the fix (the window released before
|
|
154
|
+
// registration); after it, this invariant holds for every interleaving.
|
|
155
|
+
const [a, b] = await Promise.all([
|
|
156
|
+
persist.persistExtraction(
|
|
157
|
+
preferenceResult(PREFERENCE),
|
|
158
|
+
storage,
|
|
159
|
+
null,
|
|
160
|
+
{ sessionKey: "agent:test:a", principal: "test" },
|
|
161
|
+
undefined,
|
|
162
|
+
null,
|
|
163
|
+
sourceText,
|
|
164
|
+
),
|
|
165
|
+
persist.persistExtraction(
|
|
166
|
+
preferenceResult(PREFERENCE),
|
|
167
|
+
storage,
|
|
168
|
+
null,
|
|
169
|
+
{ sessionKey: "agent:test:b", principal: "test" },
|
|
170
|
+
undefined,
|
|
171
|
+
null,
|
|
172
|
+
sourceText,
|
|
173
|
+
),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
assert.equal(
|
|
177
|
+
a.persistedIds.length + b.persistedIds.length,
|
|
178
|
+
1,
|
|
179
|
+
"exactly one concurrent write may persist",
|
|
180
|
+
);
|
|
181
|
+
assert.equal(await activePreferenceCopies(storage), 1);
|
|
182
|
+
} finally {
|
|
183
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
@@ -2581,8 +2581,8 @@ export class ExtractionPersistCoordinator {
|
|
|
2581
2581
|
const factWriteOutcome = await persistUnderFactDedupWindow(
|
|
2582
2582
|
targetStorage.dir,
|
|
2583
2583
|
raceRecheck(targetStorage, contentHashDedupKey),
|
|
2584
|
-
async () =>
|
|
2585
|
-
targetStorage.writeSealedMemory(factWriteEnvelope, {
|
|
2584
|
+
async () => {
|
|
2585
|
+
const factWrite = await targetStorage.writeSealedMemory(factWriteEnvelope, {
|
|
2586
2586
|
importance,
|
|
2587
2587
|
supersedes,
|
|
2588
2588
|
links: links.length > 0 ? links : undefined,
|
|
@@ -2605,7 +2605,22 @@ export class ExtractionPersistCoordinator {
|
|
|
2605
2605
|
// must see this hash, or its recheck misses and duplicates it.
|
|
2606
2606
|
deferHashIndexSave: false,
|
|
2607
2607
|
...(factToolScoped ? { toolScoped: true as const } : {}),
|
|
2608
|
-
})
|
|
2608
|
+
});
|
|
2609
|
+
// Register the dedup hash inside the same commit window: writeMemory
|
|
2610
|
+
// indexes facts only (rule 44 #1579), so non-fact categories
|
|
2611
|
+
// previously registered after the window released, letting a
|
|
2612
|
+
// concurrent session's in-window recheck miss and both write the
|
|
2613
|
+
// same content (issue #3312). Fail-open, like the post-write
|
|
2614
|
+
// registration this replaces.
|
|
2615
|
+
if (!factWrite.tombstoneBlocked) {
|
|
2616
|
+
try {
|
|
2617
|
+
await this.deps.addContentHashDedup(targetStorage, contentHashDedupKey);
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
log.warn(`content-hash dedup registration failed for memory ${factWrite.id}: ${err}`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return factWrite;
|
|
2623
|
+
},
|
|
2609
2624
|
).catch(finalizeOnWindowFailure);
|
|
2610
2625
|
if (factWriteOutcome.suppressed) {
|
|
2611
2626
|
// Codex P2: backfill bounds onto the window's winning copy first.
|
|
@@ -2800,29 +2815,6 @@ export class ExtractionPersistCoordinator {
|
|
|
2800
2815
|
...(factToolScoped ? { toolScoped: true as const } : {}),
|
|
2801
2816
|
});
|
|
2802
2817
|
}
|
|
2803
|
-
// Register in the target storage content-hash index after successful
|
|
2804
|
-
// write. Thread 3 fix: canonicalize by stripping any pre-existing
|
|
2805
|
-
// citation so the stored hash matches what the dedup check computes.
|
|
2806
|
-
try {
|
|
2807
|
-
const canonicalFactContent =
|
|
2808
|
-
citationEnabled &&
|
|
2809
|
-
hasCitationForTemplate(fact.content, citationTemplate)
|
|
2810
|
-
? stripCitationForTemplate(fact.content, citationTemplate)
|
|
2811
|
-
: fact.content;
|
|
2812
|
-
const hashRegisterKey =
|
|
2813
|
-
writeCategory === "procedure"
|
|
2814
|
-
? buildProcedurePersistBody(fact.content, fact.procedureSteps)
|
|
2815
|
-
: canonicalFactContent;
|
|
2816
|
-
// #1645: do NOT register a tombstone-blocked fact's content in the dedup
|
|
2817
|
-
// index (rule 44 defeat) — see chunked path comment.
|
|
2818
|
-
if (!tombstoneBlocked) {
|
|
2819
|
-
await this.deps.addContentHashDedup(targetStorage, hashRegisterKey);
|
|
2820
|
-
}
|
|
2821
|
-
} catch (err) {
|
|
2822
|
-
log.warn(
|
|
2823
|
-
`content-hash dedup registration failed for memory ${memoryId}: ${err}`,
|
|
2824
|
-
);
|
|
2825
|
-
}
|
|
2826
2818
|
} catch (err) {
|
|
2827
2819
|
// PR #2016 / #3231: finalize deferred side effects for facts already
|
|
2828
2820
|
// durable before propagating so hash/behavior/harmonic/temporal/QMD
|
|
@@ -87,3 +87,25 @@ test("an unowned forced outcome neither trips the half-open threshold nor settle
|
|
|
87
87
|
breaker.success(probe.token);
|
|
88
88
|
assert.deepEqual(breaker.acquire(6_500).suppressed, false, "owner success closes the breaker");
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
test("an unowned auth_config failure does not reopen over a held probe", () => {
|
|
92
|
+
const breaker = new ProviderCircuitBreaker();
|
|
93
|
+
breaker.failure("provider_retryable", 1_000, CONFIG);
|
|
94
|
+
breaker.failure("provider_retryable", 1_050, CONFIG);
|
|
95
|
+
breaker.failure("provider_retryable", 1_100, CONFIG);
|
|
96
|
+
const probe = breaker.acquire(6_100);
|
|
97
|
+
assert.equal(probe.probe, true);
|
|
98
|
+
|
|
99
|
+
// A forced extraction reporting auth_config while the probe runs must not
|
|
100
|
+
// flip the breaker open over the still-held owner token (PRRT_kwDORJXyws6z07Mu).
|
|
101
|
+
breaker.failure("auth_config", 1_200, CONFIG);
|
|
102
|
+
assert.deepEqual(breaker.acquire(1_300), { suppressed: true, probe: false }, "probe stays held");
|
|
103
|
+
|
|
104
|
+
// The owner's own auth_config failure settles it with the auth cooldown.
|
|
105
|
+
breaker.failure("auth_config", 1_400, CONFIG, probe.token);
|
|
106
|
+
assert.deepEqual(breaker.acquire(1_500).suppressed, true, "owned auth_config failure reopens at once");
|
|
107
|
+
assert.deepEqual(breaker.acquire(60_000).suppressed, true, "auth cooldown still gates the next probe");
|
|
108
|
+
// The stranded-owner hazard: after the auth cooldown elapses, no SECOND
|
|
109
|
+
// probe may mint while the original owner is still held.
|
|
110
|
+
assert.deepEqual(breaker.acquire(60_001), { suppressed: true, probe: false }, "no second probe over a held one");
|
|
111
|
+
});
|