recallnest 2.3.0 → 2.6.0
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 +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- 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/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 +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -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__/api-server-security.test.ts +102 -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__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -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 +116 -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 +945 -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 +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -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 +262 -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 +129 -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 +780 -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__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -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-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -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 +282 -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__/noise-prototype-bank.test.ts +280 -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 +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -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 +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -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-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.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-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -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 +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -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-terminal-roster.test.ts +127 -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__/ui-server-security.test.ts +93 -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 +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- 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 +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- 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 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/auto-gc.ts
CHANGED
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
* instead of raw age/importance heuristics. Archive-first, delete-never.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { MemoryStorePort } from "./memory-store-port.js";
|
|
13
13
|
import {
|
|
14
14
|
parseEvolution,
|
|
15
15
|
computeDecayScore,
|
|
16
|
+
computeUsageAdjustedDecayScore,
|
|
16
17
|
buildArchivedMetadata,
|
|
17
18
|
isActiveMemory,
|
|
18
19
|
} from "./memory-evolution.js";
|
|
19
20
|
import { loadRetentionPolicy, shouldArchiveByPolicy } from "./retention-policy.js";
|
|
20
21
|
import type { RetentionPolicy } from "./retention-policy.js";
|
|
21
22
|
import type { AuditLogger } from "./audit-log.js";
|
|
23
|
+
import * as envConfig from "./env-config.js";
|
|
24
|
+
import { isUsageSignalActive } from "./usage-tracker.js";
|
|
25
|
+
import { acquireLock, releaseLock, getLastDistillTime, stampLock, lockPathForKey } from "./distill-lock.js";
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Config
|
|
@@ -73,48 +77,89 @@ export interface GcResult {
|
|
|
73
77
|
* - Not pinned (importance >= 0.95 is considered pinned)
|
|
74
78
|
*/
|
|
75
79
|
export async function maybeRunGc(
|
|
76
|
-
store:
|
|
80
|
+
store: MemoryStorePort,
|
|
77
81
|
config: AutoGcConfig = DEFAULT_AUTO_GC_CONFIG,
|
|
78
82
|
retentionConfigDir?: string,
|
|
79
83
|
auditLogger?: AuditLogger,
|
|
80
84
|
): Promise<GcResult> {
|
|
81
85
|
const stats = await store.stats();
|
|
82
|
-
const totalMemories = stats.
|
|
86
|
+
const totalMemories = stats.totalCount ?? 0;
|
|
83
87
|
|
|
84
88
|
// Condition 1: enough memories to warrant GC
|
|
85
89
|
if (totalMemories < config.minMemoryCount) {
|
|
86
90
|
return { triggered: false, reason: "below_memory_threshold", archivedCount: 0, totalChecked: 0 };
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
// Condition 2: enough time since last GC
|
|
90
|
-
|
|
93
|
+
// Condition 2: enough time since last GC. P0-1: the throttle is cross-process now —
|
|
94
|
+
// take the max of this process's in-memory timestamp and the shared gc-last stamp
|
|
95
|
+
// file's mtime. The old module-level var alone let each of the 11 mcp-server
|
|
96
|
+
// processes run gc on its own independent 24h clock.
|
|
97
|
+
const gcStampPath = lockPathForKey("gc-last");
|
|
98
|
+
const lastGc = Math.max(lastGcTimestamp, getLastDistillTime({ lockPath: gcStampPath }));
|
|
99
|
+
const hoursSinceLastGc = (Date.now() - lastGc) / 3_600_000;
|
|
91
100
|
if (hoursSinceLastGc < config.minHoursSinceLastGc) {
|
|
92
101
|
return { triggered: false, reason: "too_soon", archivedCount: 0, totalChecked: 0 };
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
// Concurrency guard: only one process scans the full corpus at a time. A process that
|
|
105
|
+
// passed the throttle in the same instant loses the O_EXCL race here and skips.
|
|
106
|
+
const gcRunLock = { lockPath: lockPathForKey("gc-run"), expireMs: 30 * 60_000 };
|
|
107
|
+
if (!acquireLock(gcRunLock)) {
|
|
108
|
+
return { triggered: false, reason: "locked_by_another_process", archivedCount: 0, totalChecked: 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
// All conditions met — run GC
|
|
96
|
-
lastGcTimestamp = Date.now();
|
|
112
|
+
lastGcTimestamp = Date.now(); // in-process immediate throttle
|
|
97
113
|
|
|
114
|
+
try {
|
|
115
|
+
const { archivedCount, totalChecked } = await runGcScan(store, config, retentionConfigDir, auditLogger);
|
|
116
|
+
// Stamp completion into the shared throttle marker (create-or-touch → mtime = now)
|
|
117
|
+
// so the other processes observe "last run" via getLastDistillTime.
|
|
118
|
+
stampLock({ lockPath: gcStampPath });
|
|
119
|
+
return { triggered: true, archivedCount, totalChecked };
|
|
120
|
+
} finally {
|
|
121
|
+
releaseLock(gcRunLock);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The full-corpus archive scan — the body of a triggered GC run. Extracted from
|
|
127
|
+
* maybeRunGc so the P0-1 lock/throttle wiring stays readable; the scan logic itself
|
|
128
|
+
* is unchanged.
|
|
129
|
+
*/
|
|
130
|
+
async function runGcScan(
|
|
131
|
+
store: MemoryStorePort,
|
|
132
|
+
config: AutoGcConfig,
|
|
133
|
+
retentionConfigDir: string | undefined,
|
|
134
|
+
auditLogger: AuditLogger | undefined,
|
|
135
|
+
): Promise<{ archivedCount: number; totalChecked: number }> {
|
|
98
136
|
const now = Date.now();
|
|
99
137
|
let archivedCount = 0;
|
|
100
138
|
let totalChecked = 0;
|
|
101
139
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
// Full-corpus scan, page by page. The previous implementation listed only
|
|
141
|
+
// the newest 5000 rows (list() sorts by timestamp desc before slicing) —
|
|
142
|
+
// old low-value memories, the very target of GC, never entered the scan
|
|
143
|
+
// window on a 99K+ corpus. Two passes keep memory at one page instead of
|
|
144
|
+
// the whole corpus; pass 2 mutates rows (archive), which may shift fragment
|
|
145
|
+
// scan order — a row missed this run is caught by the next one.
|
|
146
|
+
const GC_PAGE_SIZE = 2000;
|
|
105
147
|
|
|
106
|
-
//
|
|
107
|
-
// This avoids repeated file reads inside the loop.
|
|
148
|
+
// Pass 1: per-scope active memory counts for retention policies.
|
|
108
149
|
const activeCounts = new Map<string, number>();
|
|
109
150
|
const policyCache = new Map<string, RetentionPolicy>();
|
|
110
151
|
const scopesSeen = new Set<string>();
|
|
111
152
|
|
|
112
|
-
for (
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
for (let offset = 0; ; offset += GC_PAGE_SIZE) {
|
|
154
|
+
const page = await store.listPage({ limit: GC_PAGE_SIZE, offset });
|
|
155
|
+
for (const entry of page) {
|
|
156
|
+
const scope = entry.scope ?? "";
|
|
157
|
+
scopesSeen.add(scope);
|
|
158
|
+
if (isActiveMemory(entry.metadata)) {
|
|
159
|
+
activeCounts.set(scope, (activeCounts.get(scope) ?? 0) + 1);
|
|
160
|
+
}
|
|
117
161
|
}
|
|
162
|
+
if (page.length < GC_PAGE_SIZE) break;
|
|
118
163
|
}
|
|
119
164
|
|
|
120
165
|
// Batch-load retention policies for all scopes (one file read per scope)
|
|
@@ -122,8 +167,15 @@ export async function maybeRunGc(
|
|
|
122
167
|
policyCache.set(scope, loadRetentionPolicy(scope, retentionConfigDir));
|
|
123
168
|
}
|
|
124
169
|
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
// Pass 2: archive sweep.
|
|
171
|
+
const useUsageAdjustedDecay = envConfig.usageDecay() && isUsageSignalActive();
|
|
172
|
+
outer:
|
|
173
|
+
for (let offset = 0; ; offset += GC_PAGE_SIZE) {
|
|
174
|
+
const page = await store.listPage({ limit: GC_PAGE_SIZE, offset });
|
|
175
|
+
totalChecked += page.length;
|
|
176
|
+
|
|
177
|
+
for (const entry of page) {
|
|
178
|
+
if (archivedCount >= config.maxArchivePerRun) break outer;
|
|
127
179
|
|
|
128
180
|
// Only archive active memories
|
|
129
181
|
if (!isActiveMemory(entry.metadata)) continue;
|
|
@@ -139,7 +191,9 @@ export async function maybeRunGc(
|
|
|
139
191
|
let shouldArchive = false;
|
|
140
192
|
if (ageDays >= config.minAgeDays) {
|
|
141
193
|
const evo = parseEvolution(entry.metadata, entry.timestamp);
|
|
142
|
-
let decayScore =
|
|
194
|
+
let decayScore = useUsageAdjustedDecay
|
|
195
|
+
? computeUsageAdjustedDecayScore(evo, importance, now, entry.metadata)
|
|
196
|
+
: computeDecayScore(evo, importance, now, entry.metadata);
|
|
143
197
|
// F3: Expired memories (validUntil < now) get 2x decay acceleration
|
|
144
198
|
if (evo.validUntil != null && evo.validUntil < now) {
|
|
145
199
|
decayScore *= 0.5; // Halve the score → archives faster
|
|
@@ -182,12 +236,22 @@ export async function maybeRunGc(
|
|
|
182
236
|
}
|
|
183
237
|
}
|
|
184
238
|
|
|
185
|
-
|
|
239
|
+
if (page.length < GC_PAGE_SIZE) break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { archivedCount, totalChecked };
|
|
186
243
|
}
|
|
187
244
|
|
|
188
245
|
/**
|
|
189
|
-
* Reset last GC timestamp (for testing).
|
|
246
|
+
* Reset last GC timestamp (for testing). Clears both the in-memory timestamp and the
|
|
247
|
+
* persisted cross-process throttle marker / run lock so tests start from a clean slate.
|
|
190
248
|
*/
|
|
191
249
|
export function resetGcTimestamp(): void {
|
|
192
250
|
lastGcTimestamp = 0;
|
|
251
|
+
try {
|
|
252
|
+
releaseLock({ lockPath: lockPathForKey("gc-last") });
|
|
253
|
+
releaseLock({ lockPath: lockPathForKey("gc-run") });
|
|
254
|
+
} catch {
|
|
255
|
+
/* ignore — nothing to clear */
|
|
256
|
+
}
|
|
193
257
|
}
|
package/src/auto-recall.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { composeResumeContext, type ResumeContextDeps } from "./context-composer.js";
|
|
2
2
|
import { resolveTier } from "./decay-engine.js";
|
|
3
3
|
import type { DurableMemoryCategory } from "./memory-schema.js";
|
|
4
|
+
import {
|
|
5
|
+
classifyRecallGate,
|
|
6
|
+
logRecallGateShadow,
|
|
7
|
+
resolveRecallGateMode,
|
|
8
|
+
type RecallGateResult,
|
|
9
|
+
} from "./recall-gate.js";
|
|
4
10
|
import { governResults, truncateQuery, type GovernorConfig, type GovernorSession } from "./recall-governor.js";
|
|
5
11
|
import type { RetrievalResult } from "./retriever.js";
|
|
6
12
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
@@ -33,6 +39,20 @@ export interface AutoRecallResponse {
|
|
|
33
39
|
searchSkippedReason?: string;
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/** Minimal valid ResumeContextResponse for enforce-mode skip-all short-circuits. */
|
|
43
|
+
function emptyResumeContext(): ResumeContextResponse {
|
|
44
|
+
return {
|
|
45
|
+
summary: "",
|
|
46
|
+
stableContext: [],
|
|
47
|
+
relevantPatterns: [],
|
|
48
|
+
recentCases: [],
|
|
49
|
+
injectionHint: "user_attachment",
|
|
50
|
+
ephemeral: true,
|
|
51
|
+
responseMode: "default",
|
|
52
|
+
generatedAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
export async function runAutoRecall(
|
|
37
57
|
deps: ResumeContextDeps,
|
|
38
58
|
request: AutoRecallRequest,
|
|
@@ -46,6 +66,34 @@ export async function runAutoRecall(
|
|
|
46
66
|
throw new Error("message is required");
|
|
47
67
|
}
|
|
48
68
|
|
|
69
|
+
// Recall gate (mlp adaptive-retrieval borrow, three-way per Codex review).
|
|
70
|
+
// Sits BEFORE composeResumeContext — compose itself hits the retriever, so a
|
|
71
|
+
// gate placed after it would save nothing. Default mode is observe: verdicts
|
|
72
|
+
// are computed and shadow-logged but behavior is unchanged until real-sample
|
|
73
|
+
// false-positive rates have been reviewed (shared-behaviors §5).
|
|
74
|
+
const gateMode = resolveRecallGateMode(request.env);
|
|
75
|
+
let gateVerdict: RecallGateResult | undefined;
|
|
76
|
+
if (gateMode !== "off") {
|
|
77
|
+
gateVerdict = classifyRecallGate(message);
|
|
78
|
+
logRecallGateShadow({
|
|
79
|
+
ts: new Date().toISOString(),
|
|
80
|
+
decision: gateVerdict.decision,
|
|
81
|
+
ruleId: gateVerdict.ruleId,
|
|
82
|
+
msgLen: message.length,
|
|
83
|
+
mode: gateMode,
|
|
84
|
+
source: request.operation,
|
|
85
|
+
});
|
|
86
|
+
if (gateMode === "enforce" && gateVerdict.decision === "skip-all") {
|
|
87
|
+
return {
|
|
88
|
+
mode: "resume-only",
|
|
89
|
+
resolvedScope: undefined,
|
|
90
|
+
resume: emptyResumeContext(),
|
|
91
|
+
results: [],
|
|
92
|
+
searchSkippedReason: `recall-gate: skip-all (${gateVerdict.ruleId})`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
const scopeSelection = resolveScopeSelection({
|
|
50
98
|
scope: request.scope,
|
|
51
99
|
sessionId: request.sessionId,
|
|
@@ -67,6 +115,18 @@ export async function runAutoRecall(
|
|
|
67
115
|
profile: request.profile,
|
|
68
116
|
});
|
|
69
117
|
|
|
118
|
+
// Continuity nudges ("继续", "下一步") want the resume context they just got,
|
|
119
|
+
// not an embedding search over their own two characters.
|
|
120
|
+
if (gateMode === "enforce" && gateVerdict?.decision === "resume-only") {
|
|
121
|
+
return {
|
|
122
|
+
mode: "resume-only",
|
|
123
|
+
resolvedScope: resume.resolvedScope || resume.latestCheckpoint?.resolvedScope || scopeSelection.resolvedScope,
|
|
124
|
+
resume,
|
|
125
|
+
results: [],
|
|
126
|
+
searchSkippedReason: `recall-gate: resume-only (${gateVerdict.ruleId})`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
70
130
|
const resolvedScope = request.allScopes
|
|
71
131
|
? undefined
|
|
72
132
|
: (resume.resolvedScope || resume.latestCheckpoint?.resolvedScope || scopeSelection.resolvedScope);
|
|
@@ -101,7 +161,7 @@ export async function runAutoRecall(
|
|
|
101
161
|
const promoted: RetrievalResult[] = [];
|
|
102
162
|
const demoted: RetrievalResult[] = [];
|
|
103
163
|
for (const r of rawResults) {
|
|
104
|
-
const tier = resolveTier(r.entry.metadata);
|
|
164
|
+
const tier = resolveTier(r.entry.metadata, r.entry.importance);
|
|
105
165
|
if (tier === "core" || tier === "working") {
|
|
106
166
|
promoted.push(r);
|
|
107
167
|
} else {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Belief history — copy-on-write archival for belief-changing writes.
|
|
3
|
+
*
|
|
4
|
+
* Problem this solves: the latest-wins and conflict-resolution paths used to call
|
|
5
|
+
* store.update() on the canonical row, replacing text + vector in place. The previous
|
|
6
|
+
* belief was physically gone, so no retrieval strategy could ever answer
|
|
7
|
+
* "what did I believe last month?".
|
|
8
|
+
*
|
|
9
|
+
* Approach: before overwriting canonical row C, copy C's current content into a new
|
|
10
|
+
* `superseded` history row H, then update C in place as before. C keeps its id, so the
|
|
11
|
+
* `deterministicId(scope, canonicalKey)` invariant every canonical lookup depends on is
|
|
12
|
+
* untouched — history lives beside it rather than replacing it.
|
|
13
|
+
*
|
|
14
|
+
* Chain shape (C is the stable canonical id, H1 older than H2):
|
|
15
|
+
* H1 --supersededBy--> C C --supersedes--> H2
|
|
16
|
+
* H2 --supersededBy--> C H2 --supersedes--> H1
|
|
17
|
+
* so traceEvolution() walks backward C → H2 → H1 to rebuild the timeline, and forward
|
|
18
|
+
* from any history row to the belief that is live now.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { deterministicId, type MemoryEntry, type MemoryStore } from "./store.js";
|
|
22
|
+
import { parseEvolution, patchEvolution, type EvolutionMetadata } from "./memory-evolution.js";
|
|
23
|
+
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
24
|
+
import type { Embedder } from "./embedder.js";
|
|
25
|
+
|
|
26
|
+
/** Namespace for history-row ids — keeps them deterministic (retry-safe) yet distinct from C. */
|
|
27
|
+
const BELIEF_HISTORY_ID_PREFIX = "belief-history:";
|
|
28
|
+
|
|
29
|
+
export interface BeliefHistoryDeps {
|
|
30
|
+
store: Pick<MemoryStore, "store"> & Partial<Pick<MemoryStore, "upsert" | "getById">>;
|
|
31
|
+
embedder: Pick<Embedder, "embedPassage">;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BeliefArchiveResult {
|
|
35
|
+
/** Id of the newly written history row — becomes the canonical row's `supersedes`. */
|
|
36
|
+
historyId: string;
|
|
37
|
+
/** When the archived version stopped being the live belief. */
|
|
38
|
+
validUntil: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a usable vector for the row being archived.
|
|
43
|
+
*
|
|
44
|
+
* store.list() deliberately returns `vector: []` for performance, and that is where the
|
|
45
|
+
* canonical-match candidates come from — so the entry handed to us usually has no vector.
|
|
46
|
+
* Re-read the full row first; only re-embed if the store can't give it back.
|
|
47
|
+
*/
|
|
48
|
+
async function resolveVector(deps: BeliefHistoryDeps, current: MemoryEntry): Promise<number[]> {
|
|
49
|
+
if (current.vector && current.vector.length > 0) return current.vector;
|
|
50
|
+
|
|
51
|
+
if (deps.store.getById) {
|
|
52
|
+
const full = await deps.store.getById(current.id);
|
|
53
|
+
if (full?.vector && full.vector.length > 0) return full.vector;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return await deps.embedder.embedPassage(current.text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Archive the current content of a canonical memory as a `superseded` history row.
|
|
61
|
+
*
|
|
62
|
+
* Throws rather than degrading to a silent overwrite: a belief-changing write that cannot
|
|
63
|
+
* preserve the old belief is exactly the failure this module exists to prevent, so callers
|
|
64
|
+
* must not proceed with the overwrite if this rejects.
|
|
65
|
+
*
|
|
66
|
+
* Known limitation — archive and overwrite are two writes with no transaction around them.
|
|
67
|
+
* If the caller's update() fails after this returns, the history row is already committed
|
|
68
|
+
* while the canonical row still holds that same text, leaving a superseded duplicate of a
|
|
69
|
+
* belief that is in fact still live. Damage is bounded: nothing is destroyed, and the
|
|
70
|
+
* deterministic id means a retry reuses the same row rather than piling up copies.
|
|
71
|
+
*/
|
|
72
|
+
export async function archiveBeliefVersion(
|
|
73
|
+
deps: BeliefHistoryDeps,
|
|
74
|
+
current: MemoryEntry,
|
|
75
|
+
options: { now?: number } = {},
|
|
76
|
+
): Promise<BeliefArchiveResult> {
|
|
77
|
+
const now = options.now ?? Date.now();
|
|
78
|
+
const currentEvo = parseEvolution(current.metadata, current.timestamp);
|
|
79
|
+
|
|
80
|
+
// Text is part of the id, not just (id, timestamp): two belief changes landing in the
|
|
81
|
+
// same millisecond would otherwise collide and the second archive would overwrite the
|
|
82
|
+
// first. With the text folded in, a collision means the content is identical anyway.
|
|
83
|
+
const historyId = deterministicId(
|
|
84
|
+
current.scope,
|
|
85
|
+
`${BELIEF_HISTORY_ID_PREFIX}${current.id}@${current.timestamp}\n${current.text}`,
|
|
86
|
+
);
|
|
87
|
+
if (historyId === current.id) {
|
|
88
|
+
throw new Error(`Belief history id collided with canonical id ${current.id}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const vector = await resolveVector(deps, current);
|
|
92
|
+
if (vector.length === 0) {
|
|
93
|
+
throw new Error(`Cannot archive belief ${current.id}: no vector available`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The history row inherits the old metadata wholesale (canonicalKey, boundary, emotion…)
|
|
97
|
+
// and only has its evolution block rewritten to close the interval.
|
|
98
|
+
const historyMetadata = patchEvolution(current.metadata, {
|
|
99
|
+
status: "superseded",
|
|
100
|
+
validFrom: currentEvo.validFrom,
|
|
101
|
+
validUntil: now,
|
|
102
|
+
supersededBy: current.id,
|
|
103
|
+
// Point at the version this one replaced, so the backward walk stays a chain.
|
|
104
|
+
supersedes: currentEvo.supersedes,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const historyRow: MemoryEntry = {
|
|
108
|
+
id: historyId,
|
|
109
|
+
text: current.text,
|
|
110
|
+
vector,
|
|
111
|
+
category: current.category,
|
|
112
|
+
scope: current.scope,
|
|
113
|
+
importance: current.importance,
|
|
114
|
+
// Keep the original timestamp: canonical matching scans the most recent
|
|
115
|
+
// CANONICAL_SCAN_LIMIT rows, and stamping history rows "now" would let them crowd
|
|
116
|
+
// live entries out of that window.
|
|
117
|
+
timestamp: current.timestamp,
|
|
118
|
+
metadata: historyMetadata,
|
|
119
|
+
// Derive rather than copy: neither list() nor getById() selects these columns, so
|
|
120
|
+
// `current.language` / `current.fts_text` are all but guaranteed to be undefined here.
|
|
121
|
+
// Falling back to the "en" / raw-text defaults would mislabel Chinese memories and
|
|
122
|
+
// store them unsegmented for FTS — so recompute them the same way live entries do.
|
|
123
|
+
language: current.language || detectLang(current.text),
|
|
124
|
+
fts_text: current.fts_text || tokenizeFts(current.text, current.language || detectLang(current.text)),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (deps.store.upsert) {
|
|
128
|
+
await deps.store.upsert(historyRow);
|
|
129
|
+
} else {
|
|
130
|
+
// Fallback for narrower store deps (mocks): store() stamps its own timestamp, which
|
|
131
|
+
// only costs us the scan-window optimisation above — the history row is still written.
|
|
132
|
+
await deps.store.store(historyRow);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { historyId, validUntil: now };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build the evolution block for the canonical row that is about to be overwritten.
|
|
140
|
+
*
|
|
141
|
+
* Callers previously passed the incoming metadata straight to store.update(), which wiped
|
|
142
|
+
* the evolution block along with the text. This merges the incoming metadata with a fresh
|
|
143
|
+
* evolution block that links back to the archived version.
|
|
144
|
+
*/
|
|
145
|
+
export function buildSupersedingBeliefMetadata(
|
|
146
|
+
incomingMetadata: string | undefined,
|
|
147
|
+
previousEvolution: EvolutionMetadata,
|
|
148
|
+
historyId: string,
|
|
149
|
+
now: number,
|
|
150
|
+
evolutionNote?: string | null,
|
|
151
|
+
): string {
|
|
152
|
+
return patchEvolution(incomingMetadata, {
|
|
153
|
+
status: "active",
|
|
154
|
+
version: previousEvolution.version + 1,
|
|
155
|
+
supersedes: historyId,
|
|
156
|
+
supersededBy: null,
|
|
157
|
+
validFrom: now,
|
|
158
|
+
validUntil: null,
|
|
159
|
+
// Access stats belong to the canonical key, not to a single version's text.
|
|
160
|
+
accessCount: previousEvolution.accessCount,
|
|
161
|
+
lastAccessedAt: previousEvolution.lastAccessedAt,
|
|
162
|
+
...(evolutionNote ? { evolutionNote } : {}),
|
|
163
|
+
});
|
|
164
|
+
}
|