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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promote-Scan: auto-promotion pipeline for transcript-downgraded stable facts.
|
|
3
|
+
*
|
|
4
|
+
* Transcript ingest downgrades profile/preferences into `events` evidence
|
|
5
|
+
* (memory-boundaries.ts:resolveIngestBoundary), tagging boundary.downgradedFrom.
|
|
6
|
+
* Without an automatic promotion path those high-signal facts stay sparse in
|
|
7
|
+
* durable memory. This module clusters recurring downgraded evidence and, when a
|
|
8
|
+
* cluster recurs often enough at high enough importance, promotes the seed back
|
|
9
|
+
* to its original durable category via the injected `promote` callback.
|
|
10
|
+
*
|
|
11
|
+
* Idempotency is delegated to promoteMemory -> writeDurableEntry's canonicalKey
|
|
12
|
+
* dedup, so re-scanning is safe (we do not mutate source evidence — see #19
|
|
13
|
+
* non-atomic store.update — and rely on dedup instead of a promotedTo marker).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { MemoryEntry, MemoryStore } from "./store.js";
|
|
17
|
+
import type { DurableMemoryCategory, StoredPromotedMemoryRecord } from "./memory-schema.js";
|
|
18
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
19
|
+
import { isNoise } from "./noise-filter.js";
|
|
20
|
+
import { extractBoundaryMetadata, parseMetadataObject } from "./memory-boundaries.js";
|
|
21
|
+
import { greedyCluster } from "./skill-promotion.js";
|
|
22
|
+
import { promoteMemory, type PersistMemoryDeps } from "./capture-engine.js";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Categories that transcript ingest downgrades into evidence. */
|
|
29
|
+
type PromotableDowngrade = "profile" | "preferences";
|
|
30
|
+
|
|
31
|
+
export interface PromoteRequest {
|
|
32
|
+
memoryId: string;
|
|
33
|
+
category: DurableMemoryCategory;
|
|
34
|
+
scope: string;
|
|
35
|
+
importance: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type PromoteFn = (req: PromoteRequest) => Promise<StoredPromotedMemoryRecord>;
|
|
39
|
+
|
|
40
|
+
export interface PromoteScanDeps {
|
|
41
|
+
store: Pick<MemoryStore, "list" | "getVectors">;
|
|
42
|
+
/** Injected so the scan unit-tests without the full persist pipeline. */
|
|
43
|
+
promote: PromoteFn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PromoteScanConfig {
|
|
47
|
+
/** Min cluster members before a downgraded fact is promoted (default 3). */
|
|
48
|
+
minOccurrences: number;
|
|
49
|
+
/** Min average cluster importance to promote (default 0.6). */
|
|
50
|
+
minImportance: number;
|
|
51
|
+
/** Cosine similarity threshold for greedy clustering (default 0.82). */
|
|
52
|
+
clusterThreshold: number;
|
|
53
|
+
/** Max events fetched from the store (default 2000). */
|
|
54
|
+
listLimit: number;
|
|
55
|
+
/** When true (default), find candidates without writing anything. */
|
|
56
|
+
dryRun: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_PROMOTE_SCAN_CONFIG: PromoteScanConfig = {
|
|
60
|
+
minOccurrences: 3,
|
|
61
|
+
minImportance: 0.6,
|
|
62
|
+
clusterThreshold: 0.82,
|
|
63
|
+
listLimit: 2000,
|
|
64
|
+
dryRun: true,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface PromoteCandidate {
|
|
68
|
+
downgradedFrom: PromotableDowngrade;
|
|
69
|
+
seedId: string;
|
|
70
|
+
seedText: string;
|
|
71
|
+
memberIds: string[];
|
|
72
|
+
occurrences: number;
|
|
73
|
+
avgImportance: number;
|
|
74
|
+
/** Filled after a real promotion; null in dryRun mode. */
|
|
75
|
+
promoted: StoredPromotedMemoryRecord | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PromoteScanResult {
|
|
79
|
+
candidates: PromoteCandidate[];
|
|
80
|
+
scannedEvidence: number;
|
|
81
|
+
clusters: number;
|
|
82
|
+
promoted: number;
|
|
83
|
+
dryRun: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Helpers
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function hasPromotedToMarker(metadata?: string): boolean {
|
|
91
|
+
const parsed = parseMetadataObject(metadata);
|
|
92
|
+
return typeof parsed?.promotedTo === "string" && parsed.promotedTo.trim().length > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveDowngrade(entry: MemoryEntry): PromotableDowngrade | null {
|
|
96
|
+
const from = extractBoundaryMetadata(entry.metadata)?.downgradedFrom;
|
|
97
|
+
return from === "profile" || from === "preferences" ? from : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function averageImportance(members: MemoryEntry[]): number {
|
|
101
|
+
if (members.length === 0) return 0;
|
|
102
|
+
return members.reduce((sum, m) => sum + (m.importance ?? 0), 0) / members.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Pick the highest-importance member as the promotion seed. */
|
|
106
|
+
function pickSeed(members: MemoryEntry[]): MemoryEntry {
|
|
107
|
+
return members.reduce((best, m) => (m.importance > best.importance ? m : best), members[0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Main scan
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export async function scanMemoryPromotions(
|
|
115
|
+
deps: PromoteScanDeps,
|
|
116
|
+
scope: string,
|
|
117
|
+
config?: Partial<PromoteScanConfig>,
|
|
118
|
+
): Promise<PromoteScanResult> {
|
|
119
|
+
const cfg: PromoteScanConfig = { ...DEFAULT_PROMOTE_SCAN_CONFIG, ...config };
|
|
120
|
+
|
|
121
|
+
// 1. Load downgraded evidence (events). list() returns empty vectors.
|
|
122
|
+
const events = await deps.store.list([scope], "events", cfg.listLimit, 0);
|
|
123
|
+
|
|
124
|
+
// 2. Keep only active, downgraded-from profile/preferences, not-yet-promoted.
|
|
125
|
+
const downgraded = events.filter(
|
|
126
|
+
(e) =>
|
|
127
|
+
isActiveMemory(e.metadata) &&
|
|
128
|
+
!hasPromotedToMarker(e.metadata) &&
|
|
129
|
+
!isNoise(e.text) &&
|
|
130
|
+
resolveDowngrade(e) !== null,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const result: PromoteScanResult = {
|
|
134
|
+
candidates: [],
|
|
135
|
+
scannedEvidence: downgraded.length,
|
|
136
|
+
clusters: 0,
|
|
137
|
+
promoted: 0,
|
|
138
|
+
dryRun: cfg.dryRun,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (downgraded.length === 0) return result;
|
|
142
|
+
|
|
143
|
+
// 3. Backfill vectors (list omits them for performance) and drop the
|
|
144
|
+
// vectorless — they cannot be clustered.
|
|
145
|
+
const vectorMap = await deps.store.getVectors(downgraded.map((e) => e.id));
|
|
146
|
+
const withVectors = downgraded
|
|
147
|
+
.map((e) => ({ ...e, vector: vectorMap.get(e.id) ?? [] }))
|
|
148
|
+
.filter((e) => e.vector.length > 0);
|
|
149
|
+
|
|
150
|
+
// 4. Group by downgraded category — they promote to different targets.
|
|
151
|
+
const groups = new Map<PromotableDowngrade, MemoryEntry[]>();
|
|
152
|
+
for (const entry of withVectors) {
|
|
153
|
+
const from = resolveDowngrade(entry);
|
|
154
|
+
if (!from) continue;
|
|
155
|
+
const bucket = groups.get(from);
|
|
156
|
+
if (bucket) bucket.push(entry);
|
|
157
|
+
else groups.set(from, [entry]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 5. Cluster each group; qualify candidates by occurrence + importance.
|
|
161
|
+
for (const [downgradedFrom, members] of groups) {
|
|
162
|
+
const clusters = greedyCluster(members, cfg.clusterThreshold);
|
|
163
|
+
result.clusters += clusters.length;
|
|
164
|
+
|
|
165
|
+
for (const cluster of clusters) {
|
|
166
|
+
if (cluster.members.length < cfg.minOccurrences) continue;
|
|
167
|
+
const avgImportance = averageImportance(cluster.members);
|
|
168
|
+
if (avgImportance < cfg.minImportance) continue;
|
|
169
|
+
|
|
170
|
+
const seed = pickSeed(cluster.members);
|
|
171
|
+
const candidate: PromoteCandidate = {
|
|
172
|
+
downgradedFrom,
|
|
173
|
+
seedId: seed.id,
|
|
174
|
+
seedText: seed.text,
|
|
175
|
+
memberIds: cluster.members.map((m) => m.id),
|
|
176
|
+
occurrences: cluster.members.length,
|
|
177
|
+
avgImportance,
|
|
178
|
+
promoted: null,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 6. Promote (unless dry-run). Dedup is handled by promoteMemory.
|
|
182
|
+
if (!cfg.dryRun) {
|
|
183
|
+
candidate.promoted = await deps.promote({
|
|
184
|
+
memoryId: seed.id,
|
|
185
|
+
category: downgradedFrom,
|
|
186
|
+
scope,
|
|
187
|
+
importance: avgImportance,
|
|
188
|
+
});
|
|
189
|
+
result.promoted += 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
result.candidates.push(candidate);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Production wiring
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/** Build scan deps from the persist pipeline, wrapping promoteMemory. */
|
|
204
|
+
export function buildPromoteScanDeps(
|
|
205
|
+
deps: PersistMemoryDeps & {
|
|
206
|
+
store: PersistMemoryDeps["store"] & Pick<MemoryStore, "list" | "getVectors">;
|
|
207
|
+
},
|
|
208
|
+
): PromoteScanDeps {
|
|
209
|
+
return {
|
|
210
|
+
store: deps.store,
|
|
211
|
+
promote: (req) => promoteMemory(deps, req),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Formatting
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export function formatPromoteScanResult(result: PromoteScanResult): string {
|
|
220
|
+
const lines = [
|
|
221
|
+
`Promote-scan: ${result.scannedEvidence} downgraded evidence, ${result.clusters} cluster(s) scanned.`,
|
|
222
|
+
];
|
|
223
|
+
if (result.candidates.length === 0) {
|
|
224
|
+
lines.push("No promotion candidates found.");
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
lines.push(
|
|
228
|
+
result.dryRun
|
|
229
|
+
? `Found ${result.candidates.length} candidate(s) (dry-run, nothing written):\n`
|
|
230
|
+
: `Promoted ${result.promoted} of ${result.candidates.length} candidate(s):\n`,
|
|
231
|
+
);
|
|
232
|
+
for (const [i, c] of result.candidates.entries()) {
|
|
233
|
+
lines.push(`### ${i + 1}. [${c.downgradedFrom}] ${c.seedText.split("\n")[0].slice(0, 80)}`);
|
|
234
|
+
lines.push(`Occurrences: ${c.occurrences} | avg importance: ${c.avgImportance.toFixed(2)}`);
|
|
235
|
+
lines.push(`Seed: ${c.seedId.slice(0, 8)} | members: ${c.memberIds.length}`);
|
|
236
|
+
if (c.promoted) {
|
|
237
|
+
lines.push(
|
|
238
|
+
c.promoted.disposition === "conflict" && c.promoted.conflictId
|
|
239
|
+
? `-> conflict ${c.promoted.conflictId.slice(0, 8)} (manual review)`
|
|
240
|
+
: `-> durable ${c.promoted.id.slice(0, 8)} (${c.promoted.disposition}, key ${c.promoted.canonicalKey})`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
lines.push("");
|
|
244
|
+
}
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
package/src/memory-schema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { boundedStringSchema, identifierSchema, normalizedStringListSchema, optionalBoundedStringSchema } from "./schema-utils.js";
|
|
4
|
+
import * as envConfig from "./env-config.js";
|
|
4
5
|
|
|
5
6
|
export const DURABLE_MEMORY_CATEGORIES = [
|
|
6
7
|
"profile",
|
|
@@ -130,12 +131,16 @@ export const StoreMemoryInputSchema = z.object({
|
|
|
130
131
|
privacyTier: PrivacyTierSchema.default("durable"),
|
|
131
132
|
});
|
|
132
133
|
|
|
134
|
+
// Keep in sync with RejectionReason in admission-control.ts
|
|
135
|
+
export const RejectionReasonSchema = z.enum(["text_too_short", "noise_detected", "importance_too_low", "rate_limited"]);
|
|
136
|
+
|
|
133
137
|
export const StoredMemoryRecordSchema = StoreMemoryInputSchema.extend({
|
|
134
138
|
id: identifierSchema("id", 128),
|
|
135
139
|
resolvedScope: identifierSchema("resolvedScope", 160),
|
|
136
140
|
storedAt: z.string().datetime(),
|
|
137
141
|
disposition: WriteDispositionSchema.default("stored"),
|
|
138
142
|
conflictId: identifierSchema("conflictId", 128).optional(),
|
|
143
|
+
rejectionReason: RejectionReasonSchema.optional(),
|
|
139
144
|
});
|
|
140
145
|
|
|
141
146
|
export const CaptureMemoryItemSchema = z.object({
|
|
@@ -181,6 +186,23 @@ export const WorkflowPatternInputSchema = z.object({
|
|
|
181
186
|
canonicalKey: CanonicalKeySchema,
|
|
182
187
|
});
|
|
183
188
|
|
|
189
|
+
/** A1 · break-loop 五维(Trellis 借鉴):debug case 的结构化归因,全可选、向后兼容。 */
|
|
190
|
+
export const DebugFramingSchema = z.object({
|
|
191
|
+
rootCause: z.enum([
|
|
192
|
+
"missing_spec",
|
|
193
|
+
"cross_layer_contract",
|
|
194
|
+
"change_propagation",
|
|
195
|
+
"test_coverage_gap",
|
|
196
|
+
"implicit_assumption",
|
|
197
|
+
]).describe("根因归类:缺spec/跨层契约/改动传播失败/测试覆盖缺口/隐式假设"),
|
|
198
|
+
whyPriorFixFailed: z.string().max(300).optional().describe("为何之前没修对"),
|
|
199
|
+
defense: z.string().max(300).optional().describe("防御机制(强度:架构>编译期>运行期>测试>review>文档)"),
|
|
200
|
+
systematicExtension: z.string().max(300).optional().describe("系统性扩展:同类问题还在哪"),
|
|
201
|
+
knowledgeFix: z.string().max(300).optional().describe("知识固化:沉淀去向"),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export type DebugFraming = z.infer<typeof DebugFramingSchema>;
|
|
205
|
+
|
|
184
206
|
export const CaseMemoryInputSchema = z.object({
|
|
185
207
|
title: CaseMemoryTitleSchema,
|
|
186
208
|
problem: CaseMemoryProblemSchema,
|
|
@@ -193,6 +215,7 @@ export const CaseMemoryInputSchema = z.object({
|
|
|
193
215
|
source: StoreMemorySourceSchema.default("agent"),
|
|
194
216
|
tags: MemoryTagsSchema,
|
|
195
217
|
canonicalKey: CanonicalKeySchema,
|
|
218
|
+
debugFraming: DebugFramingSchema.optional(),
|
|
196
219
|
});
|
|
197
220
|
|
|
198
221
|
export const PromoteMemoryInputSchema = z.object({
|
|
@@ -248,9 +271,9 @@ export type PromoteMemoryInput = z.infer<typeof PromoteMemoryInputSchema>;
|
|
|
248
271
|
export type StoredPromotedMemoryRecord = z.infer<typeof StoredPromotedMemoryRecordSchema>;
|
|
249
272
|
|
|
250
273
|
export function isEmotionScoringEnabled(): boolean {
|
|
251
|
-
return
|
|
274
|
+
return envConfig.emotionScoring();
|
|
252
275
|
}
|
|
253
276
|
|
|
254
277
|
export function isPredictiveMemoryEnabled(): boolean {
|
|
255
|
-
return
|
|
278
|
+
return envConfig.predictiveMemory();
|
|
256
279
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MemoryEntry, MemorySearchResult } from "./store.js";
|
|
2
|
+
|
|
3
|
+
export interface MemoryStoreStats {
|
|
4
|
+
totalCount: number;
|
|
5
|
+
scopeCounts: Record<string, number>;
|
|
6
|
+
categoryCounts: Record<string, number>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type MemoryStoreUpdate = {
|
|
10
|
+
text?: string;
|
|
11
|
+
vector?: number[];
|
|
12
|
+
importance?: number;
|
|
13
|
+
category?: MemoryEntry["category"];
|
|
14
|
+
metadata?: string;
|
|
15
|
+
timestamp?: number;
|
|
16
|
+
language?: string;
|
|
17
|
+
fts_text?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface MemoryStorePort {
|
|
21
|
+
stats(scopeFilter?: string[]): Promise<MemoryStoreStats>;
|
|
22
|
+
list(scopeFilter?: string[], category?: string, limit?: number, offset?: number): Promise<MemoryEntry[]>;
|
|
23
|
+
/** DB 层真分页(where/limit/offset 下推),全库维护扫描用;list() 是全量拉取再切片。 */
|
|
24
|
+
listPage(opts?: {
|
|
25
|
+
scopeFilter?: string[];
|
|
26
|
+
category?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
offset?: number;
|
|
29
|
+
includeVector?: boolean;
|
|
30
|
+
}): Promise<MemoryEntry[]>;
|
|
31
|
+
get(id: string, scopeFilter?: string[]): Promise<MemoryEntry | null>;
|
|
32
|
+
getById(id: string): Promise<MemoryEntry | null>;
|
|
33
|
+
store(entry: Omit<MemoryEntry, "id" | "timestamp"> & { id?: string }): Promise<MemoryEntry>;
|
|
34
|
+
update(id: string, updates: MemoryStoreUpdate, scopeFilter?: string[]): Promise<MemoryEntry | null>;
|
|
35
|
+
/** metadata 读改写的单写通道(per-id 串行队列)。新代码改 metadata 一律走这里,
|
|
36
|
+
* 不要 getById+update 裸 RMW(并发覆盖)。 */
|
|
37
|
+
patchMetadata(
|
|
38
|
+
id: string,
|
|
39
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
|
|
40
|
+
scopeFilter?: string[],
|
|
41
|
+
): Promise<MemoryEntry | null>;
|
|
42
|
+
vectorSearch(vector: number[], limit?: number, minScore?: number, scopeFilter?: string[]): Promise<MemorySearchResult[]>;
|
|
43
|
+
}
|
package/src/multi-vector.ts
CHANGED
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Embedder } from "./embedder.js";
|
|
19
|
+
import * as envConfig from "./env-config.js";
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Config
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
|
|
24
25
|
export function isMultiVectorEnabled(): boolean {
|
|
25
|
-
return
|
|
26
|
+
return envConfig.multiVector();
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
package/src/narrative-schema.ts
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
import * as envConfig from "./env-config.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Feature Flag
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
|
|
20
21
|
export function isNarrativeModeEnabled(): boolean {
|
|
21
|
-
return
|
|
22
|
+
return envConfig.narrativeMode();
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
package/src/noise-filter.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { logInfo } from "./stderr-log.js";
|
|
8
|
+
import { redactForLog } from "./pii-detector.js";
|
|
8
9
|
|
|
9
10
|
// Agent-side denial patterns
|
|
10
11
|
const DENIAL_PATTERNS = [
|
|
@@ -45,7 +46,7 @@ const META_QUESTION_PATTERNS = [
|
|
|
45
46
|
|
|
46
47
|
// Session boilerplate (anchored to prevent false positives on real content)
|
|
47
48
|
const BOILERPLATE_PATTERNS = [
|
|
48
|
-
/^(hi|hello|hey|good morning|good evening|greetings)/i,
|
|
49
|
+
/^(hi|hello|hey|good morning|good evening|greetings)\b/i,
|
|
49
50
|
/^fresh session/i,
|
|
50
51
|
/^new session/i,
|
|
51
52
|
/^HEARTBEAT/i,
|
|
@@ -81,6 +82,43 @@ const DIAGNOSTIC_ARTIFACT_PATTERNS = [
|
|
|
81
82
|
/^只回复精确代号/i,
|
|
82
83
|
];
|
|
83
84
|
|
|
85
|
+
// CC bridge injected agent prompts: the Telegram bridge writes CC's own system
|
|
86
|
+
// prompts (reply / self-decision templates) as `user` turns, which transcript
|
|
87
|
+
// ingest then mis-extracts as profile/preferences. They are never user content.
|
|
88
|
+
const AGENT_PROMPT_PATTERNS = [
|
|
89
|
+
/^#{1,3}\s*CC\s+(Reply|Self-Decision)\s+Prompt\b/im,
|
|
90
|
+
/你不是\s*Alice\s*的数字分身/,
|
|
91
|
+
/你是\s*CC[。.]?\s*(Alice\s*刚给你发了消息|现在你周期性醒来)/,
|
|
92
|
+
// Fixed sections of the bridge prompt that survive ingest as standalone chunks
|
|
93
|
+
// (the long prompt turn gets split, so the title/persona lines may be absent).
|
|
94
|
+
/你的输出\s*[\r\n]+\s*直接回复\s*Alice/,
|
|
95
|
+
/节奏要求[::]\s*[\r\n]+\s*[-*]\s*短话题/,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// CC bridge self-decision / RecallNest distill payloads embed structured JSON
|
|
99
|
+
// (decision logs, conversation history, interaction stats). Long prompts get
|
|
100
|
+
// split by transcript chunking, orphaning JSON field-lines that lost their
|
|
101
|
+
// enclosing braces — these survive ingest looking like preferences. We match
|
|
102
|
+
// the severed-fragment SHAPE, not JSON in general: a complete { } object is left
|
|
103
|
+
// alone (it may be real discussion); only orphaned field-lines (start as "key":
|
|
104
|
+
// with no brace) and dangling structural tokens (leading , } ]) qualify.
|
|
105
|
+
const CONTEXT_JSON_FRAGMENT_PATTERNS = [
|
|
106
|
+
// bridge/distill proprietary schema keys — ANCHORED to line start (^). These
|
|
107
|
+
// keys are also RecallNest's own schema fields, so a real memory may MENTION
|
|
108
|
+
// them inline (e.g. 我们讨论 "distilled_at": 要不要保留); anchoring ensures only
|
|
109
|
+
// an orphaned line that LEADS with the key is filtered, not such discussion.
|
|
110
|
+
/^"?(?:next_check_hint|alice_interaction_stats(?:_7d)?|alice_last_cc_hours_ago|engagement_rate|seen_no_reply|distilled_at)"\s*:/,
|
|
111
|
+
// orphan field-line: whole chunk starts as "key": <scalar> with no { wrapper.
|
|
112
|
+
// SCALAR values only (string/number/bool/null). A `{` or `[` value is left
|
|
113
|
+
// alone so real config / package.json chunks like `"scripts": {` survive.
|
|
114
|
+
/^"?[A-Za-z_][\w]*"\s*:\s*(?:"|\d|true\b|false\b|null\b)/,
|
|
115
|
+
// severed JSON array/object element: a dangling , } ] then a NEWLINE into a
|
|
116
|
+
// brace/quoted-key. Requiring the newline-then-JSON shape avoids real code
|
|
117
|
+
// fragments like `} catch (e) {` or `});` (close-token followed by code on
|
|
118
|
+
// the same line, never a newline into a JSON key).
|
|
119
|
+
/^[,}\]]+[ \t]*\r?\n\s*[{[]?\s*"[A-Za-z_]/,
|
|
120
|
+
];
|
|
121
|
+
|
|
84
122
|
export interface NoiseFilterOptions {
|
|
85
123
|
/** Filter agent denial responses (default: true) */
|
|
86
124
|
filterDenials?: boolean;
|
|
@@ -88,12 +126,15 @@ export interface NoiseFilterOptions {
|
|
|
88
126
|
filterMetaQuestions?: boolean;
|
|
89
127
|
/** Filter session boilerplate (default: true) */
|
|
90
128
|
filterBoilerplate?: boolean;
|
|
129
|
+
/** Filter CC bridge injected agent prompts (default: true) */
|
|
130
|
+
filterAgentPrompts?: boolean;
|
|
91
131
|
}
|
|
92
132
|
|
|
93
133
|
const DEFAULT_OPTIONS: Required<NoiseFilterOptions> = {
|
|
94
134
|
filterDenials: true,
|
|
95
135
|
filterMetaQuestions: true,
|
|
96
136
|
filterBoilerplate: true,
|
|
137
|
+
filterAgentPrompts: true,
|
|
97
138
|
};
|
|
98
139
|
|
|
99
140
|
/**
|
|
@@ -110,21 +151,21 @@ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean
|
|
|
110
151
|
}
|
|
111
152
|
|
|
112
153
|
if (opts.filterDenials && DENIAL_PATTERNS.some(p => p.test(trimmed))) {
|
|
113
|
-
logInfo(`[INFO] noise-filter: denial pattern matched: "${trimmed
|
|
154
|
+
logInfo(`[INFO] noise-filter: denial pattern matched: "${redactForLog(trimmed)}..."`);
|
|
114
155
|
return true;
|
|
115
156
|
}
|
|
116
157
|
if (opts.filterMetaQuestions && META_QUESTION_PATTERNS.some(p => p.test(trimmed))) {
|
|
117
|
-
logInfo(`[INFO] noise-filter: meta-question filtered: "${trimmed
|
|
158
|
+
logInfo(`[INFO] noise-filter: meta-question filtered: "${redactForLog(trimmed)}..."`);
|
|
118
159
|
return true;
|
|
119
160
|
}
|
|
120
161
|
if (opts.filterBoilerplate) {
|
|
121
162
|
if (BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
|
|
122
|
-
logInfo(`[INFO] noise-filter: boilerplate filtered: "${trimmed
|
|
163
|
+
logInfo(`[INFO] noise-filter: boilerplate filtered: "${redactForLog(trimmed)}..."`);
|
|
123
164
|
return true;
|
|
124
165
|
}
|
|
125
166
|
if (trimmed.length <= BOILERPLATE_MAX_LENGTH &&
|
|
126
167
|
SHORT_BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
|
|
127
|
-
logInfo(`[INFO] noise-filter: short boilerplate filtered: "${trimmed}"`);
|
|
168
|
+
logInfo(`[INFO] noise-filter: short boilerplate filtered: "${redactForLog(trimmed)}"`);
|
|
128
169
|
return true;
|
|
129
170
|
}
|
|
130
171
|
}
|
|
@@ -135,7 +176,17 @@ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean
|
|
|
135
176
|
}
|
|
136
177
|
// Diagnostic artifact noise (synced from upstream v1.1.0-beta.3)
|
|
137
178
|
if (DIAGNOSTIC_ARTIFACT_PATTERNS.some(p => p.test(trimmed))) {
|
|
138
|
-
logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${trimmed
|
|
179
|
+
logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${redactForLog(trimmed)}..."`);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// CC bridge injected agent prompts (reply/self-decision templates leaked as user turns)
|
|
183
|
+
if (opts.filterAgentPrompts && AGENT_PROMPT_PATTERNS.some(p => p.test(trimmed))) {
|
|
184
|
+
logInfo(`[INFO] noise-filter: agent prompt filtered: "${redactForLog(trimmed)}..."`);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// Severed bridge/distill context-JSON fragments (orphaned field-lines from chunking)
|
|
188
|
+
if (opts.filterAgentPrompts && CONTEXT_JSON_FRAGMENT_PATTERNS.some(p => p.test(trimmed))) {
|
|
189
|
+
logInfo(`[INFO] noise-filter: context JSON fragment filtered: "${redactForLog(trimmed)}..."`);
|
|
139
190
|
return true;
|
|
140
191
|
}
|
|
141
192
|
|