@remnic/core 9.3.714 → 9.3.716
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-boundary.d.ts +2 -2
- package/dist/access-cli.js +2 -2
- package/dist/access-http.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-operations.d.ts +2 -2
- package/dist/{access-service-lddjZoRh.d.ts → access-service-BW8cm_uU.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-OQH5XUH3.js → chunk-Q6IP2ARR.js} +2 -5
- package/dist/chunk-Q6IP2ARR.js.map +1 -0
- package/dist/{chunk-FXZKXW7C.js → chunk-SQ4LU27I.js} +48 -20
- package/dist/chunk-SQ4LU27I.js.map +1 -0
- package/dist/{cli-iAiVkbYJ.d.ts → cli-Dpivy32W.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/direct-answer-wiring.d.ts +9 -9
- package/dist/direct-answer-wiring.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-iVZWi8aW.d.ts → orchestrator-vygo2YIM.d.ts} +15 -5
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +2 -2
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-planner-llm.js.map +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +2 -2
- package/src/capabilities.test.ts +28 -0
- package/src/direct-answer-wiring.test.ts +20 -13
- package/src/direct-answer-wiring.ts +9 -13
- package/src/faithfulness-graph-guard.test.ts +214 -8
- package/src/orchestrator.ts +68 -27
- package/src/recall-planner-llm.test.ts +30 -11
- package/src/recall-planner-llm.ts +4 -5
- package/dist/chunk-FXZKXW7C.js.map +0 -1
- package/dist/chunk-OQH5XUH3.js.map +0 -1
package/dist/transfer/types.d.ts
CHANGED
|
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
|
|
|
313
313
|
peerProfiles: boolean;
|
|
314
314
|
}>;
|
|
315
315
|
}, "strip", z.ZodTypeAny, {
|
|
316
|
-
schemaVersion: string;
|
|
317
316
|
includes: {
|
|
318
317
|
procedural: boolean;
|
|
319
318
|
taxonomy: boolean;
|
|
320
319
|
identityAnchors: boolean;
|
|
321
320
|
peerProfiles: boolean;
|
|
322
321
|
};
|
|
322
|
+
schemaVersion: string;
|
|
323
323
|
id: string;
|
|
324
324
|
version: string;
|
|
325
325
|
description: string;
|
|
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
|
|
|
334
334
|
directAnswerEnabled: boolean;
|
|
335
335
|
};
|
|
336
336
|
}, {
|
|
337
|
-
schemaVersion: string;
|
|
338
337
|
includes: {
|
|
339
338
|
procedural: boolean;
|
|
340
339
|
taxonomy: boolean;
|
|
341
340
|
identityAnchors: boolean;
|
|
342
341
|
peerProfiles: boolean;
|
|
343
342
|
};
|
|
343
|
+
schemaVersion: string;
|
|
344
344
|
id: string;
|
|
345
345
|
version: string;
|
|
346
346
|
description: string;
|
|
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
464
464
|
peerProfiles: boolean;
|
|
465
465
|
}>;
|
|
466
466
|
}, "strip", z.ZodTypeAny, {
|
|
467
|
-
schemaVersion: string;
|
|
468
467
|
includes: {
|
|
469
468
|
procedural: boolean;
|
|
470
469
|
taxonomy: boolean;
|
|
471
470
|
identityAnchors: boolean;
|
|
472
471
|
peerProfiles: boolean;
|
|
473
472
|
};
|
|
473
|
+
schemaVersion: string;
|
|
474
474
|
id: string;
|
|
475
475
|
version: string;
|
|
476
476
|
description: string;
|
|
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
485
485
|
directAnswerEnabled: boolean;
|
|
486
486
|
};
|
|
487
487
|
}, {
|
|
488
|
-
schemaVersion: string;
|
|
489
488
|
includes: {
|
|
490
489
|
procedural: boolean;
|
|
491
490
|
taxonomy: boolean;
|
|
492
491
|
identityAnchors: boolean;
|
|
493
492
|
peerProfiles: boolean;
|
|
494
493
|
};
|
|
494
|
+
schemaVersion: string;
|
|
495
495
|
id: string;
|
|
496
496
|
version: string;
|
|
497
497
|
description: string;
|
|
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
518
518
|
pluginVersion: string;
|
|
519
519
|
includesTranscripts: boolean;
|
|
520
520
|
capsule: {
|
|
521
|
-
schemaVersion: string;
|
|
522
521
|
includes: {
|
|
523
522
|
procedural: boolean;
|
|
524
523
|
taxonomy: boolean;
|
|
525
524
|
identityAnchors: boolean;
|
|
526
525
|
peerProfiles: boolean;
|
|
527
526
|
};
|
|
527
|
+
schemaVersion: string;
|
|
528
528
|
id: string;
|
|
529
529
|
version: string;
|
|
530
530
|
description: string;
|
|
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
551
551
|
pluginVersion: string;
|
|
552
552
|
includesTranscripts: boolean;
|
|
553
553
|
capsule: {
|
|
554
|
-
schemaVersion: string;
|
|
555
554
|
includes: {
|
|
556
555
|
procedural: boolean;
|
|
557
556
|
taxonomy: boolean;
|
|
558
557
|
identityAnchors: boolean;
|
|
559
558
|
peerProfiles: boolean;
|
|
560
559
|
};
|
|
560
|
+
schemaVersion: string;
|
|
561
561
|
id: string;
|
|
562
562
|
version: string;
|
|
563
563
|
description: string;
|
|
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
683
683
|
peerProfiles: boolean;
|
|
684
684
|
}>;
|
|
685
685
|
}, "strip", z.ZodTypeAny, {
|
|
686
|
-
schemaVersion: string;
|
|
687
686
|
includes: {
|
|
688
687
|
procedural: boolean;
|
|
689
688
|
taxonomy: boolean;
|
|
690
689
|
identityAnchors: boolean;
|
|
691
690
|
peerProfiles: boolean;
|
|
692
691
|
};
|
|
692
|
+
schemaVersion: string;
|
|
693
693
|
id: string;
|
|
694
694
|
version: string;
|
|
695
695
|
description: string;
|
|
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
704
704
|
directAnswerEnabled: boolean;
|
|
705
705
|
};
|
|
706
706
|
}, {
|
|
707
|
-
schemaVersion: string;
|
|
708
707
|
includes: {
|
|
709
708
|
procedural: boolean;
|
|
710
709
|
taxonomy: boolean;
|
|
711
710
|
identityAnchors: boolean;
|
|
712
711
|
peerProfiles: boolean;
|
|
713
712
|
};
|
|
713
|
+
schemaVersion: string;
|
|
714
714
|
id: string;
|
|
715
715
|
version: string;
|
|
716
716
|
description: string;
|
|
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
737
737
|
pluginVersion: string;
|
|
738
738
|
includesTranscripts: boolean;
|
|
739
739
|
capsule: {
|
|
740
|
-
schemaVersion: string;
|
|
741
740
|
includes: {
|
|
742
741
|
procedural: boolean;
|
|
743
742
|
taxonomy: boolean;
|
|
744
743
|
identityAnchors: boolean;
|
|
745
744
|
peerProfiles: boolean;
|
|
746
745
|
};
|
|
746
|
+
schemaVersion: string;
|
|
747
747
|
id: string;
|
|
748
748
|
version: string;
|
|
749
749
|
description: string;
|
|
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
770
770
|
pluginVersion: string;
|
|
771
771
|
includesTranscripts: boolean;
|
|
772
772
|
capsule: {
|
|
773
|
-
schemaVersion: string;
|
|
774
773
|
includes: {
|
|
775
774
|
procedural: boolean;
|
|
776
775
|
taxonomy: boolean;
|
|
777
776
|
identityAnchors: boolean;
|
|
778
777
|
peerProfiles: boolean;
|
|
779
778
|
};
|
|
779
|
+
schemaVersion: string;
|
|
780
780
|
id: string;
|
|
781
781
|
version: string;
|
|
782
782
|
description: string;
|
|
@@ -819,13 +819,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
819
819
|
pluginVersion: string;
|
|
820
820
|
includesTranscripts: boolean;
|
|
821
821
|
capsule: {
|
|
822
|
-
schemaVersion: string;
|
|
823
822
|
includes: {
|
|
824
823
|
procedural: boolean;
|
|
825
824
|
taxonomy: boolean;
|
|
826
825
|
identityAnchors: boolean;
|
|
827
826
|
peerProfiles: boolean;
|
|
828
827
|
};
|
|
828
|
+
schemaVersion: string;
|
|
829
829
|
id: string;
|
|
830
830
|
version: string;
|
|
831
831
|
description: string;
|
|
@@ -858,13 +858,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
858
858
|
pluginVersion: string;
|
|
859
859
|
includesTranscripts: boolean;
|
|
860
860
|
capsule: {
|
|
861
|
-
schemaVersion: string;
|
|
862
861
|
includes: {
|
|
863
862
|
procedural: boolean;
|
|
864
863
|
taxonomy: boolean;
|
|
865
864
|
identityAnchors: boolean;
|
|
866
865
|
peerProfiles: boolean;
|
|
867
866
|
};
|
|
867
|
+
schemaVersion: string;
|
|
868
868
|
id: string;
|
|
869
869
|
version: string;
|
|
870
870
|
description: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.3.
|
|
3
|
+
"version": "9.3.716",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -2966,7 +2966,7 @@
|
|
|
2966
2966
|
"core"
|
|
2967
2967
|
],
|
|
2968
2968
|
"peerDependencies": {
|
|
2969
|
-
"@remnic/coding-graph": "^9.3.
|
|
2969
|
+
"@remnic/coding-graph": "^9.3.716"
|
|
2970
2970
|
},
|
|
2971
2971
|
"peerDependenciesMeta": {
|
|
2972
2972
|
"@remnic/coding-graph": {
|
package/src/capabilities.test.ts
CHANGED
|
@@ -96,6 +96,34 @@ test("resolveCapabilities returns a frozen object", () => {
|
|
|
96
96
|
assert.equal(Object.isFrozen(caps), true, "CapabilitySet must be frozen");
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("resolveCapabilities is deterministic — same config always yields identical gate values (one-resolution-per-op #1523)", () => {
|
|
100
|
+
// The one-resolution-per-op contract (issue #1523): resolve ONCE at the
|
|
101
|
+
// operation entry, thread the frozen result down. This test proves the
|
|
102
|
+
// resolution is deterministic — calling it twice with the same config
|
|
103
|
+
// produces identical values for every gate, so there is never a reason to
|
|
104
|
+
// re-resolve mid-operation.
|
|
105
|
+
const config = parseConfig({
|
|
106
|
+
rerankCacheEnabled: true,
|
|
107
|
+
recallDirectAnswerEnabled: false,
|
|
108
|
+
recallMmrEnabled: true,
|
|
109
|
+
recallPlannerLlmEnabled: true,
|
|
110
|
+
graphRecallEnabled: true,
|
|
111
|
+
});
|
|
112
|
+
const first = resolveCapabilities(config);
|
|
113
|
+
const second = resolveCapabilities(config);
|
|
114
|
+
for (const field of FIELDS) {
|
|
115
|
+
assert.equal(
|
|
116
|
+
first[field],
|
|
117
|
+
second[field],
|
|
118
|
+
`field ${field} must be deterministic across resolutions`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Both must be frozen — a mid-operation mutation would break the contract.
|
|
122
|
+
assert.equal(Object.isFrozen(first), true);
|
|
123
|
+
assert.equal(Object.isFrozen(second), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
|
|
99
127
|
// ---------------------------------------------------------------------------
|
|
100
128
|
// GraphConstructionCapabilitySet — gate-parity tests (issue #1566 Cluster A).
|
|
101
129
|
//
|
|
@@ -13,7 +13,6 @@ import type { TrustZoneName } from "./trust-zones.js";
|
|
|
13
13
|
type WiringConfig = DirectAnswerWiringInput["config"];
|
|
14
14
|
|
|
15
15
|
const BASE_CONFIG: WiringConfig = {
|
|
16
|
-
recallDirectAnswerEnabled: true,
|
|
17
16
|
recallDirectAnswerTokenOverlapFloor: 0.5,
|
|
18
17
|
recallDirectAnswerImportanceFloor: 0.7,
|
|
19
18
|
recallDirectAnswerAmbiguityMargin: 0.15,
|
|
@@ -105,17 +104,24 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
|
|
|
105
104
|
assert.deepEqual(sources.calls.importance, []);
|
|
106
105
|
});
|
|
107
106
|
|
|
108
|
-
// ──
|
|
107
|
+
// ── One-resolution-per-op (#1523): `enabled` is the sole gate ──────────────
|
|
109
108
|
|
|
110
|
-
test("tryDirectAnswer
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
109
|
+
test("tryDirectAnswer disabled when `enabled: false` even with matching candidates (caps wins over any config-derived value)", async () => {
|
|
110
|
+
// Issue #1523 contract: the resolved capability is the sole gate.
|
|
111
|
+
// `enabled` comes from `resolveCapabilities(config).recallDirectAnswer`,
|
|
112
|
+
// resolved ONCE at the recall entry. This test proves the gate value is
|
|
113
|
+
// authoritative — when it says disabled, the function short-circuits
|
|
114
|
+
// regardless of candidate quality.
|
|
115
|
+
const sources = makeMockSources({
|
|
116
|
+
memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
|
|
117
|
+
trustZones: { m1: "trusted" },
|
|
118
|
+
importance: { m1: 0.9 },
|
|
119
|
+
});
|
|
115
120
|
const result = await tryDirectAnswer({
|
|
116
|
-
query: "
|
|
121
|
+
query: "package manager remnic",
|
|
117
122
|
namespace: "default",
|
|
118
|
-
config:
|
|
123
|
+
config: BASE_CONFIG,
|
|
124
|
+
enabled: false,
|
|
119
125
|
sources,
|
|
120
126
|
});
|
|
121
127
|
assert.equal(result.eligible, false);
|
|
@@ -123,9 +129,9 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
|
|
|
123
129
|
assert.equal(sources.calls.listCandidates, 0);
|
|
124
130
|
});
|
|
125
131
|
|
|
126
|
-
test("tryDirectAnswer
|
|
127
|
-
//
|
|
128
|
-
//
|
|
132
|
+
test("tryDirectAnswer proceeds when `enabled: true` and candidates match", async () => {
|
|
133
|
+
// Same contract, positive case: when the resolved capability says enabled,
|
|
134
|
+
// the function proceeds to materialize candidates.
|
|
129
135
|
const sources = makeMockSources({
|
|
130
136
|
memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
|
|
131
137
|
trustZones: { m1: "trusted" },
|
|
@@ -134,7 +140,8 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
|
|
|
134
140
|
const result = await tryDirectAnswer({
|
|
135
141
|
query: "package manager remnic",
|
|
136
142
|
namespace: "default",
|
|
137
|
-
config: BASE_CONFIG,
|
|
143
|
+
config: BASE_CONFIG,
|
|
144
|
+
enabled: true,
|
|
138
145
|
sources,
|
|
139
146
|
});
|
|
140
147
|
assert.notEqual(result.reason, "disabled");
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Short-circuit contract:
|
|
16
16
|
*
|
|
17
|
-
* - When the resolved gate (`input.enabled
|
|
18
|
-
* `
|
|
17
|
+
* - When the resolved gate (`input.enabled`, projected from
|
|
18
|
+
* `caps.recallDirectAnswer` at the recall-operation entry) is `false`, the
|
|
19
|
+
* function returns the
|
|
19
20
|
* eligibility verdict with reason `"disabled"` without touching any source
|
|
20
21
|
* accessor. This is the documented default.
|
|
21
22
|
* - When enabled, the wiring cheaply drops non-trusted-zone memories
|
|
@@ -74,7 +75,6 @@ export interface DirectAnswerWiringInput {
|
|
|
74
75
|
namespace: string;
|
|
75
76
|
config: Pick<
|
|
76
77
|
PluginConfig,
|
|
77
|
-
| "recallDirectAnswerEnabled"
|
|
78
78
|
| "recallDirectAnswerTokenOverlapFloor"
|
|
79
79
|
| "recallDirectAnswerImportanceFloor"
|
|
80
80
|
| "recallDirectAnswerAmbiguityMargin"
|
|
@@ -82,13 +82,12 @@ export interface DirectAnswerWiringInput {
|
|
|
82
82
|
>;
|
|
83
83
|
/**
|
|
84
84
|
* Direct-answer capability gate, resolved once at the recall-operation entry
|
|
85
|
-
* (issue #1523
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* shape keep identical behavior.
|
|
85
|
+
* via `resolveCapabilities(config).recallDirectAnswer` (issue #1523). This
|
|
86
|
+
* is the sole gate — the function no longer reads the config flag directly.
|
|
87
|
+
* Callers MUST pass the resolved capability so the whole operation agrees on
|
|
88
|
+
* a single gate value.
|
|
90
89
|
*/
|
|
91
|
-
enabled
|
|
90
|
+
enabled: boolean;
|
|
92
91
|
sources: DirectAnswerSources;
|
|
93
92
|
queryEntityRefs?: string[];
|
|
94
93
|
abortSignal?: AbortSignal;
|
|
@@ -105,10 +104,7 @@ export async function tryDirectAnswer(
|
|
|
105
104
|
const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;
|
|
106
105
|
|
|
107
106
|
const eligibilityConfig: DirectAnswerConfig = {
|
|
108
|
-
|
|
109
|
-
// flag so callers on the old input shape (config-only, no `enabled`) get
|
|
110
|
-
// identical gating (issue #1523 backward-compat).
|
|
111
|
-
enabled: enabled ?? config.recallDirectAnswerEnabled,
|
|
107
|
+
enabled,
|
|
112
108
|
tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,
|
|
113
109
|
importanceFloor: config.recallDirectAnswerImportanceFloor,
|
|
114
110
|
ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,
|
|
@@ -25,6 +25,7 @@ import { Orchestrator } from "./orchestrator.js";
|
|
|
25
25
|
import { readEdges } from "./graph.js";
|
|
26
26
|
import type { ExtractionResult, PluginConfig } from "./types.js";
|
|
27
27
|
import type { LocalLlmClient } from "./local-llm.js";
|
|
28
|
+
import type { BufferTurn, ConversationThread } from "./types.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Stub local LLM whose `chatCompletion` only answers faithfulness-gate calls
|
|
@@ -42,15 +43,20 @@ function faithfulnessStubLocalLlm(
|
|
|
42
43
|
) => {
|
|
43
44
|
if (options.operation !== "extraction-faithfulness") return null;
|
|
44
45
|
const userMsg = messages.find((m) => m.role === "user");
|
|
45
|
-
// The prompt emits `FACT: "<text>"`
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
46
|
+
// The batch prompt emits one `FACT: "<text>"` line per fact, each
|
|
47
|
+
// followed by `Respond with the JSON array entry for index <i>`. The
|
|
48
|
+
// gate maps verdicts back to facts by that index, so return one entry
|
|
49
|
+
// per fact in prompt order. (Single-fact callers still get one entry.)
|
|
50
|
+
const factMatches = [...(userMsg?.content ?? "").matchAll(/FACT: "([^"]*)"/g)];
|
|
51
|
+
const entries = factMatches.map((m, i) => ({
|
|
52
|
+
index: i,
|
|
53
|
+
verdict: verdictForFactContent(m[1] ?? ""),
|
|
54
|
+
rationale: "stub verdict",
|
|
55
|
+
}));
|
|
50
56
|
return {
|
|
51
|
-
content: JSON.stringify(
|
|
52
|
-
{ index: 0, verdict, rationale: "stub
|
|
53
|
-
|
|
57
|
+
content: JSON.stringify(
|
|
58
|
+
entries.length > 0 ? entries : [{ index: 0, verdict: "entailed" as const, rationale: "stub" }],
|
|
59
|
+
),
|
|
54
60
|
};
|
|
55
61
|
},
|
|
56
62
|
} as unknown as LocalLlmClient;
|
|
@@ -250,3 +256,203 @@ test("pending_review fact gets no graph edge; an active fact with the same entit
|
|
|
250
256
|
await rm(memoryDir, { recursive: true, force: true });
|
|
251
257
|
}
|
|
252
258
|
});
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Thread-enabled harness for issue #1635: time + causal graph ON (so
|
|
263
|
+
* thread-predecessor edges are built), faithfulness gate in enforce mode.
|
|
264
|
+
* Entity graph is left OFF to isolate the thread-episode predecessor
|
|
265
|
+
* mechanism (entity-sibling edges are a separate, out-of-scope path).
|
|
266
|
+
*/
|
|
267
|
+
async function makeThreadHarness(): Promise<OrchHarness> {
|
|
268
|
+
const memoryDir = await mkdtemp(
|
|
269
|
+
path.join(os.tmpdir(), "remnic-faithfulness-thread-"),
|
|
270
|
+
);
|
|
271
|
+
const config: PluginConfig = parseConfig({
|
|
272
|
+
openaiApiKey: "sk-test",
|
|
273
|
+
memoryDir,
|
|
274
|
+
workspaceDir: path.join(memoryDir, "workspace"),
|
|
275
|
+
multiGraphMemoryEnabled: true,
|
|
276
|
+
entityGraphEnabled: false,
|
|
277
|
+
timeGraphEnabled: true,
|
|
278
|
+
causalGraphEnabled: true,
|
|
279
|
+
extractionFaithfulnessGate: "enforce",
|
|
280
|
+
extractionFaithfulnessContextChars: 400,
|
|
281
|
+
extractionFaithfulnessTimeoutMs: 2000,
|
|
282
|
+
extractionJudgeEnabled: false,
|
|
283
|
+
temporalSupersessionEnabled: false,
|
|
284
|
+
contradictionDetectionEnabled: false,
|
|
285
|
+
chunkingEnabled: false,
|
|
286
|
+
embeddingFallbackEnabled: false,
|
|
287
|
+
qmdEnabled: false,
|
|
288
|
+
extractionMinChars: 0,
|
|
289
|
+
extractionMinImportanceLevel: "trivial",
|
|
290
|
+
inlineSourceAttributionEnabled: false,
|
|
291
|
+
});
|
|
292
|
+
const orchestrator = new Orchestrator(config);
|
|
293
|
+
return { orchestrator, memoryDir };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** ExtractionResult with two facts (order preserved by the persist loop). */
|
|
297
|
+
function twoFactResult(
|
|
298
|
+
contentA: string,
|
|
299
|
+
entityRefA: string,
|
|
300
|
+
contentB: string,
|
|
301
|
+
entityRefB: string,
|
|
302
|
+
): ExtractionResult {
|
|
303
|
+
return {
|
|
304
|
+
facts: [
|
|
305
|
+
{ content: contentA, category: "fact", tags: [], confidence: 0.9, entityRef: entityRefA },
|
|
306
|
+
{ content: contentB, category: "fact", tags: [], confidence: 0.9, entityRef: entityRefB },
|
|
307
|
+
],
|
|
308
|
+
entities: [],
|
|
309
|
+
relationships: [],
|
|
310
|
+
questions: [],
|
|
311
|
+
profileUpdates: [],
|
|
312
|
+
} as unknown as ExtractionResult;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface StorageLike {
|
|
316
|
+
ensureDirectories: () => Promise<void>;
|
|
317
|
+
dir: string;
|
|
318
|
+
readAllMemories: () => Promise<
|
|
319
|
+
Array<{ path: string; frontmatter: { id: string; status?: string } }>
|
|
320
|
+
>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interface PersistExtractionSig {
|
|
324
|
+
(
|
|
325
|
+
r: ExtractionResult,
|
|
326
|
+
s: unknown,
|
|
327
|
+
t: string | null,
|
|
328
|
+
ctx: unknown,
|
|
329
|
+
bn: unknown,
|
|
330
|
+
plan: unknown,
|
|
331
|
+
src: string,
|
|
332
|
+
): Promise<string[]>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
test("pending_review id must NOT enter persisted thread episodes across a cross-flush (#1635)", async () => {
|
|
336
|
+
const { orchestrator, memoryDir } = await makeThreadHarness();
|
|
337
|
+
try {
|
|
338
|
+
// fact content containing "closed permanently" → unsupported → pending_review.
|
|
339
|
+
(orchestrator as unknown as { localLlm: LocalLlmClient }).localLlm =
|
|
340
|
+
faithfulnessStubLocalLlm((c) =>
|
|
341
|
+
c.includes("closed permanently") ? "unsupported" : "entailed",
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const getStorage = orchestrator as unknown as {
|
|
345
|
+
getStorage: (ns: string) => Promise<StorageLike>;
|
|
346
|
+
};
|
|
347
|
+
const storage = await getStorage.getStorage("default");
|
|
348
|
+
await storage.ensureDirectories();
|
|
349
|
+
|
|
350
|
+
// Establish a real thread (mirrors runExtraction's processTurn step) so
|
|
351
|
+
// appendEpisodeIds has a thread file to write episode ids into.
|
|
352
|
+
const threading = orchestrator as unknown as {
|
|
353
|
+
threading: {
|
|
354
|
+
processTurn: (turn: BufferTurn, ids: string[]) => Promise<string>;
|
|
355
|
+
loadThread: (id: string) => Promise<ConversationThread | null>;
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
const seedTurn: BufferTurn = {
|
|
359
|
+
role: "user",
|
|
360
|
+
content: "Tell me about the widget factory.",
|
|
361
|
+
timestamp: "2026-07-06T10:00:00.000Z",
|
|
362
|
+
sessionKey: "agent:test:main",
|
|
363
|
+
};
|
|
364
|
+
const threadId = await threading.threading.processTurn(seedTurn, []);
|
|
365
|
+
assert.ok(threadId, "thread established via processTurn");
|
|
366
|
+
|
|
367
|
+
const persist = orchestrator as unknown as {
|
|
368
|
+
persistExtraction: PersistExtractionSig;
|
|
369
|
+
appendPersistedThreadEpisodes: (
|
|
370
|
+
threadId: string,
|
|
371
|
+
persistedIds: string[],
|
|
372
|
+
) => Promise<void>;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const sourceText =
|
|
376
|
+
"The widget factory reopened this quarter after being closed permanently last year.";
|
|
377
|
+
|
|
378
|
+
// --- Flush 1: an active fact + a pending_review fact, same thread ---
|
|
379
|
+
const flush1 = await persist.persistExtraction(
|
|
380
|
+
twoFactResult(
|
|
381
|
+
"The widget factory reopened this quarter.", "wf-active",
|
|
382
|
+
"The widget factory closed permanently last year.", "wf-pending",
|
|
383
|
+
),
|
|
384
|
+
storage,
|
|
385
|
+
threadId,
|
|
386
|
+
{ sessionKey: "agent:test:main", principal: "test" },
|
|
387
|
+
undefined,
|
|
388
|
+
null,
|
|
389
|
+
sourceText,
|
|
390
|
+
);
|
|
391
|
+
const [activeId1, pendingId] = flush1;
|
|
392
|
+
assert.ok(activeId1, "flush1 active fact persisted");
|
|
393
|
+
assert.ok(pendingId, "flush1 pending_review fact persisted");
|
|
394
|
+
|
|
395
|
+
// runExtraction's append step (the real helper; filters pending_review).
|
|
396
|
+
await persist.appendPersistedThreadEpisodes(threadId, flush1);
|
|
397
|
+
|
|
398
|
+
// PART (a): the thread FILE must hold the active id but NOT the pending id.
|
|
399
|
+
const threadAfter1 = await threading.threading.loadThread(threadId);
|
|
400
|
+
assert.ok(threadAfter1, "thread file readable after flush 1");
|
|
401
|
+
assert.ok(
|
|
402
|
+
threadAfter1!.episodeIds.includes(activeId1),
|
|
403
|
+
"active fact id is in persisted thread episodes",
|
|
404
|
+
);
|
|
405
|
+
assert.ok(
|
|
406
|
+
!threadAfter1!.episodeIds.includes(pendingId),
|
|
407
|
+
"pending_review id must NOT enter persisted thread episodes (#1635)",
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// The pending fact IS durable (with status pending_review) — proves the
|
|
411
|
+
// gate routed it correctly; the guard is about its absence from the thread.
|
|
412
|
+
const memsAfter1 = await storage.readAllMemories();
|
|
413
|
+
const pendingMem = memsAfter1.find((m) => m.frontmatter.id === pendingId);
|
|
414
|
+
assert.ok(pendingMem, "pending_review fact is durable");
|
|
415
|
+
assert.equal(
|
|
416
|
+
pendingMem!.frontmatter.status,
|
|
417
|
+
"pending_review",
|
|
418
|
+
"unsupported fact is pending_review under enforce",
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// --- Flush 2: an active fact in the SAME thread ---
|
|
422
|
+
const flush2 = await persist.persistExtraction(
|
|
423
|
+
factResult("The widget factory hired ten new engineers.", "wf-active"),
|
|
424
|
+
storage,
|
|
425
|
+
threadId,
|
|
426
|
+
{ sessionKey: "agent:test:main", principal: "test" },
|
|
427
|
+
undefined,
|
|
428
|
+
null,
|
|
429
|
+
sourceText,
|
|
430
|
+
);
|
|
431
|
+
const activeId2 = flush2[0];
|
|
432
|
+
assert.ok(activeId2, "flush2 active fact persisted");
|
|
433
|
+
await persist.appendPersistedThreadEpisodes(threadId, flush2);
|
|
434
|
+
|
|
435
|
+
// PART (a)+(b): no predecessor edge may reference the pending fact. Under
|
|
436
|
+
// the bug, flush 1 would have appended pendingId to the thread file, flush
|
|
437
|
+
// 2's loadThread would re-seed it into threadEpisodeIdsForGraph, and the
|
|
438
|
+
// active fact2 would build a time/causal predecessor edge to it.
|
|
439
|
+
const pendingPath = path.relative(memoryDir, pendingMem!.path);
|
|
440
|
+
for (const gtype of ["time", "causal"] as const) {
|
|
441
|
+
const edges = await readEdges(memoryDir, gtype);
|
|
442
|
+
assert.ok(
|
|
443
|
+
!edges.some((e) => e.from === pendingPath || e.to === pendingPath),
|
|
444
|
+
`no ${gtype} edge references the pending_review fact path (${pendingPath}); got ${edges.length} edges`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Control: the graph + threading are wired — the active fact2 builds a
|
|
449
|
+
// time predecessor edge to the active fact1 (which IS in the thread file).
|
|
450
|
+
const timeEdges = await readEdges(memoryDir, "time");
|
|
451
|
+
assert.ok(
|
|
452
|
+
timeEdges.length > 0,
|
|
453
|
+
"active fact2 builds a time predecessor edge (control: graph+threading wired)",
|
|
454
|
+
);
|
|
455
|
+
} finally {
|
|
456
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
457
|
+
}
|
|
458
|
+
});
|