@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/index.js CHANGED
@@ -181,7 +181,7 @@ import {
181
181
  writeManifest,
182
182
  writeMeetingEpisodeMemory,
183
183
  writeTimelineState
184
- } from "./chunk-LFYEQK45.js";
184
+ } from "./chunk-FAF4A76Q.js";
185
185
  import {
186
186
  WEARABLE_SOURCE_PREFIX,
187
187
  buildExtractionTurns,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TurnIngestionCoordinator
3
- } from "../chunk-LFYEQK45.js";
3
+ } from "../chunk-FAF4A76Q.js";
4
4
  import "../chunk-5QV6CE6K.js";
5
5
  import "../chunk-GGD2YDS6.js";
6
6
  import "../chunk-5PHOU5QI.js";
@@ -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;CAkxFpC"}
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"}
@@ -4,7 +4,7 @@ import {
4
4
  graphPathRelativeToStorage,
5
5
  mergeGraphExpandedResults,
6
6
  qmdStartupCollectionChecksWithTimeout
7
- } from "./chunk-LFYEQK45.js";
7
+ } from "./chunk-FAF4A76Q.js";
8
8
  import "./chunk-5QV6CE6K.js";
9
9
  import "./chunk-GGD2YDS6.js";
10
10
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "10.0.67",
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.67"
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
+ });