recallnest 1.4.1 → 2.2.1
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/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
package/src/rif.ts
CHANGED
|
@@ -22,34 +22,60 @@ import type { RetrievalResult } from "./retriever.js";
|
|
|
22
22
|
* @param results - Scored results sorted by score descending
|
|
23
23
|
* @param similarityThreshold - Cosine similarity above which two results are "near-duplicate" (default: 0.85)
|
|
24
24
|
* @param scoreRatio - A result scoring below this ratio of its near-duplicate is demoted (default: 0.80)
|
|
25
|
+
* @param clusterTopK - F2: Max results per semantic cluster before demotion (default: 3)
|
|
25
26
|
*/
|
|
26
27
|
export function filterInterference(
|
|
27
28
|
results: RetrievalResult[],
|
|
28
29
|
similarityThreshold = 0.85,
|
|
29
30
|
scoreRatio = 0.80,
|
|
31
|
+
clusterTopK = 3,
|
|
30
32
|
): RetrievalResult[] {
|
|
31
33
|
if (results.length <= 2) return results;
|
|
32
34
|
|
|
33
35
|
const kept: RetrievalResult[] = [];
|
|
34
36
|
const demoted: RetrievalResult[] = [];
|
|
35
37
|
|
|
38
|
+
// F2: Track cluster membership — each kept result seeds or joins a cluster
|
|
39
|
+
// clusterMap: kept-index → cluster-id
|
|
40
|
+
const clusterMap: number[] = [];
|
|
41
|
+
const clusterCounts = new Map<number, number>(); // cluster-id → count
|
|
42
|
+
let nextClusterId = 0;
|
|
43
|
+
|
|
36
44
|
for (const candidate of results) {
|
|
37
|
-
// Check if this candidate is a weak near-duplicate of any kept result
|
|
38
45
|
let isDemoted = false;
|
|
46
|
+
let matchedClusterId: number | null = null;
|
|
39
47
|
|
|
40
|
-
for (
|
|
41
|
-
const sim = cosineSimilarity(candidate.entry.vector,
|
|
42
|
-
if (sim > similarityThreshold
|
|
43
|
-
|
|
48
|
+
for (let i = 0; i < kept.length; i++) {
|
|
49
|
+
const sim = cosineSimilarity(candidate.entry.vector, kept[i].entry.vector);
|
|
50
|
+
if (sim > similarityThreshold) {
|
|
51
|
+
matchedClusterId = clusterMap[i];
|
|
52
|
+
// Original RIF: demote if score too low relative to stronger result
|
|
53
|
+
if (candidate.score < kept[i].score * scoreRatio) {
|
|
54
|
+
isDemoted = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
// F2: Cluster top-K — demote if cluster already has K members
|
|
58
|
+
const count = clusterCounts.get(matchedClusterId) ?? 0;
|
|
59
|
+
if (count >= clusterTopK) {
|
|
60
|
+
// Demote with 50% score penalty instead of full demotion
|
|
61
|
+
demoted.push({ ...candidate, score: candidate.score * 0.5 });
|
|
62
|
+
isDemoted = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
44
65
|
break;
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
|
|
48
|
-
if (isDemoted) {
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
if (!isDemoted) {
|
|
70
|
+
const cid = matchedClusterId ?? nextClusterId++;
|
|
71
|
+
clusterMap.push(cid);
|
|
72
|
+
clusterCounts.set(cid, (clusterCounts.get(cid) ?? 0) + 1);
|
|
51
73
|
kept.push(candidate);
|
|
74
|
+
} else if (matchedClusterId == null) {
|
|
75
|
+
// Demoted by score ratio, not cluster overflow
|
|
76
|
+
demoted.push(candidate);
|
|
52
77
|
}
|
|
78
|
+
// cluster overflow case already pushed to demoted above
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
// Demoted results go to the end as fallback
|
package/src/runtime-config.ts
CHANGED
|
@@ -2,15 +2,17 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
|
+
import { metaDir } from "./compat.js";
|
|
5
6
|
import { MemoryStore, validateStoragePath } from "./store.js";
|
|
6
7
|
import { createEmbedder, getVectorDimensions, type EmbeddingConfig } from "./embedder.js";
|
|
7
8
|
import { createRetriever, type RetrievalConfig, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
|
|
8
9
|
import { applyRetrievalProfile } from "./retrieval-profiles.js";
|
|
9
10
|
import { AccessTracker } from "./access-tracker.js";
|
|
11
|
+
import { FrequencyTracker } from "./frequency-tracker.js";
|
|
10
12
|
import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
|
|
11
13
|
import { logInfo } from "./stderr-log.js";
|
|
12
14
|
|
|
13
|
-
export type RecallMode = "full" | "summary" | "off";
|
|
15
|
+
export type RecallMode = "full" | "light" | "summary" | "off";
|
|
14
16
|
|
|
15
17
|
export interface LocalMemoryConfig {
|
|
16
18
|
dbPath: string;
|
|
@@ -42,7 +44,7 @@ export interface LocalMemoryConfig {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export function loadDotEnv(): void {
|
|
45
|
-
const envPath = resolve(import.meta
|
|
47
|
+
const envPath = resolve(metaDir(import.meta), "../.env");
|
|
46
48
|
if (!existsSync(envPath)) return;
|
|
47
49
|
const content = readFileSync(envPath, "utf-8");
|
|
48
50
|
for (const line of content.split("\n")) {
|
|
@@ -60,13 +62,13 @@ export function loadDotEnv(): void {
|
|
|
60
62
|
export function findConfigPath(): string {
|
|
61
63
|
if (process.env.LOCAL_MEMORY_CONFIG) return resolve(process.env.LOCAL_MEMORY_CONFIG);
|
|
62
64
|
|
|
63
|
-
const localConfig = resolve(import.meta
|
|
65
|
+
const localConfig = resolve(metaDir(import.meta), "../config.json");
|
|
64
66
|
if (existsSync(localConfig)) return localConfig;
|
|
65
67
|
|
|
66
68
|
const branded = join(homedir(), ".config", "recallnest", "config.json");
|
|
67
69
|
if (existsSync(branded)) return branded;
|
|
68
70
|
|
|
69
|
-
const exampleExists = existsSync(resolve(import.meta
|
|
71
|
+
const exampleExists = existsSync(resolve(metaDir(import.meta), "../config.json.example"));
|
|
70
72
|
throw new Error(
|
|
71
73
|
"Config not found.\n" +
|
|
72
74
|
(exampleExists
|
|
@@ -95,7 +97,7 @@ export function expandHome(p: string): string {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
export function createComponents(config: LocalMemoryConfig, profileName?: string) {
|
|
98
|
-
const dbPath = resolve(import.meta
|
|
100
|
+
const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
|
|
99
101
|
validateStoragePath(dbPath);
|
|
100
102
|
|
|
101
103
|
const embeddingConfig: EmbeddingConfig = {
|
|
@@ -121,6 +123,13 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
121
123
|
const accessTracker = new AccessTracker(store);
|
|
122
124
|
retriever.setAccessTracker(accessTracker);
|
|
123
125
|
|
|
126
|
+
// P0.2: Attach frequency tracker for hit-count based boosting
|
|
127
|
+
const dataDir = resolve(metaDir(import.meta), "..", expandHome(config.dbPath), "..");
|
|
128
|
+
const frequencyTracker = new FrequencyTracker({
|
|
129
|
+
filePath: join(dataDir, "frequency-stats.json"),
|
|
130
|
+
});
|
|
131
|
+
retriever.setFrequencyTracker(frequencyTracker);
|
|
132
|
+
|
|
124
133
|
// Create LLM client if configured (optional, graceful)
|
|
125
134
|
let llm: LLMClient | null = null;
|
|
126
135
|
if (config.llm) {
|
|
@@ -130,11 +139,11 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
return { store, embedder, retriever, profile, accessTracker, llm };
|
|
142
|
+
return { store, embedder, retriever, profile, accessTracker, frequencyTracker, llm };
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|
|
137
|
-
const dbPath = resolve(import.meta
|
|
146
|
+
const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
|
|
138
147
|
validateStoragePath(dbPath);
|
|
139
148
|
return new MemoryStore({
|
|
140
149
|
dbPath,
|
|
@@ -144,18 +153,24 @@ export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|
|
|
144
153
|
|
|
145
154
|
export function createComponentResolver(config: LocalMemoryConfig) {
|
|
146
155
|
const cache = new Map<string, ReturnType<typeof createComponents>>();
|
|
156
|
+
const MAX_COMPONENT_CACHE_SIZE = 32;
|
|
147
157
|
|
|
148
158
|
return function getComponents(profileName?: string) {
|
|
149
159
|
const key = profileName || "default";
|
|
150
160
|
const cached = cache.get(key);
|
|
151
161
|
if (cached) return cached;
|
|
162
|
+
// Evict oldest entry if cache is full
|
|
163
|
+
if (cache.size >= MAX_COMPONENT_CACHE_SIZE) {
|
|
164
|
+
const firstKey = cache.keys().next().value;
|
|
165
|
+
if (firstKey !== undefined) cache.delete(firstKey);
|
|
166
|
+
}
|
|
152
167
|
const created = createComponents(config, profileName);
|
|
153
168
|
cache.set(key, created);
|
|
154
169
|
return created;
|
|
155
170
|
};
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
const VALID_RECALL_MODES: RecallMode[] = ["full", "summary", "off"];
|
|
173
|
+
const VALID_RECALL_MODES: RecallMode[] = ["full", "light", "summary", "off"];
|
|
159
174
|
|
|
160
175
|
/**
|
|
161
176
|
* Resolve effective recall mode: per-call override > env var > config > default ("summary").
|
package/src/scope-inventory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { metaDir } from "./compat.js";
|
|
4
5
|
import { suppressesLegacyScopeIssue } from "./legacy-scope-review.js";
|
|
5
6
|
import { classifyLegacyScope, type LegacyScopeAudit, type LegacyScopeIssueKind, type MemoryStore } from "./store.js";
|
|
6
7
|
|
|
@@ -205,9 +206,9 @@ export async function collectScopeInventory(options: ScopeInventoryOptions): Pro
|
|
|
205
206
|
recommendation: "Migrate or delete historical unscoped memory rows before removing compatibility shims.",
|
|
206
207
|
};
|
|
207
208
|
|
|
208
|
-
const pinsDir = options.pinsDir || resolve(import.meta
|
|
209
|
-
const checkpointsDir = options.checkpointsDir || resolve(import.meta
|
|
210
|
-
const workflowObservationsDir = options.workflowObservationsDir || resolve(import.meta
|
|
209
|
+
const pinsDir = options.pinsDir || resolve(metaDir(import.meta), "../data/pins");
|
|
210
|
+
const checkpointsDir = options.checkpointsDir || resolve(metaDir(import.meta), "../data/session-checkpoints");
|
|
211
|
+
const workflowObservationsDir = options.workflowObservationsDir || resolve(metaDir(import.meta), "../data/workflow-observations");
|
|
211
212
|
|
|
212
213
|
const pinLayer = scanJsonLayer({
|
|
213
214
|
dir: pinsDir,
|
package/src/scope-policy.ts
CHANGED
|
@@ -137,6 +137,10 @@ export function buildRetrievalContext(
|
|
|
137
137
|
includeArchived: base.includeArchived,
|
|
138
138
|
trace: base.trace,
|
|
139
139
|
graph: base.graph,
|
|
140
|
+
topicTag: base.topicTag,
|
|
141
|
+
reconstruct: base.reconstruct,
|
|
142
|
+
validAt: base.validAt,
|
|
143
|
+
includeExpired: base.includeExpired,
|
|
140
144
|
scopeFilter: selection.scopeFilter,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { LLMClient } from "./llm-client.js";
|
|
2
|
+
import type { Embedder } from "./embedder.js";
|
|
3
|
+
import type { MemoryStore } from "./store.js";
|
|
4
|
+
import type { ConflictCandidateStore } from "./conflict-store.js";
|
|
5
|
+
import type { KGExtractor } from "./kg-extractor.js";
|
|
6
|
+
import type { AuditLogger } from "./audit-log.js";
|
|
7
|
+
import type { StoredMemoryRecord, DurableMemoryCategory } from "./memory-schema.js";
|
|
8
|
+
import { persistMemory } from "./capture-engine.js";
|
|
9
|
+
import { detectLang, getSessionPromptHook } from "./language-hook.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface ConversationMessage {
|
|
16
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
17
|
+
content: string;
|
|
18
|
+
tool_name?: string;
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MicrocompactResult {
|
|
23
|
+
tokens_freed: number;
|
|
24
|
+
tools_cleared: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SummaryResult {
|
|
28
|
+
text: string;
|
|
29
|
+
dimensions: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PersistResult {
|
|
33
|
+
memories_stored: number;
|
|
34
|
+
memories_deduped: number;
|
|
35
|
+
memories_conflicted: number;
|
|
36
|
+
ids: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DistillResult {
|
|
40
|
+
microcompact: MicrocompactResult;
|
|
41
|
+
summary: SummaryResult;
|
|
42
|
+
persisted: PersistResult;
|
|
43
|
+
compacted_messages: ConversationMessage[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PersistDeps {
|
|
47
|
+
store: Pick<MemoryStore, "store" | "list" | "update" | "getById" | "get" | "vectorSearch">;
|
|
48
|
+
embedder: Pick<Embedder, "embedPassage">;
|
|
49
|
+
conflictStore?: Pick<ConflictCandidateStore, "save" | "replace" | "getOpenByFingerprint" | "getLatestByFingerprint">;
|
|
50
|
+
llm?: LLMClient | null;
|
|
51
|
+
kgExtractor?: KGExtractor | null;
|
|
52
|
+
auditLogger?: AuditLogger | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Constants
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const CLEARABLE_TOOLS = new Set([
|
|
60
|
+
"read_file", "bash", "grep", "glob", "web_search", "web_fetch",
|
|
61
|
+
"edit_file", "write_file", "Read", "Write", "Edit", "Bash",
|
|
62
|
+
"Grep", "Glob", "WebSearch", "WebFetch",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// Session summary fallback prompts (used when babel-memory is not installed)
|
|
66
|
+
|
|
67
|
+
const DEFAULT_DIMENSION_LABELS: Record<string, string> = {
|
|
68
|
+
user_intent: "User intent and requests",
|
|
69
|
+
technical_concepts: "Key technical concepts",
|
|
70
|
+
files_and_code: "Files and code segments involved",
|
|
71
|
+
errors_and_fixes: "Errors and fix records",
|
|
72
|
+
problem_solving: "Problem solving process",
|
|
73
|
+
user_quotes: "User original quotes preserved",
|
|
74
|
+
unfinished_tasks: "Unfinished tasks",
|
|
75
|
+
current_state: "Current work state",
|
|
76
|
+
next_steps: "Suggested next steps",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const DEFAULT_SESSION_SYSTEM = `You are a session summarizer. Analyze the conversation and produce a structured summary.
|
|
80
|
+
Wrap the summary in <summary></summary> tags.
|
|
81
|
+
Use numbered ## headings for each dimension:
|
|
82
|
+
${Object.entries(DEFAULT_DIMENSION_LABELS).map(([, label], i) => `## ${i + 1}. ${label}`).join("\n")}
|
|
83
|
+
If a dimension has no relevant content, write "N/A".`;
|
|
84
|
+
|
|
85
|
+
interface DimensionMapping {
|
|
86
|
+
category: DurableMemoryCategory;
|
|
87
|
+
importance: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DIMENSION_TO_MEMORY: Record<string, DimensionMapping> = {
|
|
91
|
+
user_intent: { category: "events", importance: 0.5 },
|
|
92
|
+
files_and_code: { category: "entities", importance: 0.6 },
|
|
93
|
+
errors_and_fixes: { category: "cases", importance: 0.7 },
|
|
94
|
+
problem_solving: { category: "patterns", importance: 0.8 },
|
|
95
|
+
user_quotes: { category: "preferences", importance: 0.7 },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Layer 1: Microcompact
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
export function microcompact(
|
|
103
|
+
messages: ConversationMessage[],
|
|
104
|
+
opts: { preserveRecent?: number; keepRecentTools?: number } = {},
|
|
105
|
+
): { messages: ConversationMessage[]; result: MicrocompactResult } {
|
|
106
|
+
const preserveRecent = opts.preserveRecent ?? 6;
|
|
107
|
+
const keepRecentTools = opts.keepRecentTools ?? 5;
|
|
108
|
+
|
|
109
|
+
if (messages.length === 0) {
|
|
110
|
+
return { messages: [], result: { tokens_freed: 0, tools_cleared: 0 } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cutoff = Math.max(0, messages.length - preserveRecent);
|
|
114
|
+
let tokensFreed = 0;
|
|
115
|
+
let toolsCleared = 0;
|
|
116
|
+
|
|
117
|
+
// Find tool messages eligible for clearing (before the preserve window)
|
|
118
|
+
const toolIndices: number[] = [];
|
|
119
|
+
for (let i = 0; i < cutoff; i++) {
|
|
120
|
+
const msg = messages[i];
|
|
121
|
+
if (msg.role === "tool" && msg.tool_name && CLEARABLE_TOOLS.has(msg.tool_name)) {
|
|
122
|
+
toolIndices.push(i);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Keep the most recent N tool results even if they're before the cutoff
|
|
127
|
+
const indicesToClear = new Set(
|
|
128
|
+
toolIndices.slice(0, Math.max(0, toolIndices.length - keepRecentTools)),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const compacted: ConversationMessage[] = [];
|
|
132
|
+
for (let i = 0; i < messages.length; i++) {
|
|
133
|
+
const msg = messages[i];
|
|
134
|
+
if (indicesToClear.has(i)) {
|
|
135
|
+
const tokenCount = Math.ceil(msg.content.length / 4);
|
|
136
|
+
tokensFreed += tokenCount;
|
|
137
|
+
toolsCleared++;
|
|
138
|
+
compacted.push({
|
|
139
|
+
...msg,
|
|
140
|
+
content: `[Cleared: ${msg.tool_name}, ~${tokenCount} tokens]`,
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
compacted.push({ ...msg });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
messages: compacted,
|
|
149
|
+
result: { tokens_freed: tokensFreed, tools_cleared: toolsCleared },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Layer 2: LLM Structured Summary
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
export async function summarizeSession(
|
|
158
|
+
messages: ConversationMessage[],
|
|
159
|
+
llm: LLMClient,
|
|
160
|
+
): Promise<SummaryResult> {
|
|
161
|
+
const conversationText = messages
|
|
162
|
+
.map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${m.content.slice(0, 500)}`)
|
|
163
|
+
.join("\n")
|
|
164
|
+
.slice(0, 8000);
|
|
165
|
+
|
|
166
|
+
// Use babel-memory bilingual prompt if available, otherwise fallback to defaults
|
|
167
|
+
const lang = detectLang(conversationText);
|
|
168
|
+
const babelPrompt = getSessionPromptHook(lang);
|
|
169
|
+
const sessionSystemPrompt = babelPrompt?.system ?? DEFAULT_SESSION_SYSTEM;
|
|
170
|
+
const dimensionLabels = babelPrompt?.dimensionLabels ?? DEFAULT_DIMENSION_LABELS;
|
|
171
|
+
const dimensionKeys = Object.keys(dimensionLabels);
|
|
172
|
+
|
|
173
|
+
const raw = await llm.chatLong(sessionSystemPrompt, conversationText, 2000);
|
|
174
|
+
|
|
175
|
+
if (!raw) {
|
|
176
|
+
return { text: "", dimensions: {} };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Extract <summary> block, discard <analysis>
|
|
180
|
+
const summaryMatch = raw.match(/<summary>([\s\S]*?)<\/summary>/);
|
|
181
|
+
const summaryText = summaryMatch ? summaryMatch[1].trim() : raw;
|
|
182
|
+
|
|
183
|
+
// Parse dimensions from ## headings
|
|
184
|
+
const dimensions: Record<string, string> = {};
|
|
185
|
+
const sections = summaryText.split(/^## \d+\.\s*/m).filter(Boolean);
|
|
186
|
+
|
|
187
|
+
for (const section of sections) {
|
|
188
|
+
const firstLine = section.split("\n")[0].trim().toLowerCase();
|
|
189
|
+
for (const key of dimensionKeys) {
|
|
190
|
+
const label = dimensionLabels[key].toLowerCase();
|
|
191
|
+
if (firstLine.includes(label) || label.includes(firstLine.replace(/\s+/g, " ").trim())) {
|
|
192
|
+
const body = section.split("\n").slice(1).join("\n").trim();
|
|
193
|
+
if (body && body !== "N/A") {
|
|
194
|
+
dimensions[key] = body;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { text: summaryText, dimensions };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Layer 3: Extract and Persist
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
export async function extractAndPersist(
|
|
209
|
+
summary: SummaryResult,
|
|
210
|
+
deps: PersistDeps,
|
|
211
|
+
scope: string,
|
|
212
|
+
): Promise<PersistResult> {
|
|
213
|
+
const result: PersistResult = {
|
|
214
|
+
memories_stored: 0,
|
|
215
|
+
memories_deduped: 0,
|
|
216
|
+
memories_conflicted: 0,
|
|
217
|
+
ids: [],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (const [dimKey, mapping] of Object.entries(DIMENSION_TO_MEMORY)) {
|
|
221
|
+
const dimText = summary.dimensions[dimKey];
|
|
222
|
+
if (!dimText || dimText.trim().length < 10) continue;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const stored = await persistMemory(deps, {
|
|
226
|
+
text: dimText.slice(0, 4000),
|
|
227
|
+
category: mapping.category,
|
|
228
|
+
importance: mapping.importance,
|
|
229
|
+
scope,
|
|
230
|
+
source: "agent" as const,
|
|
231
|
+
tags: ["session_distill", dimKey],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
trackDisposition(result, stored);
|
|
235
|
+
} catch {
|
|
236
|
+
// Non-fatal: continue with other dimensions
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function trackDisposition(result: PersistResult, stored: StoredMemoryRecord): void {
|
|
244
|
+
result.ids.push(stored.id);
|
|
245
|
+
switch (stored.disposition) {
|
|
246
|
+
case "deduped":
|
|
247
|
+
result.memories_deduped++;
|
|
248
|
+
break;
|
|
249
|
+
case "conflict":
|
|
250
|
+
result.memories_conflicted++;
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
result.memories_stored++;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Full Pipeline
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
export async function distillSession(
|
|
263
|
+
messages: ConversationMessage[],
|
|
264
|
+
deps: PersistDeps & { llm: LLMClient },
|
|
265
|
+
opts: {
|
|
266
|
+
scope?: string;
|
|
267
|
+
preserveRecent?: number;
|
|
268
|
+
keepRecentTools?: number;
|
|
269
|
+
persist?: boolean;
|
|
270
|
+
} = {},
|
|
271
|
+
): Promise<DistillResult> {
|
|
272
|
+
const scope = opts.scope || "default";
|
|
273
|
+
const shouldPersist = opts.persist ?? true;
|
|
274
|
+
|
|
275
|
+
// Layer 1: Microcompact
|
|
276
|
+
const { messages: compacted, result: microcompactResult } = microcompact(
|
|
277
|
+
messages,
|
|
278
|
+
{ preserveRecent: opts.preserveRecent, keepRecentTools: opts.keepRecentTools },
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Layer 2: Summarize (using compacted messages minus preserved recent)
|
|
282
|
+
const preserveRecent = opts.preserveRecent ?? 6;
|
|
283
|
+
const cutoff = Math.max(0, compacted.length - preserveRecent);
|
|
284
|
+
const oldMessages = compacted.slice(0, cutoff);
|
|
285
|
+
const summaryResult = oldMessages.length > 0
|
|
286
|
+
? await summarizeSession(oldMessages, deps.llm)
|
|
287
|
+
: { text: "", dimensions: {} };
|
|
288
|
+
|
|
289
|
+
// Layer 3: Persist (optional)
|
|
290
|
+
let persistResult: PersistResult = {
|
|
291
|
+
memories_stored: 0,
|
|
292
|
+
memories_deduped: 0,
|
|
293
|
+
memories_conflicted: 0,
|
|
294
|
+
ids: [],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (shouldPersist && Object.keys(summaryResult.dimensions).length > 0) {
|
|
298
|
+
persistResult = await extractAndPersist(summaryResult, deps, scope);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
microcompact: microcompactResult,
|
|
303
|
+
summary: summaryResult,
|
|
304
|
+
persisted: persistResult,
|
|
305
|
+
compacted_messages: compacted,
|
|
306
|
+
};
|
|
307
|
+
}
|
package/src/session-output.ts
CHANGED
|
@@ -82,6 +82,41 @@ export function formatResumeContext(response: ResumeContextResponse): string {
|
|
|
82
82
|
...listBlock("Recent cases", response.recentCases),
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
+
// CC-7: Collapsed items with renderLevel + staleness hints
|
|
86
|
+
if (response.collapsedItems && response.collapsedItems.length > 0) {
|
|
87
|
+
lines.push("Collapsed context (mixed granularity):");
|
|
88
|
+
for (const item of response.collapsedItems) {
|
|
89
|
+
const hint = item.stalenessHint ? ` ${item.stalenessHint}` : "";
|
|
90
|
+
lines.push(`[${item.renderLevel}] ${item.text}${hint}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// CC-8: Essential context (pinned memories, active patterns, open loops)
|
|
95
|
+
if (response.essentialContext) {
|
|
96
|
+
const ec = response.essentialContext;
|
|
97
|
+
const hasContent = (ec.pinnedMemories && ec.pinnedMemories.length > 0)
|
|
98
|
+
|| (ec.activePatterns && ec.activePatterns.length > 0)
|
|
99
|
+
|| (ec.openLoops && ec.openLoops.length > 0);
|
|
100
|
+
if (hasContent) {
|
|
101
|
+
lines.push("Essential context:");
|
|
102
|
+
if (ec.pinnedMemories && ec.pinnedMemories.length > 0) {
|
|
103
|
+
for (const pin of ec.pinnedMemories) {
|
|
104
|
+
lines.push(`- Pinned: ${pin}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (ec.activePatterns && ec.activePatterns.length > 0) {
|
|
108
|
+
for (const pattern of ec.activePatterns) {
|
|
109
|
+
lines.push(`- Pattern: ${pattern}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (ec.openLoops && ec.openLoops.length > 0) {
|
|
113
|
+
for (const loop of ec.openLoops) {
|
|
114
|
+
lines.push(`- Open loop: ${loop}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
if (response.latestCheckpoint) {
|
|
86
121
|
lines.push("Latest checkpoint:");
|
|
87
122
|
lines.push(`Session: ${response.latestCheckpoint.sessionId}`);
|
|
@@ -92,5 +127,10 @@ export function formatResumeContext(response: ResumeContextResponse): string {
|
|
|
92
127
|
lines.push(`Summary: ${response.latestCheckpoint.summary}`);
|
|
93
128
|
}
|
|
94
129
|
|
|
130
|
+
// CC-1: Injection hint for prompt placement
|
|
131
|
+
if (response.injectionHint) {
|
|
132
|
+
lines.push(`Injection hint: ${response.injectionHint} (place recalled context as user message attachment for better prompt cache hit rate)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
95
135
|
return lines.join("\n");
|
|
96
136
|
}
|
package/src/session-schema.ts
CHANGED
|
@@ -40,15 +40,51 @@ export const ResumeCheckpointSummarySchema = z.object({
|
|
|
40
40
|
|
|
41
41
|
export const ResumeResponseModeSchema = z.enum(["default", "recall-only"]);
|
|
42
42
|
|
|
43
|
+
export const CollapsedItemSchema = z.object({
|
|
44
|
+
entryId: z.string(),
|
|
45
|
+
text: z.string(),
|
|
46
|
+
renderLevel: z.enum(["L0", "L1", "L2"]),
|
|
47
|
+
stalenessHint: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** CC-8: Essential context reconstructed after compact — pinned memories, active patterns, open loops. */
|
|
51
|
+
export const EssentialContextSchema = z.object({
|
|
52
|
+
pinnedMemories: z.array(z.string()).max(3).optional(),
|
|
53
|
+
activePatterns: z.array(z.string()).max(2).optional(),
|
|
54
|
+
openLoops: z.array(z.string()).max(3).optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
43
57
|
export const ResumeContextResponseSchema = z.object({
|
|
44
58
|
summary: boundedStringSchema("summary", 800),
|
|
45
59
|
resolvedScope: optionalBoundedStringSchema(160),
|
|
46
60
|
stableContext: normalizedStringListSchema("stableContext", 6, 220),
|
|
47
61
|
relevantPatterns: normalizedStringListSchema("relevantPatterns", 6, 220),
|
|
48
62
|
recentCases: normalizedStringListSchema("recentCases", 6, 220),
|
|
63
|
+
/** CC-7: Mixed-granularity collapsed view of all recalled items. */
|
|
64
|
+
collapsedItems: z.array(CollapsedItemSchema).max(20).optional(),
|
|
65
|
+
/** CC-8: Essential context reconstructed after compact. */
|
|
66
|
+
essentialContext: EssentialContextSchema.optional(),
|
|
49
67
|
latestCheckpoint: ResumeCheckpointSummarySchema.optional(),
|
|
68
|
+
/** CC-1: Hint for where to inject recalled context in the prompt. */
|
|
69
|
+
injectionHint: z.enum(["system_prompt", "user_attachment"]).default("user_attachment").optional(),
|
|
70
|
+
/** Upstream #345: Mark recalled context as ephemeral — host should discard on compaction, not persist to transcript. */
|
|
71
|
+
ephemeral: z.boolean().default(true).optional(),
|
|
50
72
|
responseMode: ResumeResponseModeSchema.default("default"),
|
|
51
73
|
responseGuidance: optionalBoundedStringSchema(400),
|
|
74
|
+
/** Constructive retrieval: LLM-synthesized reconstruction of context. */
|
|
75
|
+
reconstructedContext: z.string().max(2000).optional(),
|
|
76
|
+
/** Confidence score (0-1) for the reconstructed context. */
|
|
77
|
+
reconstructionConfidence: z.number().min(0).max(1).optional(),
|
|
78
|
+
/** Phase 4: Contradictions detected during reconstruction. */
|
|
79
|
+
reconstructionContradictions: z.array(z.object({
|
|
80
|
+
memoryIds: z.tuple([z.string(), z.string()]),
|
|
81
|
+
description: z.string().max(300),
|
|
82
|
+
})).max(5).optional(),
|
|
83
|
+
/** HP-narrative: Memories grouped by autobiographical life period. */
|
|
84
|
+
narrativeGroups: z.array(z.object({
|
|
85
|
+
period: z.string().max(120),
|
|
86
|
+
items: z.array(z.string().max(240)).max(5),
|
|
87
|
+
})).max(10).optional(),
|
|
52
88
|
generatedAt: z.string().datetime(),
|
|
53
89
|
});
|
|
54
90
|
|
|
@@ -56,5 +92,6 @@ export type SessionCheckpointInput = z.infer<typeof SessionCheckpointInputSchema
|
|
|
56
92
|
export type SessionCheckpointRecord = z.infer<typeof SessionCheckpointRecordSchema>;
|
|
57
93
|
export type ResumeContextRequest = z.infer<typeof ResumeContextRequestSchema>;
|
|
58
94
|
export type ResumeContextResponse = z.infer<typeof ResumeContextResponseSchema>;
|
|
95
|
+
export type EssentialContext = z.infer<typeof EssentialContextSchema>;
|
|
59
96
|
export type RetrievalProfileName = z.infer<typeof RetrievalProfileSchema>;
|
|
60
97
|
export type ResumeResponseMode = z.infer<typeof ResumeResponseModeSchema>;
|
package/src/session-store.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { metaDir } from "./compat.js";
|
|
4
5
|
import type { SessionCheckpointRecord } from "./session-schema.js";
|
|
5
6
|
import { SessionCheckpointRecordSchema } from "./session-schema.js";
|
|
6
7
|
import { normalizeCheckpointScope, type CheckpointQuality } from "./session-engine.js";
|
|
@@ -66,7 +67,7 @@ export interface CheckpointGcResult {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export class SessionCheckpointStore {
|
|
69
|
-
constructor(private readonly dir = resolve(import.meta
|
|
70
|
+
constructor(private readonly dir = resolve(metaDir(import.meta), "../data/session-checkpoints")) {}
|
|
70
71
|
|
|
71
72
|
get dataDir(): string {
|
|
72
73
|
return ensureDir(this.dir);
|
|
@@ -76,7 +77,11 @@ export class SessionCheckpointStore {
|
|
|
76
77
|
const parsed = SessionCheckpointRecordSchema.parse(record);
|
|
77
78
|
const timestampToken = parsed.updatedAt.replace(/[:.]/g, "-");
|
|
78
79
|
const path = join(this.dataDir, `${timestampToken}-${parsed.checkpointId}.json`);
|
|
79
|
-
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("[recallnest] Failed to save session checkpoint:", err instanceof Error ? err.message : String(err));
|
|
84
|
+
}
|
|
80
85
|
return parsed;
|
|
81
86
|
}
|
|
82
87
|
|