recallnest 2.2.1 → 2.5.4
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/CHANGELOG.md +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach, beforeEach } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import * as envConfig from "../env-config.js";
|
|
4
|
+
|
|
5
|
+
// Every RECALLNEST_* key this suite mutates. Snapshot + restore so we never leak
|
|
6
|
+
// env state into sibling test files (the whole `bun test` run shares one process).
|
|
7
|
+
const KEYS = [
|
|
8
|
+
"RECALLNEST_MULTI_VECTOR",
|
|
9
|
+
"RECALLNEST_EMOTION_SCORING",
|
|
10
|
+
"RECALLNEST_PREDICTIVE_MEMORY",
|
|
11
|
+
"RECALLNEST_SYNTHESIZE",
|
|
12
|
+
"RECALLNEST_LLM_CONSOLIDATION",
|
|
13
|
+
"RECALLNEST_CONSTRUCTIVE_RETRIEVAL",
|
|
14
|
+
"RECALLNEST_NARRATIVE_MODE",
|
|
15
|
+
"RECALLNEST_KG_MODE",
|
|
16
|
+
"RECALLNEST_CORE_SUMMARY",
|
|
17
|
+
"RECALLNEST_ERROR_SIGNATURE_BOOST",
|
|
18
|
+
"RECALLNEST_USAGE_DECAY",
|
|
19
|
+
"RECALLNEST_DATA_DIR",
|
|
20
|
+
"RECALLNEST_MCP_TIER",
|
|
21
|
+
"RECALLNEST_RECALL_MODE",
|
|
22
|
+
"RECALLNEST_UI_PORT",
|
|
23
|
+
"RECALLNEST_API_PORT",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const saved: Record<string, string | undefined> = {};
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
for (const key of KEYS) {
|
|
30
|
+
saved[key] = process.env[key];
|
|
31
|
+
delete process.env[key];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
for (const key of KEYS) {
|
|
37
|
+
if (saved[key] === undefined) delete process.env[key];
|
|
38
|
+
else process.env[key] = saved[key];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('env-config boolean flags — strict === "true"', () => {
|
|
43
|
+
const accessors: Array<[string, () => boolean, string]> = [
|
|
44
|
+
["multiVector", envConfig.multiVector, "RECALLNEST_MULTI_VECTOR"],
|
|
45
|
+
["emotionScoring", envConfig.emotionScoring, "RECALLNEST_EMOTION_SCORING"],
|
|
46
|
+
["predictiveMemory", envConfig.predictiveMemory, "RECALLNEST_PREDICTIVE_MEMORY"],
|
|
47
|
+
["synthesize", envConfig.synthesize, "RECALLNEST_SYNTHESIZE"],
|
|
48
|
+
["llmConsolidation", envConfig.llmConsolidation, "RECALLNEST_LLM_CONSOLIDATION"],
|
|
49
|
+
["constructiveRetrieval", envConfig.constructiveRetrieval, "RECALLNEST_CONSTRUCTIVE_RETRIEVAL"],
|
|
50
|
+
["narrativeMode", envConfig.narrativeMode, "RECALLNEST_NARRATIVE_MODE"],
|
|
51
|
+
["kgMode", envConfig.kgMode, "RECALLNEST_KG_MODE"],
|
|
52
|
+
["coreSummary", envConfig.coreSummary, "RECALLNEST_CORE_SUMMARY"],
|
|
53
|
+
["errorSignatureBoost", envConfig.errorSignatureBoost, "RECALLNEST_ERROR_SIGNATURE_BOOST"],
|
|
54
|
+
["usageDecay", envConfig.usageDecay, "RECALLNEST_USAGE_DECAY"],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const [name, accessor, key] of accessors) {
|
|
58
|
+
it(`${name}: false unless the value is exactly "true"`, () => {
|
|
59
|
+
delete process.env[key];
|
|
60
|
+
expect(accessor()).toBe(false);
|
|
61
|
+
|
|
62
|
+
process.env[key] = "true";
|
|
63
|
+
expect(accessor()).toBe(true);
|
|
64
|
+
|
|
65
|
+
// No trim, no case-folding, no truthy coercion — only the literal "true".
|
|
66
|
+
for (const fuzzy of ["True", "TRUE", " true ", "1", "yes", "false", ""]) {
|
|
67
|
+
process.env[key] = fuzzy;
|
|
68
|
+
expect(accessor()).toBe(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("env-config string settings — || default (empty string falls through)", () => {
|
|
75
|
+
it('dataDir defaults to "data" when unset or empty', () => {
|
|
76
|
+
delete process.env.RECALLNEST_DATA_DIR;
|
|
77
|
+
expect(envConfig.dataDir()).toBe("data");
|
|
78
|
+
|
|
79
|
+
process.env.RECALLNEST_DATA_DIR = ""; // || not ??
|
|
80
|
+
expect(envConfig.dataDir()).toBe("data");
|
|
81
|
+
|
|
82
|
+
process.env.RECALLNEST_DATA_DIR = "/tmp/custom-data";
|
|
83
|
+
expect(envConfig.dataDir()).toBe("/tmp/custom-data");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('mcpTier defaults to "advanced"; valid passes through; illegal value kept verbatim', () => {
|
|
87
|
+
delete process.env.RECALLNEST_MCP_TIER;
|
|
88
|
+
expect(envConfig.mcpTier()).toBe("advanced");
|
|
89
|
+
|
|
90
|
+
process.env.RECALLNEST_MCP_TIER = ""; // || not ??
|
|
91
|
+
expect(envConfig.mcpTier()).toBe("advanced");
|
|
92
|
+
|
|
93
|
+
process.env.RECALLNEST_MCP_TIER = "core";
|
|
94
|
+
expect(envConfig.mcpTier()).toBe("core");
|
|
95
|
+
|
|
96
|
+
// No runtime validation — an unknown non-empty value is preserved as-is
|
|
97
|
+
// (downstream shouldRegisterTool relies on this; do not coerce to default).
|
|
98
|
+
process.env.RECALLNEST_MCP_TIER = "garbage";
|
|
99
|
+
expect(envConfig.mcpTier()).toBe("garbage");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("env-config raw passthrough — caller validates / clamps / falls back", () => {
|
|
104
|
+
const rawAccessors: Array<[string, () => string | undefined, string]> = [
|
|
105
|
+
["recallModeRaw", envConfig.recallModeRaw, "RECALLNEST_RECALL_MODE"],
|
|
106
|
+
["uiPortRaw", envConfig.uiPortRaw, "RECALLNEST_UI_PORT"],
|
|
107
|
+
["apiPortRaw", envConfig.apiPortRaw, "RECALLNEST_API_PORT"],
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
for (const [name, accessor, key] of rawAccessors) {
|
|
111
|
+
it(`${name}: returns the raw env value, undefined when unset`, () => {
|
|
112
|
+
delete process.env[key];
|
|
113
|
+
expect(accessor()).toBeUndefined();
|
|
114
|
+
|
|
115
|
+
process.env[key] = "verbatim-123";
|
|
116
|
+
expect(accessor()).toBe("verbatim-123");
|
|
117
|
+
|
|
118
|
+
// Raw passthrough: empty string is NOT defaulted here — the caller decides.
|
|
119
|
+
process.env[key] = "";
|
|
120
|
+
expect(accessor()).toBe("");
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("env-config recallModeRaw — must not pre-default (preserves resolveRecallMode config fallback)", () => {
|
|
126
|
+
it('returns the raw value or undefined, never a hardcoded mode like "summary"', () => {
|
|
127
|
+
delete process.env.RECALLNEST_RECALL_MODE;
|
|
128
|
+
// Must be undefined, NOT "summary" — resolveRecallMode owns the config fallback.
|
|
129
|
+
expect(envConfig.recallModeRaw()).toBeUndefined();
|
|
130
|
+
|
|
131
|
+
process.env.RECALLNEST_RECALL_MODE = "light";
|
|
132
|
+
expect(envConfig.recallModeRaw()).toBe("light");
|
|
133
|
+
|
|
134
|
+
// Invalid modes pass through verbatim — the whitelist check lives in resolveRecallMode.
|
|
135
|
+
process.env.RECALLNEST_RECALL_MODE = "bogus";
|
|
136
|
+
expect(envConfig.recallModeRaw()).toBe("bogus");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("env-config accessors are lazy (read at call time, never frozen)", () => {
|
|
141
|
+
it("reflect runtime mutation rather than a module-load snapshot", () => {
|
|
142
|
+
delete process.env.RECALLNEST_EMOTION_SCORING;
|
|
143
|
+
expect(envConfig.emotionScoring()).toBe(false);
|
|
144
|
+
|
|
145
|
+
process.env.RECALLNEST_EMOTION_SCORING = "true";
|
|
146
|
+
expect(envConfig.emotionScoring()).toBe(true); // would fail if cached at import
|
|
147
|
+
|
|
148
|
+
delete process.env.RECALLNEST_EMOTION_SCORING;
|
|
149
|
+
expect(envConfig.emotionScoring()).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach, beforeEach } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { applyErrorSignatureBoost } from "../retriever.js";
|
|
4
|
+
import type { RetrievalResult } from "../retriever.js";
|
|
5
|
+
import { extractErrorSignatures } from "../error-signature.js";
|
|
6
|
+
|
|
7
|
+
const FLAG = "RECALLNEST_ERROR_SIGNATURE_BOOST";
|
|
8
|
+
|
|
9
|
+
function makeResult(opts: { category: string; score: number; sigs?: string[] }): RetrievalResult {
|
|
10
|
+
const metadata = opts.sigs ? JSON.stringify({ error_signature: opts.sigs }) : "{}";
|
|
11
|
+
return {
|
|
12
|
+
entry: {
|
|
13
|
+
id: "m1",
|
|
14
|
+
text: "x",
|
|
15
|
+
scope: "test",
|
|
16
|
+
category: opts.category,
|
|
17
|
+
timestamp: "2026-06-13T00:00:00.000Z",
|
|
18
|
+
metadata,
|
|
19
|
+
},
|
|
20
|
+
score: opts.score,
|
|
21
|
+
sources: {},
|
|
22
|
+
} as unknown as RetrievalResult;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let saved: string | undefined;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
saved = process.env[FLAG];
|
|
28
|
+
delete process.env[FLAG];
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (saved === undefined) delete process.env[FLAG];
|
|
32
|
+
else process.env[FLAG] = saved;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("applyErrorSignatureBoost — A1 检索端精确召回 boost", () => {
|
|
36
|
+
const QUERY = "build broke: xmlsec1 not found";
|
|
37
|
+
const matchingSig = () => extractErrorSignatures({ problem: QUERY })[0];
|
|
38
|
+
|
|
39
|
+
it("flag off(默认)→ 原样返回同一引用、分数不变", () => {
|
|
40
|
+
const input = [makeResult({ category: "cases", score: 0.4, sigs: [matchingSig()] })];
|
|
41
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
42
|
+
expect(out).toBe(input); // no-op,bit-identical
|
|
43
|
+
expect(out[0].score).toBe(0.4);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("flag on + 指纹命中 case → 乘法 boost (×1.5)", () => {
|
|
47
|
+
process.env[FLAG] = "true";
|
|
48
|
+
const input = [makeResult({ category: "cases", score: 0.4, sigs: [matchingSig()] })];
|
|
49
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
50
|
+
expect(out[0].score).toBeCloseTo(0.6, 6); // 0.4 * 1.5
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("flag on + 命中但高分 → clamp 到 1、不溢出", () => {
|
|
54
|
+
process.env[FLAG] = "true";
|
|
55
|
+
const input = [makeResult({ category: "cases", score: 0.8, sigs: [matchingSig()] })];
|
|
56
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
57
|
+
expect(out[0].score).toBe(1); // 0.8 * 1.5 = 1.2 → clamp 1
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("flag on + 非 case category(patterns)→ 不 boost", () => {
|
|
61
|
+
process.env[FLAG] = "true";
|
|
62
|
+
const input = [makeResult({ category: "patterns", score: 0.4, sigs: [matchingSig()] })];
|
|
63
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
64
|
+
expect(out[0].score).toBe(0.4);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("flag on + case 但指纹不重叠 → 不 boost", () => {
|
|
68
|
+
process.env[FLAG] = "true";
|
|
69
|
+
const input = [makeResult({ category: "cases", score: 0.4, sigs: ["zzz_nonexistent_signature_xyz"] })];
|
|
70
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
71
|
+
expect(out[0].score).toBe(0.4);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("flag on + case 无 error_signature metadata → 不 boost", () => {
|
|
75
|
+
process.env[FLAG] = "true";
|
|
76
|
+
const input = [makeResult({ category: "cases", score: 0.4 })];
|
|
77
|
+
const out = applyErrorSignatureBoost(input, QUERY);
|
|
78
|
+
expect(out[0].score).toBe(0.4);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("flag on + query 抽不出错误指纹 → 原样返回同一引用", () => {
|
|
82
|
+
process.env[FLAG] = "true";
|
|
83
|
+
const input = [makeResult({ category: "cases", score: 0.4, sigs: [matchingSig()] })];
|
|
84
|
+
const out = applyErrorSignatureBoost(input, "今天天气不错,喝杯咖啡");
|
|
85
|
+
expect(out).toBe(input);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { extractErrorSignatures, MAX_SIGNATURES } from "../error-signature.js";
|
|
3
|
+
|
|
4
|
+
describe("extractErrorSignatures — English patterns", () => {
|
|
5
|
+
test("bare 'X not found' (no colon) is extracted — MemOS 原 6 正则会漏", () => {
|
|
6
|
+
const sigs = extractErrorSignatures({ problem: "Build broke: xmlsec1 not found" });
|
|
7
|
+
expect(sigs.some(s => s.includes("xmlsec1 not found"))).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("'exit code N' is extracted", () => {
|
|
11
|
+
const sigs = extractErrorSignatures({ problem: "Deploy failed with exit code 1" });
|
|
12
|
+
expect(sigs.some(s => s.includes("exit code 1"))).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("'<Name>Error: body' extracted and ranks top by specificity", () => {
|
|
16
|
+
const sigs = extractErrorSignatures({ problem: "NameError: name foo is not defined" });
|
|
17
|
+
expect(sigs.some(s => s.includes("NameError"))).toBe(true);
|
|
18
|
+
expect(sigs[0]).toContain("NameError");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("'No such file or directory' standalone is extracted (case-insensitive)", () => {
|
|
22
|
+
const sigs = extractErrorSignatures({ problem: "open config: No such file or directory" });
|
|
23
|
+
expect(sigs.some(s => /No such file or directory/i.test(s))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("bare 3-digit numbers in benign text do NOT false-match as HTTP error", () => {
|
|
27
|
+
const sigs = extractErrorSignatures({ problem: "deployed 500 records and processed 404 rows in the weekly batch" });
|
|
28
|
+
expect(sigs.some(s => s.includes("500 records") || s.includes("404 rows"))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("real HTTP status phrase is still extracted after tightening", () => {
|
|
32
|
+
const sigs = extractErrorSignatures({ problem: "API call returned 500 Internal Server Error on submit" });
|
|
33
|
+
expect(sigs.some(s => s.includes("500 Internal"))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("extractErrorSignatures — Chinese (MemOS 原版会被 alpha 检查滤掉)", () => {
|
|
38
|
+
test("中文错误现象被抽取", () => {
|
|
39
|
+
const sigs = extractErrorSignatures({ problem: "部署时权限被拒绝,找不到文件" });
|
|
40
|
+
expect(sigs.some(s => s.includes("找不到") || s.includes("权限被拒"))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("中文短片段(<6 字符但 ≥3 中文字)保留", () => {
|
|
44
|
+
const sigs = extractErrorSignatures({ problem: "找不到模块" });
|
|
45
|
+
expect(sigs.length).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("extractErrorSignatures — input gating", () => {
|
|
50
|
+
test("outcome 成功态(无失败词)不被纳入,不抽反向信号", () => {
|
|
51
|
+
const sigs = extractErrorSignatures({
|
|
52
|
+
problem: "ImportError: no module named requests",
|
|
53
|
+
outcome: "exit code 0 部署成功",
|
|
54
|
+
});
|
|
55
|
+
expect(sigs.some(s => s.includes("ImportError"))).toBe(true);
|
|
56
|
+
expect(sigs.some(s => s.includes("exit code 0"))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("context 参与抽取(原始 stderr 常在 context)", () => {
|
|
60
|
+
const sigs = extractErrorSignatures({
|
|
61
|
+
problem: "服务起不来",
|
|
62
|
+
context: "stderr: pg_config: command not found",
|
|
63
|
+
});
|
|
64
|
+
expect(sigs.some(s => s.includes("pg_config") || s.includes("command not found"))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("结果不超过 MAX_SIGNATURES 条", () => {
|
|
68
|
+
const sigs = extractErrorSignatures({
|
|
69
|
+
problem: "NameError: a. TypeError: b. ValueError: c. KeyError: d. exit code 1. 404 Not Found. 权限被拒绝.",
|
|
70
|
+
});
|
|
71
|
+
expect(sigs.length).toBeLessThanOrEqual(MAX_SIGNATURES);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("extractErrorSignatures — 过泛中文词收紧(P3-A 检索端噪声治理)", () => {
|
|
76
|
+
const BARE_GENERIC = /^(?:报错|出错|错误|异常|失败|超时|崩溃|无法|未定义|不能)[了的过着吧呢啊啦哦呀嘛。,!?!?,\s]*$/;
|
|
77
|
+
|
|
78
|
+
test("裸过泛词 + 语气助词不被抽成指纹(失败了 / 超时了 / 崩溃了 / 报错了)", () => {
|
|
79
|
+
for (const bare of ["部署失败了", "请求超时了", "程序崩溃了", "系统报错了"]) {
|
|
80
|
+
const sigs = extractErrorSignatures({ problem: bare });
|
|
81
|
+
expect(sigs.some(s => BARE_GENERIC.test(s.trim()))).toBe(false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("过泛词带具体锚点(英文 / 路径 / 多位数字)时保留", () => {
|
|
86
|
+
const sigs = extractErrorSignatures({ problem: "操作失败:xmlsec1 not found" });
|
|
87
|
+
expect(sigs.some(s => s.includes("xmlsec1"))).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("具体中文短语不受收紧影响(找不到 / 权限被拒 / 连接失败)", () => {
|
|
91
|
+
expect(extractErrorSignatures({ problem: "找不到模块" }).length).toBeGreaterThan(0);
|
|
92
|
+
expect(extractErrorSignatures({ problem: "权限被拒绝" }).some(s => s.includes("权限被拒"))).toBe(true);
|
|
93
|
+
expect(extractErrorSignatures({ problem: "数据库连接失败" }).some(s => s.includes("连接失败"))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { classifyError, toErrorResult, runToolSafely } from "../error-taxonomy.js";
|
|
4
|
+
|
|
5
|
+
// P-error-taxonomy: 借鉴 RepoPrompt CE 的 error taxonomy。
|
|
6
|
+
// 每种失败标 {reasonCode, retryable, responsibility},让 agent 判断重试/改参/上报。
|
|
7
|
+
|
|
8
|
+
describe("classifyError", () => {
|
|
9
|
+
it("classifies transport errors as retryable / transport", () => {
|
|
10
|
+
for (const m of ["ECONNRESET", "socket hang up", "fetch failed", "request timed out", "ETIMEDOUT"]) {
|
|
11
|
+
const c = classifyError(new Error(m));
|
|
12
|
+
expect(c.reasonCode).toBe("transport_error");
|
|
13
|
+
expect(c.retryable).toBe(true);
|
|
14
|
+
expect(c.responsibility).toBe("transport");
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("classifies rate-limit / overload as retryable / peer", () => {
|
|
19
|
+
for (const m of ["429 Too Many Requests", "rate limit exceeded", "529 overloaded", "quota exceeded"]) {
|
|
20
|
+
const c = classifyError(new Error(m));
|
|
21
|
+
expect(c.reasonCode).toBe("upstream_overloaded");
|
|
22
|
+
expect(c.retryable).toBe(true);
|
|
23
|
+
expect(c.responsibility).toBe("peer");
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("classifies auth failures as non-retryable / host", () => {
|
|
28
|
+
const c = classifyError(new Error("401 Unauthorized: invalid api key"));
|
|
29
|
+
expect(c.reasonCode).toBe("upstream_auth");
|
|
30
|
+
expect(c.retryable).toBe(false);
|
|
31
|
+
expect(c.responsibility).toBe("host");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("classifies missing-scope as non-retryable / peer", () => {
|
|
35
|
+
const c = classifyError(new Error("search_memory requires a scope. Pass scope explicitly, provide sessionId..."));
|
|
36
|
+
expect(c.reasonCode).toBe("scope_required");
|
|
37
|
+
expect(c.retryable).toBe(false);
|
|
38
|
+
expect(c.responsibility).toBe("peer");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("classifies validation errors as invalid_input / peer", () => {
|
|
42
|
+
const c = classifyError(new Error("validation failed: expected string received number"));
|
|
43
|
+
expect(c.reasonCode).toBe("invalid_input");
|
|
44
|
+
expect(c.responsibility).toBe("peer");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("classifies store errors as app", () => {
|
|
48
|
+
const c = classifyError(new Error("LanceDB table not found"));
|
|
49
|
+
expect(c.reasonCode).toBe("store_error");
|
|
50
|
+
expect(c.responsibility).toBe("app");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("classifies config/env-missing as non-retryable / host (P2-1/P2-3)", () => {
|
|
54
|
+
const c = classifyError(new Error("Environment variable OPENAI_API_KEY not set"));
|
|
55
|
+
expect(c.reasonCode).toBe("config_missing");
|
|
56
|
+
expect(c.retryable).toBe(false);
|
|
57
|
+
expect(c.responsibility).toBe("host");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("classifies bad memory references as invalid_memory_ref / peer — caller-side (P2-3)", () => {
|
|
61
|
+
for (const m of [
|
|
62
|
+
"Invalid memory ID format: bad",
|
|
63
|
+
'Ambiguous prefix "12345678" matches 2+ memories. Use a longer prefix or full ID.',
|
|
64
|
+
"Memory 12345678 is outside accessible scopes",
|
|
65
|
+
]) {
|
|
66
|
+
const c = classifyError(new Error(m));
|
|
67
|
+
expect(c.reasonCode).toBe("invalid_memory_ref");
|
|
68
|
+
expect(c.responsibility).toBe("peer");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back to internal_error / app / non-retryable for unknown errors", () => {
|
|
73
|
+
const c = classifyError(new Error("something totally unexpected happened"));
|
|
74
|
+
expect(c.reasonCode).toBe("internal_error");
|
|
75
|
+
expect(c.retryable).toBe(false);
|
|
76
|
+
expect(c.responsibility).toBe("app");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("handles non-Error throwables", () => {
|
|
80
|
+
const c = classifyError("plain string failure");
|
|
81
|
+
expect(c.reasonCode).toBe("internal_error");
|
|
82
|
+
expect(c.responsibility).toBe("app");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("toErrorResult", () => {
|
|
87
|
+
it("produces an isError result with a machine-readable classification block", () => {
|
|
88
|
+
const r = toErrorResult("search_memory", new Error("ECONNRESET"));
|
|
89
|
+
expect(r.isError).toBe(true);
|
|
90
|
+
expect(r.content[0].type).toBe("text");
|
|
91
|
+
const text = r.content[0].text;
|
|
92
|
+
expect(text).toContain('Tool "search_memory" failed');
|
|
93
|
+
expect(text).toContain("reason_code: transport_error");
|
|
94
|
+
expect(text).toContain("retryable: true");
|
|
95
|
+
expect(text).toContain("responsibility: transport");
|
|
96
|
+
expect(text).toContain("detail: ECONNRESET");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("preserves the original detail for unknown errors", () => {
|
|
100
|
+
const r = toErrorResult("forget_memory", new Error("boom"));
|
|
101
|
+
expect(r.isError).toBe(true);
|
|
102
|
+
expect(r.content[0].text).toContain("reason_code: internal_error");
|
|
103
|
+
expect(r.content[0].text).toContain("detail: boom");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("runToolSafely", () => {
|
|
108
|
+
it("returns handler result on success (pass-through)", async () => {
|
|
109
|
+
const ok = { content: [{ type: "text", text: "ok" }] };
|
|
110
|
+
const r = await runToolSafely("search_memory", async () => ok);
|
|
111
|
+
expect(r).toEqual(ok);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("catches handler throw and returns a classified isError result", async () => {
|
|
115
|
+
const r = await runToolSafely("search_memory", async () => { throw new Error("ECONNRESET"); }) as { isError: boolean; content: { text: string }[] };
|
|
116
|
+
expect(r.isError).toBe(true);
|
|
117
|
+
expect(r.content[0].text).toContain("reason_code: transport_error");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("catches lazy-init failure and classifies it (P2-1 regression lock)", async () => {
|
|
121
|
+
// 模拟 ensureComponents 抛错(缺 env var)——必须被分类返回 isError,而非裸抛绕过。
|
|
122
|
+
const r = await runToolSafely("store_memory", async () => {
|
|
123
|
+
throw new Error("Environment variable JINA_API_KEY not set");
|
|
124
|
+
}) as { isError: boolean; content: { text: string }[] };
|
|
125
|
+
expect(r.isError).toBe(true);
|
|
126
|
+
expect(r.content[0].text).toContain("reason_code: config_missing");
|
|
127
|
+
expect(r.content[0].text).toContain("responsibility: host");
|
|
128
|
+
});
|
|
129
|
+
});
|