claude-memory-layer 1.0.10 → 1.0.12
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +3577 -389
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1383 -138
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1917 -214
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1813 -231
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1802 -205
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1909 -248
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1861 -206
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +2341 -217
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +2350 -226
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1805 -206
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +1447 -55
- package/dist/ui/index.html +318 -147
- package/dist/ui/style.css +892 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/docs/OPERATIONS.md +18 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +9 -2
- package/scripts/build.ts +6 -0
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +391 -60
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +794 -7
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +51 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +9 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +89 -8
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +508 -71
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1447 -55
- package/src/ui/index.html +318 -147
- package/src/ui/style.css +892 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
package/dist/cli/index.js
CHANGED
|
@@ -5,18 +5,25 @@ import { dirname } from 'path';
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined")
|
|
12
|
+
return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
8
15
|
|
|
9
16
|
// src/cli/index.ts
|
|
10
17
|
import { Command } from "commander";
|
|
11
18
|
import { exec } from "child_process";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
19
|
+
import * as fs9 from "fs";
|
|
20
|
+
import * as path9 from "path";
|
|
21
|
+
import * as os6 from "os";
|
|
15
22
|
|
|
16
23
|
// src/services/memory-service.ts
|
|
17
|
-
import * as
|
|
24
|
+
import * as path3 from "path";
|
|
18
25
|
import * as os from "os";
|
|
19
|
-
import * as
|
|
26
|
+
import * as fs4 from "fs";
|
|
20
27
|
import * as crypto2 from "crypto";
|
|
21
28
|
|
|
22
29
|
// src/core/event-store.ts
|
|
@@ -74,57 +81,57 @@ function toDate(value) {
|
|
|
74
81
|
return new Date(value);
|
|
75
82
|
return new Date(String(value));
|
|
76
83
|
}
|
|
77
|
-
function createDatabase(
|
|
84
|
+
function createDatabase(path10, options) {
|
|
78
85
|
if (options?.readOnly) {
|
|
79
|
-
return new duckdb.Database(
|
|
86
|
+
return new duckdb.Database(path10, { access_mode: "READ_ONLY" });
|
|
80
87
|
}
|
|
81
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path10);
|
|
82
89
|
}
|
|
83
90
|
function dbRun(db, sql, params = []) {
|
|
84
|
-
return new Promise((
|
|
91
|
+
return new Promise((resolve4, reject) => {
|
|
85
92
|
if (params.length === 0) {
|
|
86
93
|
db.run(sql, (err) => {
|
|
87
94
|
if (err)
|
|
88
95
|
reject(err);
|
|
89
96
|
else
|
|
90
|
-
|
|
97
|
+
resolve4();
|
|
91
98
|
});
|
|
92
99
|
} else {
|
|
93
100
|
db.run(sql, ...params, (err) => {
|
|
94
101
|
if (err)
|
|
95
102
|
reject(err);
|
|
96
103
|
else
|
|
97
|
-
|
|
104
|
+
resolve4();
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
});
|
|
101
108
|
}
|
|
102
109
|
function dbAll(db, sql, params = []) {
|
|
103
|
-
return new Promise((
|
|
110
|
+
return new Promise((resolve4, reject) => {
|
|
104
111
|
if (params.length === 0) {
|
|
105
112
|
db.all(sql, (err, rows) => {
|
|
106
113
|
if (err)
|
|
107
114
|
reject(err);
|
|
108
115
|
else
|
|
109
|
-
|
|
116
|
+
resolve4(convertBigInts(rows || []));
|
|
110
117
|
});
|
|
111
118
|
} else {
|
|
112
119
|
db.all(sql, ...params, (err, rows) => {
|
|
113
120
|
if (err)
|
|
114
121
|
reject(err);
|
|
115
122
|
else
|
|
116
|
-
|
|
123
|
+
resolve4(convertBigInts(rows || []));
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
126
|
});
|
|
120
127
|
}
|
|
121
128
|
function dbClose(db) {
|
|
122
|
-
return new Promise((
|
|
129
|
+
return new Promise((resolve4, reject) => {
|
|
123
130
|
db.close((err) => {
|
|
124
131
|
if (err)
|
|
125
132
|
reject(err);
|
|
126
133
|
else
|
|
127
|
-
|
|
134
|
+
resolve4();
|
|
128
135
|
});
|
|
129
136
|
});
|
|
130
137
|
}
|
|
@@ -342,6 +349,17 @@ var EventStore = class {
|
|
|
342
349
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
343
350
|
)
|
|
344
351
|
`);
|
|
352
|
+
await dbRun(this.db, `
|
|
353
|
+
CREATE TABLE IF NOT EXISTS consolidated_rules (
|
|
354
|
+
rule_id VARCHAR PRIMARY KEY,
|
|
355
|
+
rule TEXT NOT NULL,
|
|
356
|
+
topics JSON,
|
|
357
|
+
source_memory_ids JSON,
|
|
358
|
+
source_events JSON,
|
|
359
|
+
confidence FLOAT DEFAULT 0.5,
|
|
360
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
361
|
+
)
|
|
362
|
+
`);
|
|
345
363
|
await dbRun(this.db, `
|
|
346
364
|
CREATE TABLE IF NOT EXISTS endless_config (
|
|
347
365
|
key VARCHAR PRIMARY KEY,
|
|
@@ -361,6 +379,7 @@ var EventStore = class {
|
|
|
361
379
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at)`);
|
|
362
380
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score DESC)`);
|
|
363
381
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence DESC)`);
|
|
382
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence DESC)`);
|
|
364
383
|
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at)`);
|
|
365
384
|
this.initialized = true;
|
|
366
385
|
}
|
|
@@ -748,8 +767,14 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
748
767
|
|
|
749
768
|
// src/core/sqlite-wrapper.ts
|
|
750
769
|
import Database from "better-sqlite3";
|
|
751
|
-
|
|
752
|
-
|
|
770
|
+
import * as fs from "fs";
|
|
771
|
+
import * as nodePath from "path";
|
|
772
|
+
function createSQLiteDatabase(path10, options) {
|
|
773
|
+
const dir = nodePath.dirname(path10);
|
|
774
|
+
if (!fs.existsSync(dir)) {
|
|
775
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
776
|
+
}
|
|
777
|
+
const db = new Database(path10, {
|
|
753
778
|
readonly: options?.readonly ?? false
|
|
754
779
|
});
|
|
755
780
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -790,6 +815,64 @@ function toSQLiteTimestamp(date) {
|
|
|
790
815
|
return date.toISOString();
|
|
791
816
|
}
|
|
792
817
|
|
|
818
|
+
// src/core/markdown-mirror.ts
|
|
819
|
+
import * as fs2 from "fs/promises";
|
|
820
|
+
import * as path from "path";
|
|
821
|
+
var DEFAULT_NAMESPACE = "default";
|
|
822
|
+
var DEFAULT_CATEGORY = "uncategorized";
|
|
823
|
+
function sanitizeSegment(input, fallback) {
|
|
824
|
+
const raw = String(input ?? "").trim().toLowerCase();
|
|
825
|
+
const safe = raw.normalize("NFKD").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
826
|
+
if (!safe || safe === "." || safe === "..")
|
|
827
|
+
return fallback;
|
|
828
|
+
return safe;
|
|
829
|
+
}
|
|
830
|
+
function getCategorySegments(metadata, eventType) {
|
|
831
|
+
const raw = metadata?.categoryPath;
|
|
832
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
833
|
+
return raw.map((s) => sanitizeSegment(s, DEFAULT_CATEGORY));
|
|
834
|
+
}
|
|
835
|
+
const single = metadata?.category;
|
|
836
|
+
if (typeof single === "string" && single.trim()) {
|
|
837
|
+
return [sanitizeSegment(single, DEFAULT_CATEGORY)];
|
|
838
|
+
}
|
|
839
|
+
return [sanitizeSegment(eventType, DEFAULT_CATEGORY)];
|
|
840
|
+
}
|
|
841
|
+
function buildMirrorPath(rootDir, event) {
|
|
842
|
+
const metadata = event.metadata;
|
|
843
|
+
const namespace = sanitizeSegment(metadata?.namespace, DEFAULT_NAMESPACE);
|
|
844
|
+
const categories = getCategorySegments(metadata, event.eventType);
|
|
845
|
+
const d = event.timestamp;
|
|
846
|
+
const yyyy = d.getFullYear();
|
|
847
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
848
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
849
|
+
return path.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
|
|
850
|
+
}
|
|
851
|
+
function formatMirrorEntry(event) {
|
|
852
|
+
const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
|
|
853
|
+
return [
|
|
854
|
+
"",
|
|
855
|
+
`- ts: ${event.timestamp.toISOString()}`,
|
|
856
|
+
` id: ${event.id}`,
|
|
857
|
+
` type: ${event.eventType}`,
|
|
858
|
+
` session: ${event.sessionId}`,
|
|
859
|
+
` category: ${category}`,
|
|
860
|
+
" content: |",
|
|
861
|
+
...event.content.split("\n").map((line) => ` ${line}`)
|
|
862
|
+
].join("\n") + "\n";
|
|
863
|
+
}
|
|
864
|
+
var MarkdownMirror = class {
|
|
865
|
+
constructor(rootDir) {
|
|
866
|
+
this.rootDir = rootDir;
|
|
867
|
+
}
|
|
868
|
+
async append(event) {
|
|
869
|
+
const outPath = buildMirrorPath(this.rootDir, event);
|
|
870
|
+
await fs2.mkdir(path.dirname(outPath), { recursive: true });
|
|
871
|
+
await fs2.appendFile(outPath, formatMirrorEntry(event), "utf8");
|
|
872
|
+
return outPath;
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
793
876
|
// src/core/sqlite-event-store.ts
|
|
794
877
|
var SQLiteEventStore = class {
|
|
795
878
|
constructor(dbPath, options) {
|
|
@@ -799,10 +882,12 @@ var SQLiteEventStore = class {
|
|
|
799
882
|
readonly: this.readOnly,
|
|
800
883
|
walMode: !this.readOnly
|
|
801
884
|
});
|
|
885
|
+
this.markdownMirror = this.readOnly || !options?.markdownMirrorRoot ? null : new MarkdownMirror(options.markdownMirrorRoot);
|
|
802
886
|
}
|
|
803
887
|
db;
|
|
804
888
|
initialized = false;
|
|
805
889
|
readOnly;
|
|
890
|
+
markdownMirror;
|
|
806
891
|
/**
|
|
807
892
|
* Initialize database schema
|
|
808
893
|
*/
|
|
@@ -1009,6 +1094,17 @@ var SQLiteEventStore = class {
|
|
|
1009
1094
|
created_at TEXT DEFAULT (datetime('now'))
|
|
1010
1095
|
);
|
|
1011
1096
|
|
|
1097
|
+
-- Consolidated Rules table (long-term stable memory)
|
|
1098
|
+
CREATE TABLE IF NOT EXISTS consolidated_rules (
|
|
1099
|
+
rule_id TEXT PRIMARY KEY,
|
|
1100
|
+
rule TEXT NOT NULL,
|
|
1101
|
+
topics TEXT,
|
|
1102
|
+
source_memory_ids TEXT,
|
|
1103
|
+
source_events TEXT,
|
|
1104
|
+
confidence REAL DEFAULT 0.5,
|
|
1105
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1012
1108
|
-- Endless Mode Config table
|
|
1013
1109
|
CREATE TABLE IF NOT EXISTS endless_config (
|
|
1014
1110
|
key TEXT PRIMARY KEY,
|
|
@@ -1016,6 +1112,41 @@ var SQLiteEventStore = class {
|
|
|
1016
1112
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
1017
1113
|
);
|
|
1018
1114
|
|
|
1115
|
+
-- Memory Helpfulness tracking
|
|
1116
|
+
CREATE TABLE IF NOT EXISTS memory_helpfulness (
|
|
1117
|
+
id TEXT PRIMARY KEY,
|
|
1118
|
+
event_id TEXT NOT NULL,
|
|
1119
|
+
session_id TEXT NOT NULL,
|
|
1120
|
+
retrieval_score REAL DEFAULT 0,
|
|
1121
|
+
query_preview TEXT,
|
|
1122
|
+
session_continued INTEGER DEFAULT 0,
|
|
1123
|
+
prompt_count_after INTEGER DEFAULT 0,
|
|
1124
|
+
tool_success_count INTEGER DEFAULT 0,
|
|
1125
|
+
tool_total_count INTEGER DEFAULT 0,
|
|
1126
|
+
was_reasked INTEGER DEFAULT 0,
|
|
1127
|
+
helpfulness_score REAL DEFAULT 0.5,
|
|
1128
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1129
|
+
measured_at TEXT
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
-- Retrieval trace log (query -> candidates -> selected for context)
|
|
1133
|
+
CREATE TABLE IF NOT EXISTS retrieval_traces (
|
|
1134
|
+
trace_id TEXT PRIMARY KEY,
|
|
1135
|
+
session_id TEXT,
|
|
1136
|
+
project_hash TEXT,
|
|
1137
|
+
query_text TEXT NOT NULL,
|
|
1138
|
+
strategy TEXT,
|
|
1139
|
+
candidate_event_ids TEXT,
|
|
1140
|
+
selected_event_ids TEXT,
|
|
1141
|
+
candidate_details_json TEXT,
|
|
1142
|
+
selected_details_json TEXT,
|
|
1143
|
+
candidate_count INTEGER DEFAULT 0,
|
|
1144
|
+
selected_count INTEGER DEFAULT 0,
|
|
1145
|
+
confidence TEXT,
|
|
1146
|
+
fallback_trace TEXT,
|
|
1147
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1019
1150
|
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1020
1151
|
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1021
1152
|
target_name TEXT PRIMARY KEY,
|
|
@@ -1040,7 +1171,14 @@ var SQLiteEventStore = class {
|
|
|
1040
1171
|
CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
|
|
1041
1172
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1042
1173
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1174
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence);
|
|
1043
1175
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1176
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
|
|
1177
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
|
|
1178
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
|
|
1179
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_created_at ON retrieval_traces(created_at DESC);
|
|
1180
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_project_hash ON retrieval_traces(project_hash);
|
|
1181
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_session_id ON retrieval_traces(session_id);
|
|
1044
1182
|
|
|
1045
1183
|
-- FTS5 Full-Text Search for fast keyword search
|
|
1046
1184
|
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
@@ -1064,6 +1202,14 @@ var SQLiteEventStore = class {
|
|
|
1064
1202
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1065
1203
|
END;
|
|
1066
1204
|
`);
|
|
1205
|
+
try {
|
|
1206
|
+
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
try {
|
|
1210
|
+
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1067
1213
|
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1068
1214
|
const columnNames = tableInfo.map((col) => col.name);
|
|
1069
1215
|
if (!columnNames.includes("access_count")) {
|
|
@@ -1084,6 +1230,15 @@ var SQLiteEventStore = class {
|
|
|
1084
1230
|
console.error("Error adding last_accessed_at column:", err);
|
|
1085
1231
|
}
|
|
1086
1232
|
}
|
|
1233
|
+
if (!columnNames.includes("turn_id")) {
|
|
1234
|
+
try {
|
|
1235
|
+
sqliteExec(this.db, `
|
|
1236
|
+
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
1237
|
+
`);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
console.error("Error adding turn_id column:", err);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1087
1242
|
try {
|
|
1088
1243
|
sqliteExec(this.db, `
|
|
1089
1244
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1096,6 +1251,12 @@ var SQLiteEventStore = class {
|
|
|
1096
1251
|
`);
|
|
1097
1252
|
} catch (err) {
|
|
1098
1253
|
}
|
|
1254
|
+
try {
|
|
1255
|
+
sqliteExec(this.db, `
|
|
1256
|
+
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
1257
|
+
`);
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
}
|
|
1099
1260
|
this.initialized = true;
|
|
1100
1261
|
}
|
|
1101
1262
|
/**
|
|
@@ -1120,9 +1281,11 @@ var SQLiteEventStore = class {
|
|
|
1120
1281
|
const id = randomUUID2();
|
|
1121
1282
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1122
1283
|
try {
|
|
1284
|
+
const metadata = input.metadata || {};
|
|
1285
|
+
const turnId = metadata.turnId || null;
|
|
1123
1286
|
const insertEvent = this.db.prepare(`
|
|
1124
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1125
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1287
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1288
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1126
1289
|
`);
|
|
1127
1290
|
const insertDedup = this.db.prepare(`
|
|
1128
1291
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1139,12 +1302,28 @@ var SQLiteEventStore = class {
|
|
|
1139
1302
|
input.content,
|
|
1140
1303
|
canonicalKey,
|
|
1141
1304
|
dedupeKey,
|
|
1142
|
-
JSON.stringify(
|
|
1305
|
+
JSON.stringify(metadata),
|
|
1306
|
+
turnId
|
|
1143
1307
|
);
|
|
1144
1308
|
insertDedup.run(dedupeKey, id);
|
|
1145
1309
|
insertLevel.run(id);
|
|
1146
1310
|
});
|
|
1147
1311
|
transaction();
|
|
1312
|
+
if (this.markdownMirror) {
|
|
1313
|
+
const event = {
|
|
1314
|
+
id,
|
|
1315
|
+
eventType: input.eventType,
|
|
1316
|
+
sessionId: input.sessionId,
|
|
1317
|
+
timestamp: input.timestamp,
|
|
1318
|
+
content: input.content,
|
|
1319
|
+
canonicalKey,
|
|
1320
|
+
dedupeKey,
|
|
1321
|
+
metadata
|
|
1322
|
+
};
|
|
1323
|
+
this.markdownMirror.append(event).catch((err) => {
|
|
1324
|
+
console.warn("[SQLiteEventStore] markdown mirror append failed:", err);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1148
1327
|
return { success: true, eventId: id, isDuplicate: false };
|
|
1149
1328
|
} catch (error) {
|
|
1150
1329
|
return {
|
|
@@ -1203,6 +1382,92 @@ var SQLiteEventStore = class {
|
|
|
1203
1382
|
);
|
|
1204
1383
|
return rows.map(this.rowToEvent);
|
|
1205
1384
|
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get events since a SQLite rowid (for robust incremental replication).
|
|
1387
|
+
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
1388
|
+
*/
|
|
1389
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
1390
|
+
await this.initialize();
|
|
1391
|
+
const rows = sqliteAll(
|
|
1392
|
+
this.db,
|
|
1393
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
1394
|
+
[lastRowid, limit]
|
|
1395
|
+
);
|
|
1396
|
+
return rows.map((row) => ({
|
|
1397
|
+
rowid: row._rowid,
|
|
1398
|
+
event: this.rowToEvent(row)
|
|
1399
|
+
}));
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Import events with fixed IDs (used for cross-machine replication).
|
|
1403
|
+
* Idempotent: skips if event id or dedupeKey already exists.
|
|
1404
|
+
*
|
|
1405
|
+
* NOTE: This bypasses the append() id generation to preserve stable IDs.
|
|
1406
|
+
*/
|
|
1407
|
+
async importEvents(events) {
|
|
1408
|
+
if (events.length === 0)
|
|
1409
|
+
return { inserted: 0, skipped: 0 };
|
|
1410
|
+
if (this.readOnly)
|
|
1411
|
+
return { inserted: 0, skipped: events.length };
|
|
1412
|
+
await this.initialize();
|
|
1413
|
+
const getById = this.db.prepare(`SELECT id FROM events WHERE id = ?`);
|
|
1414
|
+
const getByDedupe = this.db.prepare(`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`);
|
|
1415
|
+
const insertEvent = this.db.prepare(`
|
|
1416
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1417
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1418
|
+
`);
|
|
1419
|
+
const insertDedup = this.db.prepare(`
|
|
1420
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
1421
|
+
`);
|
|
1422
|
+
const insertLevel = this.db.prepare(`
|
|
1423
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
1424
|
+
`);
|
|
1425
|
+
let inserted = 0;
|
|
1426
|
+
let skipped = 0;
|
|
1427
|
+
const insertedEvents = [];
|
|
1428
|
+
const tx = this.db.transaction((batch) => {
|
|
1429
|
+
for (const ev of batch) {
|
|
1430
|
+
const existingById = getById.get(ev.id);
|
|
1431
|
+
if (existingById) {
|
|
1432
|
+
skipped++;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
const canonicalKey = ev.canonicalKey || makeCanonicalKey(ev.content);
|
|
1436
|
+
const dedupeKey = ev.dedupeKey || makeDedupeKey(ev.content, ev.sessionId);
|
|
1437
|
+
const existingByDedupe = getByDedupe.get(dedupeKey);
|
|
1438
|
+
if (existingByDedupe) {
|
|
1439
|
+
skipped++;
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
const metadata = ev.metadata || {};
|
|
1443
|
+
const turnId = metadata.turnId;
|
|
1444
|
+
insertEvent.run(
|
|
1445
|
+
ev.id,
|
|
1446
|
+
ev.eventType,
|
|
1447
|
+
ev.sessionId,
|
|
1448
|
+
toSQLiteTimestamp(ev.timestamp),
|
|
1449
|
+
ev.content,
|
|
1450
|
+
canonicalKey,
|
|
1451
|
+
dedupeKey,
|
|
1452
|
+
JSON.stringify(metadata),
|
|
1453
|
+
turnId ?? null
|
|
1454
|
+
);
|
|
1455
|
+
insertDedup.run(dedupeKey, ev.id);
|
|
1456
|
+
insertLevel.run(ev.id);
|
|
1457
|
+
inserted++;
|
|
1458
|
+
insertedEvents.push(ev);
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
tx(events);
|
|
1462
|
+
if (this.markdownMirror && insertedEvents.length > 0) {
|
|
1463
|
+
for (const ev of insertedEvents) {
|
|
1464
|
+
this.markdownMirror.append(ev).catch((err) => {
|
|
1465
|
+
console.warn("[SQLiteEventStore] markdown mirror append failed:", err);
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return { inserted, skipped };
|
|
1470
|
+
}
|
|
1206
1471
|
/**
|
|
1207
1472
|
* Create or update session
|
|
1208
1473
|
*/
|
|
@@ -1365,6 +1630,35 @@ var SQLiteEventStore = class {
|
|
|
1365
1630
|
[error, ...ids]
|
|
1366
1631
|
);
|
|
1367
1632
|
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Get embedding/vector outbox health statistics
|
|
1635
|
+
*/
|
|
1636
|
+
async getOutboxStats() {
|
|
1637
|
+
await this.initialize();
|
|
1638
|
+
const embeddingRows = sqliteAll(
|
|
1639
|
+
this.db,
|
|
1640
|
+
`SELECT status, COUNT(*) as count FROM embedding_outbox GROUP BY status`
|
|
1641
|
+
);
|
|
1642
|
+
const vectorRows = sqliteAll(
|
|
1643
|
+
this.db,
|
|
1644
|
+
`SELECT status, COUNT(*) as count FROM vector_outbox GROUP BY status`
|
|
1645
|
+
);
|
|
1646
|
+
const fromRows = (rows) => {
|
|
1647
|
+
const out = { pending: 0, processing: 0, failed: 0, total: 0 };
|
|
1648
|
+
for (const row of rows) {
|
|
1649
|
+
const key = row.status;
|
|
1650
|
+
if (key === "pending" || key === "processing" || key === "failed") {
|
|
1651
|
+
out[key] += row.count;
|
|
1652
|
+
}
|
|
1653
|
+
out.total += row.count;
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
};
|
|
1657
|
+
return {
|
|
1658
|
+
embedding: fromRows(embeddingRows),
|
|
1659
|
+
vector: fromRows(vectorRows)
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1368
1662
|
/**
|
|
1369
1663
|
* Update memory level
|
|
1370
1664
|
*/
|
|
@@ -1489,11 +1783,11 @@ var SQLiteEventStore = class {
|
|
|
1489
1783
|
);
|
|
1490
1784
|
}
|
|
1491
1785
|
/**
|
|
1492
|
-
* Get most accessed memories
|
|
1786
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
1493
1787
|
*/
|
|
1494
1788
|
async getMostAccessed(limit = 10) {
|
|
1495
1789
|
await this.initialize();
|
|
1496
|
-
|
|
1790
|
+
let rows = sqliteAll(
|
|
1497
1791
|
this.db,
|
|
1498
1792
|
`SELECT * FROM events
|
|
1499
1793
|
WHERE access_count > 0
|
|
@@ -1501,8 +1795,166 @@ var SQLiteEventStore = class {
|
|
|
1501
1795
|
LIMIT ?`,
|
|
1502
1796
|
[limit]
|
|
1503
1797
|
);
|
|
1798
|
+
if (rows.length === 0) {
|
|
1799
|
+
rows = sqliteAll(
|
|
1800
|
+
this.db,
|
|
1801
|
+
`SELECT * FROM events
|
|
1802
|
+
ORDER BY timestamp DESC
|
|
1803
|
+
LIMIT ?`,
|
|
1804
|
+
[limit]
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1504
1807
|
return rows.map((row) => this.rowToEvent(row));
|
|
1505
1808
|
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Record a memory retrieval for helpfulness tracking
|
|
1811
|
+
*/
|
|
1812
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
1813
|
+
if (this.readOnly)
|
|
1814
|
+
return;
|
|
1815
|
+
await this.initialize();
|
|
1816
|
+
const id = randomUUID2();
|
|
1817
|
+
sqliteRun(
|
|
1818
|
+
this.db,
|
|
1819
|
+
`INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
|
|
1820
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
1821
|
+
[id, eventId, sessionId, score, query.slice(0, 100)]
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Evaluate helpfulness for all retrievals in a session
|
|
1826
|
+
* Called at session end - uses behavioral signals to compute score
|
|
1827
|
+
*/
|
|
1828
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
1829
|
+
if (this.readOnly)
|
|
1830
|
+
return;
|
|
1831
|
+
await this.initialize();
|
|
1832
|
+
const retrievals = sqliteAll(
|
|
1833
|
+
this.db,
|
|
1834
|
+
`SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
|
|
1835
|
+
[sessionId]
|
|
1836
|
+
);
|
|
1837
|
+
if (retrievals.length === 0)
|
|
1838
|
+
return;
|
|
1839
|
+
const sessionEvents = sqliteAll(
|
|
1840
|
+
this.db,
|
|
1841
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1842
|
+
[sessionId]
|
|
1843
|
+
);
|
|
1844
|
+
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
1845
|
+
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
1846
|
+
let toolSuccessCount = 0;
|
|
1847
|
+
let toolTotalCount = toolEvents.length;
|
|
1848
|
+
for (const t of toolEvents) {
|
|
1849
|
+
try {
|
|
1850
|
+
const content = JSON.parse(t.content);
|
|
1851
|
+
if (content.success !== false)
|
|
1852
|
+
toolSuccessCount++;
|
|
1853
|
+
} catch {
|
|
1854
|
+
toolSuccessCount++;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
|
|
1858
|
+
for (const retrieval of retrievals) {
|
|
1859
|
+
const retrievalTime = retrieval.created_at;
|
|
1860
|
+
const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1861
|
+
const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
|
|
1862
|
+
const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1863
|
+
const promptCountAfter = promptsAfter.length;
|
|
1864
|
+
const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1865
|
+
let wasReasked = 0;
|
|
1866
|
+
for (const p of promptsAfter) {
|
|
1867
|
+
const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1868
|
+
let overlap = 0;
|
|
1869
|
+
for (const w of queryWords) {
|
|
1870
|
+
if (pWords.has(w))
|
|
1871
|
+
overlap++;
|
|
1872
|
+
}
|
|
1873
|
+
if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
|
|
1874
|
+
wasReasked = 1;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const retrievalScore = retrieval.retrieval_score || 0;
|
|
1879
|
+
const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
|
|
1880
|
+
sqliteRun(
|
|
1881
|
+
this.db,
|
|
1882
|
+
`UPDATE memory_helpfulness
|
|
1883
|
+
SET session_continued = ?, prompt_count_after = ?,
|
|
1884
|
+
tool_success_count = ?, tool_total_count = ?,
|
|
1885
|
+
was_reasked = ?, helpfulness_score = ?,
|
|
1886
|
+
measured_at = datetime('now')
|
|
1887
|
+
WHERE id = ?`,
|
|
1888
|
+
[
|
|
1889
|
+
sessionContinued,
|
|
1890
|
+
promptCountAfter,
|
|
1891
|
+
toolSuccessCount,
|
|
1892
|
+
toolTotalCount,
|
|
1893
|
+
wasReasked,
|
|
1894
|
+
helpfulnessScore,
|
|
1895
|
+
retrieval.id
|
|
1896
|
+
]
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Get most helpful memories ranked by helpfulness score
|
|
1902
|
+
*/
|
|
1903
|
+
async getHelpfulMemories(limit = 10) {
|
|
1904
|
+
await this.initialize();
|
|
1905
|
+
const rows = sqliteAll(
|
|
1906
|
+
this.db,
|
|
1907
|
+
`SELECT
|
|
1908
|
+
mh.event_id,
|
|
1909
|
+
AVG(mh.helpfulness_score) as avg_score,
|
|
1910
|
+
COUNT(*) as eval_count,
|
|
1911
|
+
e.content,
|
|
1912
|
+
e.access_count
|
|
1913
|
+
FROM memory_helpfulness mh
|
|
1914
|
+
JOIN events e ON e.id = mh.event_id
|
|
1915
|
+
WHERE mh.measured_at IS NOT NULL
|
|
1916
|
+
GROUP BY mh.event_id
|
|
1917
|
+
ORDER BY avg_score DESC
|
|
1918
|
+
LIMIT ?`,
|
|
1919
|
+
[limit]
|
|
1920
|
+
);
|
|
1921
|
+
return rows.map((r) => ({
|
|
1922
|
+
eventId: r.event_id,
|
|
1923
|
+
summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
|
|
1924
|
+
helpfulnessScore: Math.round(r.avg_score * 100) / 100,
|
|
1925
|
+
accessCount: r.access_count || 0,
|
|
1926
|
+
evaluationCount: r.eval_count
|
|
1927
|
+
}));
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Get helpfulness statistics for dashboard
|
|
1931
|
+
*/
|
|
1932
|
+
async getHelpfulnessStats() {
|
|
1933
|
+
await this.initialize();
|
|
1934
|
+
const stats = sqliteGet(
|
|
1935
|
+
this.db,
|
|
1936
|
+
`SELECT
|
|
1937
|
+
AVG(helpfulness_score) as avg_score,
|
|
1938
|
+
COUNT(*) as total_evaluated,
|
|
1939
|
+
SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
|
|
1940
|
+
SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
|
|
1941
|
+
SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
|
|
1942
|
+
FROM memory_helpfulness
|
|
1943
|
+
WHERE measured_at IS NOT NULL`
|
|
1944
|
+
);
|
|
1945
|
+
const totalRow = sqliteGet(
|
|
1946
|
+
this.db,
|
|
1947
|
+
`SELECT COUNT(*) as total FROM memory_helpfulness`
|
|
1948
|
+
);
|
|
1949
|
+
return {
|
|
1950
|
+
avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
|
|
1951
|
+
totalEvaluated: stats?.total_evaluated || 0,
|
|
1952
|
+
totalRetrievals: totalRow?.total || 0,
|
|
1953
|
+
helpful: stats?.helpful || 0,
|
|
1954
|
+
neutral: stats?.neutral || 0,
|
|
1955
|
+
unhelpful: stats?.unhelpful || 0
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1506
1958
|
/**
|
|
1507
1959
|
* Fast keyword search using FTS5
|
|
1508
1960
|
* Returns events matching the search query, ranked by relevance
|
|
@@ -1565,12 +2017,222 @@ var SQLiteEventStore = class {
|
|
|
1565
2017
|
getDatabase() {
|
|
1566
2018
|
return this.db;
|
|
1567
2019
|
}
|
|
2020
|
+
async recordRetrievalTrace(input) {
|
|
2021
|
+
await this.initialize();
|
|
2022
|
+
const traceId = randomUUID2();
|
|
2023
|
+
sqliteRun(
|
|
2024
|
+
this.db,
|
|
2025
|
+
`INSERT INTO retrieval_traces (
|
|
2026
|
+
trace_id, session_id, project_hash, query_text, strategy,
|
|
2027
|
+
candidate_event_ids, selected_event_ids, candidate_details_json, selected_details_json,
|
|
2028
|
+
candidate_count, selected_count, confidence, fallback_trace
|
|
2029
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2030
|
+
[
|
|
2031
|
+
traceId,
|
|
2032
|
+
input.sessionId || null,
|
|
2033
|
+
input.projectHash || null,
|
|
2034
|
+
input.queryText,
|
|
2035
|
+
input.strategy || null,
|
|
2036
|
+
JSON.stringify(input.candidateEventIds || []),
|
|
2037
|
+
JSON.stringify(input.selectedEventIds || []),
|
|
2038
|
+
JSON.stringify(input.candidateDetails || []),
|
|
2039
|
+
JSON.stringify(input.selectedDetails || []),
|
|
2040
|
+
(input.candidateEventIds || []).length,
|
|
2041
|
+
(input.selectedEventIds || []).length,
|
|
2042
|
+
input.confidence || null,
|
|
2043
|
+
JSON.stringify(input.fallbackTrace || [])
|
|
2044
|
+
]
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
async getRecentRetrievalTraces(limit = 50) {
|
|
2048
|
+
await this.initialize();
|
|
2049
|
+
const rows = sqliteAll(
|
|
2050
|
+
this.db,
|
|
2051
|
+
`SELECT * FROM retrieval_traces ORDER BY created_at DESC LIMIT ?`,
|
|
2052
|
+
[limit]
|
|
2053
|
+
);
|
|
2054
|
+
return rows.map((row) => ({
|
|
2055
|
+
traceId: row.trace_id,
|
|
2056
|
+
sessionId: row.session_id || void 0,
|
|
2057
|
+
projectHash: row.project_hash || void 0,
|
|
2058
|
+
queryText: row.query_text,
|
|
2059
|
+
strategy: row.strategy || void 0,
|
|
2060
|
+
candidateEventIds: row.candidate_event_ids ? JSON.parse(row.candidate_event_ids) : [],
|
|
2061
|
+
selectedEventIds: row.selected_event_ids ? JSON.parse(row.selected_event_ids) : [],
|
|
2062
|
+
candidateDetails: row.candidate_details_json ? JSON.parse(row.candidate_details_json) : [],
|
|
2063
|
+
selectedDetails: row.selected_details_json ? JSON.parse(row.selected_details_json) : [],
|
|
2064
|
+
candidateCount: Number(row.candidate_count || 0),
|
|
2065
|
+
selectedCount: Number(row.selected_count || 0),
|
|
2066
|
+
confidence: row.confidence || void 0,
|
|
2067
|
+
fallbackTrace: row.fallback_trace ? JSON.parse(row.fallback_trace) : [],
|
|
2068
|
+
createdAt: toDateFromSQLite(row.created_at)
|
|
2069
|
+
}));
|
|
2070
|
+
}
|
|
2071
|
+
async getRetrievalTraceStats() {
|
|
2072
|
+
await this.initialize();
|
|
2073
|
+
const row = sqliteGet(
|
|
2074
|
+
this.db,
|
|
2075
|
+
`SELECT
|
|
2076
|
+
COUNT(*) as total_queries,
|
|
2077
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
2078
|
+
AVG(selected_count) as avg_selected_count,
|
|
2079
|
+
CASE
|
|
2080
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
2081
|
+
ELSE 0
|
|
2082
|
+
END as selection_rate
|
|
2083
|
+
FROM retrieval_traces`,
|
|
2084
|
+
[]
|
|
2085
|
+
);
|
|
2086
|
+
return {
|
|
2087
|
+
totalQueries: Number(row?.total_queries || 0),
|
|
2088
|
+
avgCandidateCount: Number(row?.avg_candidate_count || 0),
|
|
2089
|
+
avgSelectedCount: Number(row?.avg_selected_count || 0),
|
|
2090
|
+
selectionRate: Number(row?.selection_rate || 0)
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
1568
2093
|
/**
|
|
1569
2094
|
* Close database connection
|
|
1570
2095
|
*/
|
|
1571
2096
|
async close() {
|
|
1572
2097
|
sqliteClose(this.db);
|
|
1573
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Get events grouped by turn_id for a session
|
|
2101
|
+
* Returns turns ordered by first event timestamp (newest first)
|
|
2102
|
+
*/
|
|
2103
|
+
async getSessionTurns(sessionId, options) {
|
|
2104
|
+
await this.initialize();
|
|
2105
|
+
const limit = options?.limit || 20;
|
|
2106
|
+
const offset = options?.offset || 0;
|
|
2107
|
+
const turnRows = sqliteAll(
|
|
2108
|
+
this.db,
|
|
2109
|
+
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
2110
|
+
FROM events
|
|
2111
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
2112
|
+
GROUP BY turn_id
|
|
2113
|
+
ORDER BY min_ts DESC
|
|
2114
|
+
LIMIT ? OFFSET ?`,
|
|
2115
|
+
[sessionId, limit, offset]
|
|
2116
|
+
);
|
|
2117
|
+
const turns = [];
|
|
2118
|
+
for (const turnRow of turnRows) {
|
|
2119
|
+
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
2120
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
2121
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
2122
|
+
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
2123
|
+
turns.push({
|
|
2124
|
+
turnId: turnRow.turn_id,
|
|
2125
|
+
events,
|
|
2126
|
+
startedAt: toDateFromSQLite(turnRow.min_ts),
|
|
2127
|
+
promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
|
|
2128
|
+
eventCount: events.length,
|
|
2129
|
+
toolCount: toolEvents.length,
|
|
2130
|
+
hasResponse
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
return turns;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Get all events for a specific turn_id
|
|
2137
|
+
*/
|
|
2138
|
+
async getEventsByTurn(turnId) {
|
|
2139
|
+
await this.initialize();
|
|
2140
|
+
const rows = sqliteAll(
|
|
2141
|
+
this.db,
|
|
2142
|
+
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
2143
|
+
[turnId]
|
|
2144
|
+
);
|
|
2145
|
+
return rows.map(this.rowToEvent);
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Count total turns for a session
|
|
2149
|
+
*/
|
|
2150
|
+
async countSessionTurns(sessionId) {
|
|
2151
|
+
await this.initialize();
|
|
2152
|
+
const row = sqliteGet(
|
|
2153
|
+
this.db,
|
|
2154
|
+
`SELECT COUNT(DISTINCT turn_id) as count
|
|
2155
|
+
FROM events
|
|
2156
|
+
WHERE session_id = ? AND turn_id IS NOT NULL`,
|
|
2157
|
+
[sessionId]
|
|
2158
|
+
);
|
|
2159
|
+
return row?.count || 0;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Migrate existing events: backfill turn_id for events that have turnId in metadata
|
|
2163
|
+
* but no turn_id column value (for events stored before this migration)
|
|
2164
|
+
*/
|
|
2165
|
+
async backfillTurnIds() {
|
|
2166
|
+
await this.initialize();
|
|
2167
|
+
const rows = sqliteAll(
|
|
2168
|
+
this.db,
|
|
2169
|
+
`SELECT id, metadata FROM events
|
|
2170
|
+
WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
|
|
2171
|
+
);
|
|
2172
|
+
let updated = 0;
|
|
2173
|
+
for (const row of rows) {
|
|
2174
|
+
try {
|
|
2175
|
+
const metadata = JSON.parse(row.metadata);
|
|
2176
|
+
if (metadata.turnId) {
|
|
2177
|
+
sqliteRun(
|
|
2178
|
+
this.db,
|
|
2179
|
+
`UPDATE events SET turn_id = ? WHERE id = ?`,
|
|
2180
|
+
[metadata.turnId, row.id]
|
|
2181
|
+
);
|
|
2182
|
+
updated++;
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return updated;
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Delete all events for a session (for force reimport)
|
|
2191
|
+
*/
|
|
2192
|
+
async deleteSessionEvents(sessionId) {
|
|
2193
|
+
await this.initialize();
|
|
2194
|
+
const events = sqliteAll(
|
|
2195
|
+
this.db,
|
|
2196
|
+
`SELECT id FROM events WHERE session_id = ?`,
|
|
2197
|
+
[sessionId]
|
|
2198
|
+
);
|
|
2199
|
+
if (events.length === 0)
|
|
2200
|
+
return 0;
|
|
2201
|
+
const eventIds = events.map((e) => e.id);
|
|
2202
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
2203
|
+
const ftsTriggersDropped = [];
|
|
2204
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
2205
|
+
try {
|
|
2206
|
+
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
2207
|
+
ftsTriggersDropped.push(triggerName);
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
2212
|
+
try {
|
|
2213
|
+
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
2214
|
+
} catch {
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
2218
|
+
if (ftsTriggersDropped.length > 0) {
|
|
2219
|
+
try {
|
|
2220
|
+
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
2221
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
2222
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
2223
|
+
END`);
|
|
2224
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
2225
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
2226
|
+
END`);
|
|
2227
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
2228
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
2229
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
2230
|
+
END`);
|
|
2231
|
+
} catch {
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return result.changes || 0;
|
|
2235
|
+
}
|
|
1574
2236
|
/**
|
|
1575
2237
|
* Convert database row to MemoryEvent
|
|
1576
2238
|
*/
|
|
@@ -1591,6 +2253,9 @@ var SQLiteEventStore = class {
|
|
|
1591
2253
|
if (row.last_accessed_at !== void 0) {
|
|
1592
2254
|
event.last_accessed_at = row.last_accessed_at;
|
|
1593
2255
|
}
|
|
2256
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
2257
|
+
event.turn_id = row.turn_id;
|
|
2258
|
+
}
|
|
1594
2259
|
return event;
|
|
1595
2260
|
}
|
|
1596
2261
|
};
|
|
@@ -1742,7 +2407,7 @@ var SyncWorker = class {
|
|
|
1742
2407
|
* Sleep utility
|
|
1743
2408
|
*/
|
|
1744
2409
|
sleep(ms) {
|
|
1745
|
-
return new Promise((
|
|
2410
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1746
2411
|
}
|
|
1747
2412
|
/**
|
|
1748
2413
|
* Get sync statistics
|
|
@@ -1802,8 +2467,17 @@ var VectorStore = class {
|
|
|
1802
2467
|
metadata: JSON.stringify(record.metadata || {})
|
|
1803
2468
|
};
|
|
1804
2469
|
if (!this.table) {
|
|
1805
|
-
|
|
1806
|
-
|
|
2470
|
+
try {
|
|
2471
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
2472
|
+
} catch (e) {
|
|
2473
|
+
if (e?.message?.includes("already exists")) {
|
|
2474
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2475
|
+
await this.table.add([data]);
|
|
2476
|
+
} else {
|
|
2477
|
+
throw e;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
} else {
|
|
1807
2481
|
await this.table.add([data]);
|
|
1808
2482
|
}
|
|
1809
2483
|
}
|
|
@@ -1828,7 +2502,16 @@ var VectorStore = class {
|
|
|
1828
2502
|
metadata: JSON.stringify(record.metadata || {})
|
|
1829
2503
|
}));
|
|
1830
2504
|
if (!this.table) {
|
|
1831
|
-
|
|
2505
|
+
try {
|
|
2506
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
2507
|
+
} catch (e) {
|
|
2508
|
+
if (e?.message?.includes("already exists")) {
|
|
2509
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2510
|
+
await this.table.add(data);
|
|
2511
|
+
} else {
|
|
2512
|
+
throw e;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
1832
2515
|
} else {
|
|
1833
2516
|
await this.table.add(data);
|
|
1834
2517
|
}
|
|
@@ -2268,7 +2951,20 @@ var DEFAULT_OPTIONS = {
|
|
|
2268
2951
|
topK: 5,
|
|
2269
2952
|
minScore: 0.7,
|
|
2270
2953
|
maxTokens: 2e3,
|
|
2271
|
-
includeSessionContext: true
|
|
2954
|
+
includeSessionContext: true,
|
|
2955
|
+
strategy: "auto",
|
|
2956
|
+
rerankWithKeyword: true,
|
|
2957
|
+
decayPolicy: {
|
|
2958
|
+
enabled: true,
|
|
2959
|
+
windowDays: 30,
|
|
2960
|
+
maxPenalty: 0.15
|
|
2961
|
+
},
|
|
2962
|
+
graphHop: {
|
|
2963
|
+
enabled: true,
|
|
2964
|
+
maxHops: 1,
|
|
2965
|
+
hopPenalty: 0.08
|
|
2966
|
+
},
|
|
2967
|
+
projectScopeMode: "global"
|
|
2272
2968
|
};
|
|
2273
2969
|
var Retriever = class {
|
|
2274
2970
|
eventStore;
|
|
@@ -2278,6 +2974,7 @@ var Retriever = class {
|
|
|
2278
2974
|
sharedStore;
|
|
2279
2975
|
sharedVectorStore;
|
|
2280
2976
|
graduation;
|
|
2977
|
+
queryRewriter;
|
|
2281
2978
|
constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
|
|
2282
2979
|
this.eventStore = eventStore;
|
|
2283
2980
|
this.vectorStore = vectorStore;
|
|
@@ -2286,47 +2983,105 @@ var Retriever = class {
|
|
|
2286
2983
|
this.sharedStore = sharedOptions?.sharedStore;
|
|
2287
2984
|
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
2288
2985
|
}
|
|
2289
|
-
/**
|
|
2290
|
-
* Set graduation pipeline for access tracking
|
|
2291
|
-
*/
|
|
2292
2986
|
setGraduationPipeline(graduation) {
|
|
2293
2987
|
this.graduation = graduation;
|
|
2294
2988
|
}
|
|
2295
|
-
/**
|
|
2296
|
-
* Set shared stores after construction
|
|
2297
|
-
*/
|
|
2298
2989
|
setSharedStores(sharedStore, sharedVectorStore) {
|
|
2299
2990
|
this.sharedStore = sharedStore;
|
|
2300
2991
|
this.sharedVectorStore = sharedVectorStore;
|
|
2301
2992
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2993
|
+
setQueryRewriter(rewriter) {
|
|
2994
|
+
this.queryRewriter = rewriter;
|
|
2995
|
+
}
|
|
2305
2996
|
async retrieve(query, options = {}) {
|
|
2306
2997
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
2307
|
-
const
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
|
|
2998
|
+
const sessionFilter = opts.scope?.sessionId ?? opts.sessionId;
|
|
2999
|
+
const fallbackTrace = [];
|
|
3000
|
+
const fallbackEnabled = (opts.strategy ?? "auto") === "auto";
|
|
3001
|
+
const primaryStrategy = opts.strategy === "auto" ? "fast" : opts.strategy || "fast";
|
|
3002
|
+
let current = await this.runStage(query, {
|
|
3003
|
+
strategy: primaryStrategy,
|
|
3004
|
+
topK: opts.topK,
|
|
2311
3005
|
minScore: opts.minScore,
|
|
2312
|
-
sessionId:
|
|
3006
|
+
sessionId: sessionFilter,
|
|
3007
|
+
scope: opts.scope,
|
|
3008
|
+
rerankWithKeyword: opts.rerankWithKeyword !== false,
|
|
3009
|
+
rerankWeights: opts.rerankWeights,
|
|
3010
|
+
decayPolicy: opts.decayPolicy,
|
|
3011
|
+
intentRewrite: opts.intentRewrite === true,
|
|
3012
|
+
graphHop: opts.graphHop,
|
|
3013
|
+
projectScopeMode: opts.projectScopeMode,
|
|
3014
|
+
projectHash: opts.projectHash,
|
|
3015
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
2313
3016
|
});
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
3017
|
+
fallbackTrace.push(`stage:primary:${primaryStrategy}`);
|
|
3018
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results) && primaryStrategy !== "deep") {
|
|
3019
|
+
current = await this.runStage(query, {
|
|
3020
|
+
strategy: "deep",
|
|
3021
|
+
topK: opts.topK,
|
|
3022
|
+
minScore: opts.minScore,
|
|
3023
|
+
sessionId: sessionFilter,
|
|
3024
|
+
scope: opts.scope,
|
|
3025
|
+
rerankWithKeyword: opts.rerankWithKeyword !== false,
|
|
3026
|
+
rerankWeights: opts.rerankWeights,
|
|
3027
|
+
decayPolicy: opts.decayPolicy,
|
|
3028
|
+
graphHop: opts.graphHop,
|
|
3029
|
+
projectScopeMode: opts.projectScopeMode,
|
|
3030
|
+
projectHash: opts.projectHash,
|
|
3031
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
3032
|
+
});
|
|
3033
|
+
fallbackTrace.push("fallback:deep");
|
|
3034
|
+
}
|
|
3035
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
|
|
3036
|
+
current = await this.runStage(query, {
|
|
3037
|
+
strategy: "deep",
|
|
3038
|
+
topK: opts.topK,
|
|
3039
|
+
minScore: Math.max(0.5, opts.minScore - 0.15),
|
|
3040
|
+
sessionId: void 0,
|
|
3041
|
+
scope: void 0,
|
|
3042
|
+
rerankWithKeyword: true,
|
|
3043
|
+
rerankWeights: opts.rerankWeights,
|
|
3044
|
+
decayPolicy: opts.decayPolicy,
|
|
3045
|
+
graphHop: opts.graphHop,
|
|
3046
|
+
projectScopeMode: opts.projectScopeMode,
|
|
3047
|
+
projectHash: opts.projectHash,
|
|
3048
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
3049
|
+
});
|
|
3050
|
+
fallbackTrace.push("fallback:scope-expanded");
|
|
3051
|
+
}
|
|
3052
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
|
|
3053
|
+
const summary = await this.buildSummaryFallback(query, opts.topK);
|
|
3054
|
+
current = {
|
|
3055
|
+
results: summary,
|
|
3056
|
+
candidateResults: summary,
|
|
3057
|
+
matchResult: this.matcher.matchSearchResults(summary, () => 0)
|
|
3058
|
+
};
|
|
3059
|
+
fallbackTrace.push("fallback:summary");
|
|
3060
|
+
}
|
|
3061
|
+
const memories = await this.enrichResults(current.results.slice(0, opts.topK), opts);
|
|
2319
3062
|
const context = this.buildContext(memories, opts.maxTokens);
|
|
2320
3063
|
return {
|
|
2321
3064
|
memories,
|
|
2322
|
-
matchResult,
|
|
3065
|
+
matchResult: current.matchResult,
|
|
2323
3066
|
totalTokens: this.estimateTokens(context),
|
|
2324
|
-
context
|
|
3067
|
+
context,
|
|
3068
|
+
fallbackTrace,
|
|
3069
|
+
selectedDebug: current.results.slice(0, opts.topK).map((r) => ({
|
|
3070
|
+
eventId: r.eventId,
|
|
3071
|
+
score: r.score,
|
|
3072
|
+
semanticScore: r.semanticScore,
|
|
3073
|
+
lexicalScore: r.lexicalScore,
|
|
3074
|
+
recencyScore: r.recencyScore
|
|
3075
|
+
})),
|
|
3076
|
+
candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => ({
|
|
3077
|
+
eventId: r.eventId,
|
|
3078
|
+
score: r.score,
|
|
3079
|
+
semanticScore: r.semanticScore,
|
|
3080
|
+
lexicalScore: r.lexicalScore,
|
|
3081
|
+
recencyScore: r.recencyScore
|
|
3082
|
+
}))
|
|
2325
3083
|
};
|
|
2326
3084
|
}
|
|
2327
|
-
/**
|
|
2328
|
-
* Retrieve with unified search (project + shared)
|
|
2329
|
-
*/
|
|
2330
3085
|
async retrieveUnified(query, options = {}) {
|
|
2331
3086
|
const projectResult = await this.retrieve(query, options);
|
|
2332
3087
|
if (!options.includeShared || !this.sharedStore || !this.sharedVectorStore) {
|
|
@@ -2334,22 +3089,19 @@ var Retriever = class {
|
|
|
2334
3089
|
}
|
|
2335
3090
|
try {
|
|
2336
3091
|
const queryEmbedding = await this.embedder.embed(query);
|
|
2337
|
-
const sharedVectorResults = await this.sharedVectorStore.search(
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
excludeProjectHash: options.projectHash
|
|
2343
|
-
}
|
|
2344
|
-
);
|
|
3092
|
+
const sharedVectorResults = await this.sharedVectorStore.search(queryEmbedding.vector, {
|
|
3093
|
+
limit: options.topK || 5,
|
|
3094
|
+
minScore: options.minScore || 0.7,
|
|
3095
|
+
excludeProjectHash: options.projectHash
|
|
3096
|
+
});
|
|
2345
3097
|
const sharedMemories = [];
|
|
2346
3098
|
for (const result of sharedVectorResults) {
|
|
2347
3099
|
const entry = await this.sharedStore.get(result.entryId);
|
|
2348
|
-
if (entry)
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
3100
|
+
if (!entry)
|
|
3101
|
+
continue;
|
|
3102
|
+
if (!options.projectHash || entry.sourceProjectHash !== options.projectHash) {
|
|
3103
|
+
sharedMemories.push(entry);
|
|
3104
|
+
await this.sharedStore.recordUsage(entry.entryId);
|
|
2353
3105
|
}
|
|
2354
3106
|
}
|
|
2355
3107
|
const unifiedContext = this.buildUnifiedContext(projectResult, sharedMemories);
|
|
@@ -2364,50 +3116,243 @@ var Retriever = class {
|
|
|
2364
3116
|
return projectResult;
|
|
2365
3117
|
}
|
|
2366
3118
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
3119
|
+
async runStage(query, input) {
|
|
3120
|
+
let initialResults = await this.searchByStrategy(query, {
|
|
3121
|
+
strategy: input.strategy,
|
|
3122
|
+
topK: input.topK,
|
|
3123
|
+
minScore: input.minScore,
|
|
3124
|
+
sessionId: input.sessionId
|
|
3125
|
+
});
|
|
3126
|
+
if (input.intentRewrite && input.strategy === "deep" && this.queryRewriter) {
|
|
3127
|
+
const rewritten = (await this.queryRewriter(query))?.trim();
|
|
3128
|
+
if (rewritten && rewritten !== query) {
|
|
3129
|
+
const rewrittenResults = await this.searchByStrategy(rewritten, {
|
|
3130
|
+
strategy: "deep",
|
|
3131
|
+
topK: input.topK,
|
|
3132
|
+
minScore: Math.max(0.5, input.minScore - 0.1),
|
|
3133
|
+
sessionId: input.sessionId
|
|
3134
|
+
});
|
|
3135
|
+
initialResults = this.mergeResults(initialResults, rewrittenResults, input.topK * 3);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
const expandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
|
|
3139
|
+
maxHops: Math.max(1, input.graphHop?.maxHops ?? 1),
|
|
3140
|
+
hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
|
|
3141
|
+
limit: input.topK * 4
|
|
3142
|
+
});
|
|
3143
|
+
const rerankedResults = input.rerankWithKeyword ? this.rerankByKeywordOverlap(expandedResults, query, input.rerankWeights, input.decayPolicy) : expandedResults;
|
|
3144
|
+
const filtered = await this.applyScopeFilters(rerankedResults, {
|
|
3145
|
+
scope: input.scope,
|
|
3146
|
+
projectScopeMode: input.projectScopeMode,
|
|
3147
|
+
projectHash: input.projectHash,
|
|
3148
|
+
allowedProjectHashes: input.allowedProjectHashes
|
|
3149
|
+
});
|
|
3150
|
+
const top = filtered.slice(0, input.topK);
|
|
3151
|
+
const matchResult = this.matcher.matchSearchResults(top, () => 0);
|
|
3152
|
+
return { results: top, candidateResults: filtered, matchResult };
|
|
3153
|
+
}
|
|
3154
|
+
mergeResults(primary, secondary, limit) {
|
|
3155
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3156
|
+
for (const row of primary)
|
|
3157
|
+
byId.set(row.eventId, row);
|
|
3158
|
+
for (const row of secondary) {
|
|
3159
|
+
const prev = byId.get(row.eventId);
|
|
3160
|
+
if (!prev || row.score > prev.score) {
|
|
3161
|
+
byId.set(row.eventId, row);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
3165
|
+
}
|
|
3166
|
+
async expandGraphHops(seeds, opts) {
|
|
3167
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3168
|
+
for (const s of seeds)
|
|
3169
|
+
byId.set(s.eventId, s);
|
|
3170
|
+
let frontier = seeds.map((s) => ({ row: s, hop: 0 }));
|
|
3171
|
+
for (let hop = 1; hop <= opts.maxHops; hop += 1) {
|
|
3172
|
+
const next = [];
|
|
3173
|
+
for (const f of frontier) {
|
|
3174
|
+
const ev = await this.eventStore.getEvent(f.row.eventId);
|
|
3175
|
+
if (!ev)
|
|
3176
|
+
continue;
|
|
3177
|
+
const rel = ev.metadata?.relatedEventIds ?? [];
|
|
3178
|
+
const relatedIds = Array.isArray(rel) ? rel.filter((x) => typeof x === "string") : [];
|
|
3179
|
+
for (const rid of relatedIds) {
|
|
3180
|
+
if (byId.has(rid))
|
|
3181
|
+
continue;
|
|
3182
|
+
const target = await this.eventStore.getEvent(rid);
|
|
3183
|
+
if (!target)
|
|
3184
|
+
continue;
|
|
3185
|
+
const score = Math.max(0, f.row.score - opts.hopPenalty * hop);
|
|
3186
|
+
const row = {
|
|
3187
|
+
id: `hop-${hop}-${rid}`,
|
|
3188
|
+
eventId: target.id,
|
|
3189
|
+
content: target.content,
|
|
3190
|
+
score,
|
|
3191
|
+
sessionId: target.sessionId,
|
|
3192
|
+
eventType: target.eventType,
|
|
3193
|
+
timestamp: target.timestamp.toISOString()
|
|
3194
|
+
};
|
|
3195
|
+
byId.set(row.eventId, row);
|
|
3196
|
+
next.push({ row, hop });
|
|
3197
|
+
if (byId.size >= opts.limit)
|
|
3198
|
+
break;
|
|
2388
3199
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
`;
|
|
3200
|
+
if (byId.size >= opts.limit)
|
|
3201
|
+
break;
|
|
2392
3202
|
}
|
|
3203
|
+
frontier = next;
|
|
3204
|
+
if (frontier.length === 0 || byId.size >= opts.limit)
|
|
3205
|
+
break;
|
|
2393
3206
|
}
|
|
2394
|
-
return
|
|
3207
|
+
return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, opts.limit);
|
|
3208
|
+
}
|
|
3209
|
+
shouldFallback(matchResult, results) {
|
|
3210
|
+
if (results.length === 0)
|
|
3211
|
+
return true;
|
|
3212
|
+
if (matchResult.confidence === "none")
|
|
3213
|
+
return true;
|
|
3214
|
+
return false;
|
|
3215
|
+
}
|
|
3216
|
+
async buildSummaryFallback(query, topK) {
|
|
3217
|
+
const recent = await this.eventStore.getRecentEvents(Math.max(topK * 6, 20));
|
|
3218
|
+
const q = this.tokenize(query);
|
|
3219
|
+
const ranked = recent.map((e) => ({ e, overlap: this.keywordOverlap(q, this.tokenize(e.content)) })).filter((r) => r.overlap > 0).sort((a, b) => b.overlap - a.overlap).slice(0, topK).map((row, idx) => ({
|
|
3220
|
+
id: `summary-${row.e.id}`,
|
|
3221
|
+
eventId: row.e.id,
|
|
3222
|
+
content: row.e.content,
|
|
3223
|
+
score: Math.max(0.25, 0.6 - idx * 0.05),
|
|
3224
|
+
sessionId: row.e.sessionId,
|
|
3225
|
+
eventType: row.e.eventType,
|
|
3226
|
+
timestamp: row.e.timestamp.toISOString()
|
|
3227
|
+
}));
|
|
3228
|
+
return ranked;
|
|
3229
|
+
}
|
|
3230
|
+
async searchByStrategy(query, input) {
|
|
3231
|
+
const strategy = input.strategy === "auto" ? "deep" : input.strategy;
|
|
3232
|
+
if (strategy === "fast") {
|
|
3233
|
+
const keyword = await this.searchByKeyword(query, {
|
|
3234
|
+
limit: Math.max(5, input.topK * 3),
|
|
3235
|
+
sessionId: input.sessionId
|
|
3236
|
+
});
|
|
3237
|
+
return keyword;
|
|
3238
|
+
}
|
|
3239
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
3240
|
+
return this.vectorStore.search(queryEmbedding.vector, {
|
|
3241
|
+
limit: Math.max(5, input.topK * 3),
|
|
3242
|
+
minScore: input.minScore,
|
|
3243
|
+
sessionId: input.sessionId
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
async searchByKeyword(query, input) {
|
|
3247
|
+
if (this.eventStore.keywordSearch) {
|
|
3248
|
+
const rows = await this.eventStore.keywordSearch(query, input.limit);
|
|
3249
|
+
const filtered2 = input.sessionId ? rows.filter((r) => r.event.sessionId === input.sessionId) : rows;
|
|
3250
|
+
return filtered2.map((row, idx) => ({
|
|
3251
|
+
id: `kw-${row.event.id}`,
|
|
3252
|
+
eventId: row.event.id,
|
|
3253
|
+
content: row.event.content,
|
|
3254
|
+
score: Math.max(0.4, 1 - idx * 0.04),
|
|
3255
|
+
sessionId: row.event.sessionId,
|
|
3256
|
+
eventType: row.event.eventType,
|
|
3257
|
+
timestamp: row.event.timestamp.toISOString()
|
|
3258
|
+
}));
|
|
3259
|
+
}
|
|
3260
|
+
const recent = await this.eventStore.getRecentEvents(input.limit * 4);
|
|
3261
|
+
const tokens = this.tokenize(query);
|
|
3262
|
+
const filtered = recent.filter((e) => input.sessionId ? e.sessionId === input.sessionId : true).map((e) => ({ e, overlap: this.keywordOverlap(tokens, this.tokenize(e.content)) })).filter((r) => r.overlap > 0).sort((a, b) => b.overlap - a.overlap).slice(0, input.limit);
|
|
3263
|
+
return filtered.map((row, idx) => ({
|
|
3264
|
+
id: `kw-fallback-${row.e.id}`,
|
|
3265
|
+
eventId: row.e.id,
|
|
3266
|
+
content: row.e.content,
|
|
3267
|
+
score: Math.max(0.3, 0.9 - idx * 0.05),
|
|
3268
|
+
sessionId: row.e.sessionId,
|
|
3269
|
+
eventType: row.e.eventType,
|
|
3270
|
+
timestamp: row.e.timestamp.toISOString()
|
|
3271
|
+
}));
|
|
3272
|
+
}
|
|
3273
|
+
rerankByKeywordOverlap(results, query, weights, decayPolicy) {
|
|
3274
|
+
const q = this.tokenize(query);
|
|
3275
|
+
const now = Date.now();
|
|
3276
|
+
const sw = Math.max(0, weights?.semantic ?? 0.7);
|
|
3277
|
+
const lw = Math.max(0, weights?.lexical ?? 0.2);
|
|
3278
|
+
const rw = Math.max(0, weights?.recency ?? 0.1);
|
|
3279
|
+
const total = sw + lw + rw || 1;
|
|
3280
|
+
const decayEnabled = decayPolicy?.enabled !== false;
|
|
3281
|
+
const decayWindow = Math.max(1, decayPolicy?.windowDays ?? 30);
|
|
3282
|
+
const decayMaxPenalty = Math.max(0, decayPolicy?.maxPenalty ?? 0.15);
|
|
3283
|
+
return [...results].map((r) => {
|
|
3284
|
+
const overlap = this.keywordOverlap(q, this.tokenize(r.content));
|
|
3285
|
+
const recencyDays = Math.max(0, (now - new Date(r.timestamp).getTime()) / (1e3 * 60 * 60 * 24));
|
|
3286
|
+
const recency = Math.max(0, 1 - recencyDays / decayWindow);
|
|
3287
|
+
let blended = (r.score * sw + overlap * lw + recency * rw) / total;
|
|
3288
|
+
if (decayEnabled && recencyDays > decayWindow && overlap < 0.5) {
|
|
3289
|
+
const ageFactor = Math.min(1, (recencyDays - decayWindow) / decayWindow);
|
|
3290
|
+
blended -= decayMaxPenalty * ageFactor;
|
|
3291
|
+
}
|
|
3292
|
+
return { ...r, score: Math.max(0, blended), semanticScore: r.score, lexicalScore: overlap, recencyScore: recency };
|
|
3293
|
+
}).sort((a, b) => b.score - a.score);
|
|
3294
|
+
}
|
|
3295
|
+
async applyScopeFilters(results, options) {
|
|
3296
|
+
const scope = options?.scope;
|
|
3297
|
+
const projectScopeMode = options?.projectScopeMode ?? "global";
|
|
3298
|
+
const allowedProjectHashes = new Set(
|
|
3299
|
+
[options?.projectHash, ...options?.allowedProjectHashes || []].filter(
|
|
3300
|
+
(value) => typeof value === "string" && value.length > 0
|
|
3301
|
+
)
|
|
3302
|
+
);
|
|
3303
|
+
if (!scope && projectScopeMode === "global")
|
|
3304
|
+
return results;
|
|
3305
|
+
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
3306
|
+
const filtered = [];
|
|
3307
|
+
for (const result of results) {
|
|
3308
|
+
if (scope?.sessionId && result.sessionId !== scope.sessionId)
|
|
3309
|
+
continue;
|
|
3310
|
+
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix))
|
|
3311
|
+
continue;
|
|
3312
|
+
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType))
|
|
3313
|
+
continue;
|
|
3314
|
+
const event = await this.eventStore.getEvent(result.eventId);
|
|
3315
|
+
if (!event)
|
|
3316
|
+
continue;
|
|
3317
|
+
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix))
|
|
3318
|
+
continue;
|
|
3319
|
+
if (normalizedIncludes.length > 0) {
|
|
3320
|
+
const lc = event.content.toLowerCase();
|
|
3321
|
+
if (!normalizedIncludes.some((needle) => lc.includes(needle)))
|
|
3322
|
+
continue;
|
|
3323
|
+
}
|
|
3324
|
+
if (scope?.metadata && !this.matchesMetadataScope(event.metadata, scope.metadata))
|
|
3325
|
+
continue;
|
|
3326
|
+
const projectHash = this.extractProjectHash(event.metadata);
|
|
3327
|
+
filtered.push({ result, projectHash });
|
|
3328
|
+
}
|
|
3329
|
+
if (projectScopeMode === "global" || allowedProjectHashes.size === 0) {
|
|
3330
|
+
return filtered.map((x) => x.result);
|
|
3331
|
+
}
|
|
3332
|
+
const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
|
|
3333
|
+
if (projectScopeMode === "strict") {
|
|
3334
|
+
return projectMatched.map((x) => x.result);
|
|
3335
|
+
}
|
|
3336
|
+
return (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
|
|
3337
|
+
}
|
|
3338
|
+
extractProjectHash(metadata) {
|
|
3339
|
+
if (!metadata || typeof metadata !== "object")
|
|
3340
|
+
return void 0;
|
|
3341
|
+
const scope = metadata.scope;
|
|
3342
|
+
if (!scope || typeof scope !== "object")
|
|
3343
|
+
return void 0;
|
|
3344
|
+
const project = scope.project;
|
|
3345
|
+
if (!project || typeof project !== "object")
|
|
3346
|
+
return void 0;
|
|
3347
|
+
const hash = project.hash;
|
|
3348
|
+
return typeof hash === "string" && hash.length > 0 ? hash : void 0;
|
|
2395
3349
|
}
|
|
2396
|
-
/**
|
|
2397
|
-
* Retrieve memories from a specific session
|
|
2398
|
-
*/
|
|
2399
3350
|
async retrieveFromSession(sessionId) {
|
|
2400
3351
|
return this.eventStore.getSessionEvents(sessionId);
|
|
2401
3352
|
}
|
|
2402
|
-
/**
|
|
2403
|
-
* Get recent memories across all sessions
|
|
2404
|
-
*/
|
|
2405
3353
|
async retrieveRecent(limit = 100) {
|
|
2406
3354
|
return this.eventStore.getRecentEvents(limit);
|
|
2407
3355
|
}
|
|
2408
|
-
/**
|
|
2409
|
-
* Enrich search results with full event data
|
|
2410
|
-
*/
|
|
2411
3356
|
async enrichResults(results, options) {
|
|
2412
3357
|
const memories = [];
|
|
2413
3358
|
for (const result of results) {
|
|
@@ -2415,27 +3360,16 @@ var Retriever = class {
|
|
|
2415
3360
|
if (!event)
|
|
2416
3361
|
continue;
|
|
2417
3362
|
if (this.graduation) {
|
|
2418
|
-
this.graduation.recordAccess(
|
|
2419
|
-
event.id,
|
|
2420
|
-
options.sessionId || "unknown",
|
|
2421
|
-
result.score
|
|
2422
|
-
);
|
|
3363
|
+
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
2423
3364
|
}
|
|
2424
3365
|
let sessionContext;
|
|
2425
3366
|
if (options.includeSessionContext) {
|
|
2426
3367
|
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
2427
3368
|
}
|
|
2428
|
-
memories.push({
|
|
2429
|
-
event,
|
|
2430
|
-
score: result.score,
|
|
2431
|
-
sessionContext
|
|
2432
|
-
});
|
|
3369
|
+
memories.push({ event, score: result.score, sessionContext });
|
|
2433
3370
|
}
|
|
2434
3371
|
return memories;
|
|
2435
3372
|
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Get surrounding context from the same session
|
|
2438
|
-
*/
|
|
2439
3373
|
async getSessionContext(sessionId, eventId) {
|
|
2440
3374
|
const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
|
|
2441
3375
|
const eventIndex = sessionEvents.findIndex((e) => e.id === eventId);
|
|
@@ -2448,55 +3382,86 @@ var Retriever = class {
|
|
|
2448
3382
|
return void 0;
|
|
2449
3383
|
return contextEvents.filter((e) => e.id !== eventId).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`).join("\n");
|
|
2450
3384
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
3385
|
+
buildUnifiedContext(projectResult, sharedMemories) {
|
|
3386
|
+
let context = projectResult.context;
|
|
3387
|
+
if (sharedMemories.length === 0)
|
|
3388
|
+
return context;
|
|
3389
|
+
context += "\n\n## Cross-Project Knowledge\n\n";
|
|
3390
|
+
for (const memory of sharedMemories.slice(0, 3)) {
|
|
3391
|
+
context += `### ${memory.title}
|
|
3392
|
+
`;
|
|
3393
|
+
if (memory.symptoms.length > 0)
|
|
3394
|
+
context += `**Symptoms:** ${memory.symptoms.join(", ")}
|
|
3395
|
+
`;
|
|
3396
|
+
context += `**Root Cause:** ${memory.rootCause}
|
|
3397
|
+
`;
|
|
3398
|
+
context += `**Solution:** ${memory.solution}
|
|
3399
|
+
`;
|
|
3400
|
+
if (memory.technologies && memory.technologies.length > 0)
|
|
3401
|
+
context += `**Technologies:** ${memory.technologies.join(", ")}
|
|
3402
|
+
`;
|
|
3403
|
+
context += `_Confidence: ${(memory.confidence * 100).toFixed(0)}%_
|
|
3404
|
+
|
|
3405
|
+
`;
|
|
3406
|
+
}
|
|
3407
|
+
return context;
|
|
3408
|
+
}
|
|
2454
3409
|
buildContext(memories, maxTokens) {
|
|
2455
3410
|
const parts = [];
|
|
2456
3411
|
let currentTokens = 0;
|
|
2457
3412
|
for (const memory of memories) {
|
|
2458
3413
|
const memoryText = this.formatMemory(memory);
|
|
2459
3414
|
const memoryTokens = this.estimateTokens(memoryText);
|
|
2460
|
-
if (currentTokens + memoryTokens > maxTokens)
|
|
3415
|
+
if (currentTokens + memoryTokens > maxTokens)
|
|
2461
3416
|
break;
|
|
2462
|
-
}
|
|
2463
3417
|
parts.push(memoryText);
|
|
2464
3418
|
currentTokens += memoryTokens;
|
|
2465
3419
|
}
|
|
2466
|
-
if (parts.length === 0)
|
|
3420
|
+
if (parts.length === 0)
|
|
2467
3421
|
return "";
|
|
2468
|
-
}
|
|
2469
3422
|
return `## Relevant Memories
|
|
2470
3423
|
|
|
2471
3424
|
${parts.join("\n\n---\n\n")}`;
|
|
2472
3425
|
}
|
|
2473
|
-
/**
|
|
2474
|
-
* Format a single memory for context
|
|
2475
|
-
*/
|
|
2476
3426
|
formatMemory(memory) {
|
|
2477
3427
|
const { event, score, sessionContext } = memory;
|
|
2478
3428
|
const date = event.timestamp.toISOString().split("T")[0];
|
|
2479
3429
|
let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})
|
|
2480
3430
|
${event.content}`;
|
|
2481
|
-
if (sessionContext)
|
|
3431
|
+
if (sessionContext)
|
|
2482
3432
|
text += `
|
|
2483
3433
|
|
|
2484
3434
|
_Context:_ ${sessionContext}`;
|
|
2485
|
-
}
|
|
2486
3435
|
return text;
|
|
2487
3436
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3437
|
+
matchesMetadataScope(metadata, expected) {
|
|
3438
|
+
if (!metadata)
|
|
3439
|
+
return false;
|
|
3440
|
+
return Object.entries(expected).every(([path10, value]) => {
|
|
3441
|
+
const actual = path10.split(".").reduce((acc, key) => {
|
|
3442
|
+
if (typeof acc !== "object" || acc === null)
|
|
3443
|
+
return void 0;
|
|
3444
|
+
return acc[key];
|
|
3445
|
+
}, metadata);
|
|
3446
|
+
return actual === value;
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
tokenize(text) {
|
|
3450
|
+
return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length >= 2).slice(0, 64);
|
|
3451
|
+
}
|
|
3452
|
+
keywordOverlap(a, b) {
|
|
3453
|
+
if (a.length === 0 || b.length === 0)
|
|
3454
|
+
return 0;
|
|
3455
|
+
const bs = new Set(b);
|
|
3456
|
+
let hit = 0;
|
|
3457
|
+
for (const t of a)
|
|
3458
|
+
if (bs.has(t))
|
|
3459
|
+
hit += 1;
|
|
3460
|
+
return hit / a.length;
|
|
3461
|
+
}
|
|
2491
3462
|
estimateTokens(text) {
|
|
2492
3463
|
return Math.ceil(text.length / 4);
|
|
2493
3464
|
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Get event age in days (for recency scoring)
|
|
2496
|
-
*/
|
|
2497
|
-
getEventAgeDays(eventId) {
|
|
2498
|
-
return 0;
|
|
2499
|
-
}
|
|
2500
3465
|
};
|
|
2501
3466
|
function createRetriever(eventStore, vectorStore, embedder, matcher) {
|
|
2502
3467
|
return new Retriever(eventStore, vectorStore, embedder, matcher);
|
|
@@ -3786,6 +4751,59 @@ var ConsolidatedStore = class {
|
|
|
3786
4751
|
[memoryId]
|
|
3787
4752
|
);
|
|
3788
4753
|
}
|
|
4754
|
+
/**
|
|
4755
|
+
* Create a long-term rule promoted from stable summaries
|
|
4756
|
+
*/
|
|
4757
|
+
async createRule(input) {
|
|
4758
|
+
const ruleId = randomUUID6();
|
|
4759
|
+
await dbRun(
|
|
4760
|
+
this.db,
|
|
4761
|
+
`INSERT INTO consolidated_rules
|
|
4762
|
+
(rule_id, rule, topics, source_memory_ids, source_events, confidence, created_at)
|
|
4763
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
4764
|
+
[
|
|
4765
|
+
ruleId,
|
|
4766
|
+
input.rule,
|
|
4767
|
+
JSON.stringify(input.topics),
|
|
4768
|
+
JSON.stringify(input.sourceMemoryIds),
|
|
4769
|
+
JSON.stringify(input.sourceEvents),
|
|
4770
|
+
input.confidence
|
|
4771
|
+
]
|
|
4772
|
+
);
|
|
4773
|
+
return ruleId;
|
|
4774
|
+
}
|
|
4775
|
+
async getRules(options) {
|
|
4776
|
+
const limit = options?.limit || 100;
|
|
4777
|
+
const rows = await dbAll(
|
|
4778
|
+
this.db,
|
|
4779
|
+
`SELECT * FROM consolidated_rules ORDER BY confidence DESC, created_at DESC LIMIT ?`,
|
|
4780
|
+
[limit]
|
|
4781
|
+
);
|
|
4782
|
+
return rows.map((row) => ({
|
|
4783
|
+
ruleId: row.rule_id,
|
|
4784
|
+
rule: row.rule,
|
|
4785
|
+
topics: JSON.parse(row.topics || "[]"),
|
|
4786
|
+
sourceMemoryIds: JSON.parse(row.source_memory_ids || "[]"),
|
|
4787
|
+
sourceEvents: JSON.parse(row.source_events || "[]"),
|
|
4788
|
+
confidence: Number(row.confidence ?? 0.5),
|
|
4789
|
+
createdAt: toDate(row.created_at) || /* @__PURE__ */ new Date()
|
|
4790
|
+
}));
|
|
4791
|
+
}
|
|
4792
|
+
async countRules() {
|
|
4793
|
+
const result = await dbAll(
|
|
4794
|
+
this.db,
|
|
4795
|
+
`SELECT COUNT(*) as count FROM consolidated_rules`
|
|
4796
|
+
);
|
|
4797
|
+
return result[0]?.count || 0;
|
|
4798
|
+
}
|
|
4799
|
+
async hasRuleForSourceMemory(memoryId) {
|
|
4800
|
+
const rows = await dbAll(
|
|
4801
|
+
this.db,
|
|
4802
|
+
`SELECT COUNT(*) as count FROM consolidated_rules WHERE source_memory_ids LIKE ?`,
|
|
4803
|
+
[`%"${memoryId}"%`]
|
|
4804
|
+
);
|
|
4805
|
+
return (rows[0]?.count || 0) > 0;
|
|
4806
|
+
}
|
|
3789
4807
|
/**
|
|
3790
4808
|
* Get count of consolidated memories
|
|
3791
4809
|
*/
|
|
@@ -3935,7 +4953,14 @@ var ConsolidationWorker = class {
|
|
|
3935
4953
|
* Force a consolidation run (manual trigger)
|
|
3936
4954
|
*/
|
|
3937
4955
|
async forceRun() {
|
|
3938
|
-
|
|
4956
|
+
const out = await this.consolidateWithReport();
|
|
4957
|
+
return out.consolidatedCount;
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Force a consolidation run and return metrics report
|
|
4961
|
+
*/
|
|
4962
|
+
async forceRunWithReport() {
|
|
4963
|
+
return this.consolidateWithReport();
|
|
3939
4964
|
}
|
|
3940
4965
|
/**
|
|
3941
4966
|
* Schedule the next consolidation check
|
|
@@ -3975,12 +5000,21 @@ var ConsolidationWorker = class {
|
|
|
3975
5000
|
* Perform consolidation
|
|
3976
5001
|
*/
|
|
3977
5002
|
async consolidate() {
|
|
5003
|
+
const out = await this.consolidateWithReport();
|
|
5004
|
+
return out.consolidatedCount;
|
|
5005
|
+
}
|
|
5006
|
+
async consolidateWithReport() {
|
|
3978
5007
|
const workingSet = await this.workingSetStore.get();
|
|
3979
5008
|
if (workingSet.recentEvents.length < 3) {
|
|
3980
|
-
return
|
|
5009
|
+
return {
|
|
5010
|
+
consolidatedCount: 0,
|
|
5011
|
+
promotedRuleCount: 0,
|
|
5012
|
+
report: this.buildCostQualityReport(workingSet.recentEvents, [], 0)
|
|
5013
|
+
};
|
|
3981
5014
|
}
|
|
3982
5015
|
const groups = this.groupByTopic(workingSet.recentEvents);
|
|
3983
5016
|
let consolidatedCount = 0;
|
|
5017
|
+
const createdMemoryIds = [];
|
|
3984
5018
|
for (const group of groups) {
|
|
3985
5019
|
if (group.events.length < 3)
|
|
3986
5020
|
continue;
|
|
@@ -3989,14 +5023,16 @@ var ConsolidationWorker = class {
|
|
|
3989
5023
|
if (alreadyConsolidated)
|
|
3990
5024
|
continue;
|
|
3991
5025
|
const summary = await this.summarize(group);
|
|
3992
|
-
await this.consolidatedStore.create({
|
|
5026
|
+
const memoryId = await this.consolidatedStore.create({
|
|
3993
5027
|
summary,
|
|
3994
5028
|
topics: group.topics,
|
|
3995
5029
|
sourceEvents: eventIds,
|
|
3996
5030
|
confidence: this.calculateConfidence(group)
|
|
3997
5031
|
});
|
|
5032
|
+
createdMemoryIds.push(memoryId);
|
|
3998
5033
|
consolidatedCount++;
|
|
3999
5034
|
}
|
|
5035
|
+
const promotedRuleCount = await this.promoteStableSummariesToRules(createdMemoryIds);
|
|
4000
5036
|
if (consolidatedCount > 0) {
|
|
4001
5037
|
const consolidatedEventIds = groups.filter((g) => g.events.length >= 3).flatMap((g) => g.events.map((e) => e.id));
|
|
4002
5038
|
const oldEventIds = consolidatedEventIds.filter((id) => {
|
|
@@ -4010,7 +5046,61 @@ var ConsolidationWorker = class {
|
|
|
4010
5046
|
await this.workingSetStore.prune(oldEventIds);
|
|
4011
5047
|
}
|
|
4012
5048
|
}
|
|
4013
|
-
|
|
5049
|
+
const report = this.buildCostQualityReport(workingSet.recentEvents, groups, consolidatedCount);
|
|
5050
|
+
return { consolidatedCount, promotedRuleCount, report };
|
|
5051
|
+
}
|
|
5052
|
+
async promoteStableSummariesToRules(memoryIds) {
|
|
5053
|
+
let promoted = 0;
|
|
5054
|
+
for (const memoryId of memoryIds) {
|
|
5055
|
+
const memory = await this.consolidatedStore.get(memoryId);
|
|
5056
|
+
if (!memory)
|
|
5057
|
+
continue;
|
|
5058
|
+
if (memory.confidence < 0.55)
|
|
5059
|
+
continue;
|
|
5060
|
+
if (memory.sourceEvents.length < 4)
|
|
5061
|
+
continue;
|
|
5062
|
+
const exists = await this.consolidatedStore.hasRuleForSourceMemory(memoryId);
|
|
5063
|
+
if (exists)
|
|
5064
|
+
continue;
|
|
5065
|
+
const rule = this.buildRuleFromSummary(memory.summary, memory.topics);
|
|
5066
|
+
if (!rule)
|
|
5067
|
+
continue;
|
|
5068
|
+
await this.consolidatedStore.createRule({
|
|
5069
|
+
rule,
|
|
5070
|
+
topics: memory.topics,
|
|
5071
|
+
sourceMemoryIds: [memory.memoryId],
|
|
5072
|
+
sourceEvents: memory.sourceEvents,
|
|
5073
|
+
confidence: Math.min(1, memory.confidence + 0.08)
|
|
5074
|
+
});
|
|
5075
|
+
promoted++;
|
|
5076
|
+
}
|
|
5077
|
+
return promoted;
|
|
5078
|
+
}
|
|
5079
|
+
buildRuleFromSummary(summary, topics) {
|
|
5080
|
+
const lines = summary.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).filter((l) => !l.toLowerCase().startsWith("topics:"));
|
|
5081
|
+
const bullet = lines.find((l) => l.startsWith("- "))?.replace(/^-\s*/, "");
|
|
5082
|
+
const seed = bullet || lines[0];
|
|
5083
|
+
if (!seed || seed.length < 8)
|
|
5084
|
+
return null;
|
|
5085
|
+
const topicPrefix = topics.length > 0 ? `[${topics.slice(0, 2).join(", ")}] ` : "";
|
|
5086
|
+
return `${topicPrefix}${seed}`;
|
|
5087
|
+
}
|
|
5088
|
+
buildCostQualityReport(events, groups, consolidatedCount) {
|
|
5089
|
+
const beforeTokenEstimate = events.reduce((acc, e) => acc + this.estimateTokens(e.content), 0);
|
|
5090
|
+
const afterSummaries = groups.filter((g) => g.events.length >= 3).slice(0, Math.max(consolidatedCount, 1));
|
|
5091
|
+
const afterTokenEstimate = afterSummaries.length > 0 ? afterSummaries.reduce((acc, g) => acc + this.estimateTokens(this.ruleBasedSummary(g)), 0) : beforeTokenEstimate;
|
|
5092
|
+
const reductionRatio = beforeTokenEstimate > 0 ? Math.max(0, (beforeTokenEstimate - afterTokenEstimate) / beforeTokenEstimate) : 0;
|
|
5093
|
+
const qualityGuardPassed = consolidatedCount === 0 ? true : groups.filter((g) => g.events.length >= 3).every((g) => this.calculateConfidence(g) >= 0.55);
|
|
5094
|
+
return {
|
|
5095
|
+
beforeTokenEstimate,
|
|
5096
|
+
afterTokenEstimate,
|
|
5097
|
+
reductionRatio,
|
|
5098
|
+
qualityGuardPassed,
|
|
5099
|
+
details: `groups=${groups.length}, consolidated=${consolidatedCount}`
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
estimateTokens(text) {
|
|
5103
|
+
return Math.ceil((text || "").length / 4);
|
|
4014
5104
|
}
|
|
4015
5105
|
/**
|
|
4016
5106
|
* Check if consolidation should run
|
|
@@ -4570,13 +5660,185 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
4570
5660
|
);
|
|
4571
5661
|
}
|
|
4572
5662
|
|
|
5663
|
+
// src/core/md-mirror.ts
|
|
5664
|
+
import * as fs3 from "node:fs";
|
|
5665
|
+
import * as path2 from "node:path";
|
|
5666
|
+
function sanitizeSegment2(input, fallback) {
|
|
5667
|
+
const v = (input || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5668
|
+
return v || fallback;
|
|
5669
|
+
}
|
|
5670
|
+
function getAtPath(obj, dotted) {
|
|
5671
|
+
if (!obj)
|
|
5672
|
+
return void 0;
|
|
5673
|
+
return dotted.split(".").reduce((acc, key) => {
|
|
5674
|
+
if (!acc || typeof acc !== "object")
|
|
5675
|
+
return void 0;
|
|
5676
|
+
return acc[key];
|
|
5677
|
+
}, obj);
|
|
5678
|
+
}
|
|
5679
|
+
function buildMirrorPath2(rootDir, event) {
|
|
5680
|
+
const meta = event.metadata;
|
|
5681
|
+
const namespaceRaw = getAtPath(meta, "namespace") ?? getAtPath(meta, "scope.namespace") ?? event.eventType;
|
|
5682
|
+
const namespace = sanitizeSegment2(typeof namespaceRaw === "string" ? namespaceRaw : void 0, "general");
|
|
5683
|
+
const categoryRaw = getAtPath(meta, "categoryPath") ?? getAtPath(meta, "scope.categoryPath");
|
|
5684
|
+
const categoryPath = Array.isArray(categoryRaw) && categoryRaw.length > 0 ? categoryRaw.map((x) => sanitizeSegment2(typeof x === "string" ? x : void 0, "uncategorized")) : ["uncategorized"];
|
|
5685
|
+
const d = event.timestamp;
|
|
5686
|
+
const yyyy = d.getFullYear();
|
|
5687
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
5688
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
5689
|
+
return path2.join(rootDir, "memory", namespace, ...categoryPath, `${yyyy}-${mm}-${dd}.md`);
|
|
5690
|
+
}
|
|
5691
|
+
var MarkdownMirror2 = class {
|
|
5692
|
+
constructor(rootDir) {
|
|
5693
|
+
this.rootDir = rootDir;
|
|
5694
|
+
}
|
|
5695
|
+
async append(event, eventId) {
|
|
5696
|
+
const out = buildMirrorPath2(this.rootDir, event);
|
|
5697
|
+
fs3.mkdirSync(path2.dirname(out), { recursive: true });
|
|
5698
|
+
const lines = [
|
|
5699
|
+
"",
|
|
5700
|
+
`## ${event.timestamp.toISOString()} | ${eventId ?? "pending-id"}`,
|
|
5701
|
+
`- type: ${event.eventType}`,
|
|
5702
|
+
`- session: ${event.sessionId}`,
|
|
5703
|
+
event.content
|
|
5704
|
+
];
|
|
5705
|
+
await fs3.promises.appendFile(out, lines.join("\n"), "utf8");
|
|
5706
|
+
await this.refreshIndex();
|
|
5707
|
+
}
|
|
5708
|
+
async refreshIndex() {
|
|
5709
|
+
const memoryRoot = path2.join(this.rootDir, "memory");
|
|
5710
|
+
await fs3.promises.mkdir(memoryRoot, { recursive: true });
|
|
5711
|
+
const files = [];
|
|
5712
|
+
await this.walk(memoryRoot, files);
|
|
5713
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).map((f) => path2.relative(this.rootDir, f)).filter((rel) => rel !== path2.join("memory", "_index.md")).sort();
|
|
5714
|
+
const index = [
|
|
5715
|
+
"# Memory Index",
|
|
5716
|
+
"",
|
|
5717
|
+
"Generated automatically by MarkdownMirror.",
|
|
5718
|
+
"",
|
|
5719
|
+
...mdFiles.map((rel) => `- ${rel}`),
|
|
5720
|
+
""
|
|
5721
|
+
].join("\n");
|
|
5722
|
+
await fs3.promises.writeFile(path2.join(memoryRoot, "_index.md"), index, "utf8");
|
|
5723
|
+
}
|
|
5724
|
+
async walk(dir, out) {
|
|
5725
|
+
const entries = await fs3.promises.readdir(dir, { withFileTypes: true });
|
|
5726
|
+
for (const e of entries) {
|
|
5727
|
+
const full = path2.join(dir, e.name);
|
|
5728
|
+
if (e.isDirectory()) {
|
|
5729
|
+
await this.walk(full, out);
|
|
5730
|
+
} else {
|
|
5731
|
+
out.push(full);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
};
|
|
5736
|
+
|
|
5737
|
+
// src/core/ingest-interceptor.ts
|
|
5738
|
+
var IngestInterceptorRegistry = class {
|
|
5739
|
+
before = [];
|
|
5740
|
+
after = [];
|
|
5741
|
+
onError = [];
|
|
5742
|
+
registerBefore(interceptor) {
|
|
5743
|
+
this.before.push(interceptor);
|
|
5744
|
+
return () => {
|
|
5745
|
+
this.before = this.before.filter((i) => i !== interceptor);
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
registerAfter(interceptor) {
|
|
5749
|
+
this.after.push(interceptor);
|
|
5750
|
+
return () => {
|
|
5751
|
+
this.after = this.after.filter((i) => i !== interceptor);
|
|
5752
|
+
};
|
|
5753
|
+
}
|
|
5754
|
+
registerOnError(interceptor) {
|
|
5755
|
+
this.onError.push(interceptor);
|
|
5756
|
+
return () => {
|
|
5757
|
+
this.onError = this.onError.filter((i) => i !== interceptor);
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
async run(stage, context) {
|
|
5761
|
+
const interceptors = stage === "before" ? this.before : stage === "after" ? this.after : this.onError;
|
|
5762
|
+
for (const interceptor of interceptors) {
|
|
5763
|
+
await interceptor({ ...context, stage });
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
};
|
|
5767
|
+
function mergeHierarchicalMetadata(base, patch) {
|
|
5768
|
+
if (!base && !patch)
|
|
5769
|
+
return void 0;
|
|
5770
|
+
if (!base)
|
|
5771
|
+
return patch;
|
|
5772
|
+
if (!patch)
|
|
5773
|
+
return base;
|
|
5774
|
+
const result = { ...base };
|
|
5775
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
5776
|
+
const current = result[key];
|
|
5777
|
+
if (typeof current === "object" && current !== null && !Array.isArray(current) && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
5778
|
+
result[key] = mergeHierarchicalMetadata(
|
|
5779
|
+
current,
|
|
5780
|
+
value
|
|
5781
|
+
);
|
|
5782
|
+
} else {
|
|
5783
|
+
result[key] = value;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
return result;
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
// src/core/tag-taxonomy.ts
|
|
5790
|
+
var TAG_NAMESPACES = {
|
|
5791
|
+
SYSTEM: "sys:",
|
|
5792
|
+
QUALITY: "q:",
|
|
5793
|
+
PROJECT: "proj:",
|
|
5794
|
+
TOPIC: "topic:",
|
|
5795
|
+
TEMPORAL: "t:",
|
|
5796
|
+
USER: "user:",
|
|
5797
|
+
AGENT: "agent:"
|
|
5798
|
+
};
|
|
5799
|
+
var VALID_TAG_NAMESPACES = new Set(Object.values(TAG_NAMESPACES));
|
|
5800
|
+
function parseTag(tag) {
|
|
5801
|
+
const value = (tag || "").trim();
|
|
5802
|
+
const idx = value.indexOf(":");
|
|
5803
|
+
if (idx <= 0)
|
|
5804
|
+
return { value };
|
|
5805
|
+
const namespace = `${value.slice(0, idx)}:`;
|
|
5806
|
+
const tagValue = value.slice(idx + 1);
|
|
5807
|
+
if (!tagValue)
|
|
5808
|
+
return { value };
|
|
5809
|
+
return { namespace, value: tagValue };
|
|
5810
|
+
}
|
|
5811
|
+
function validateTag(tag) {
|
|
5812
|
+
const normalized = (tag || "").trim();
|
|
5813
|
+
if (!normalized)
|
|
5814
|
+
return false;
|
|
5815
|
+
const { namespace } = parseTag(normalized);
|
|
5816
|
+
if (!namespace)
|
|
5817
|
+
return true;
|
|
5818
|
+
return VALID_TAG_NAMESPACES.has(namespace);
|
|
5819
|
+
}
|
|
5820
|
+
function normalizeTags(tags) {
|
|
5821
|
+
if (!Array.isArray(tags))
|
|
5822
|
+
return [];
|
|
5823
|
+
const dedup = /* @__PURE__ */ new Set();
|
|
5824
|
+
for (const item of tags) {
|
|
5825
|
+
if (typeof item !== "string")
|
|
5826
|
+
continue;
|
|
5827
|
+
const normalized = item.trim();
|
|
5828
|
+
if (!validateTag(normalized))
|
|
5829
|
+
continue;
|
|
5830
|
+
dedup.add(normalized);
|
|
5831
|
+
}
|
|
5832
|
+
return [...dedup];
|
|
5833
|
+
}
|
|
5834
|
+
|
|
4573
5835
|
// src/services/memory-service.ts
|
|
4574
5836
|
function normalizePath(projectPath) {
|
|
4575
|
-
const expanded = projectPath.startsWith("~") ?
|
|
5837
|
+
const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
4576
5838
|
try {
|
|
4577
|
-
return
|
|
5839
|
+
return fs4.realpathSync(expanded);
|
|
4578
5840
|
} catch {
|
|
4579
|
-
return
|
|
5841
|
+
return path3.resolve(expanded);
|
|
4580
5842
|
}
|
|
4581
5843
|
}
|
|
4582
5844
|
function hashProjectPath(projectPath) {
|
|
@@ -4585,10 +5847,46 @@ function hashProjectPath(projectPath) {
|
|
|
4585
5847
|
}
|
|
4586
5848
|
function getProjectStoragePath(projectPath) {
|
|
4587
5849
|
const hash = hashProjectPath(projectPath);
|
|
4588
|
-
return
|
|
5850
|
+
return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
5851
|
+
}
|
|
5852
|
+
var REGISTRY_PATH = path3.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
5853
|
+
var SHARED_STORAGE_PATH = path3.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
5854
|
+
function loadSessionRegistry() {
|
|
5855
|
+
try {
|
|
5856
|
+
if (fs4.existsSync(REGISTRY_PATH)) {
|
|
5857
|
+
const data = fs4.readFileSync(REGISTRY_PATH, "utf-8");
|
|
5858
|
+
return JSON.parse(data);
|
|
5859
|
+
}
|
|
5860
|
+
} catch (error) {
|
|
5861
|
+
console.error("Failed to load session registry:", error);
|
|
5862
|
+
}
|
|
5863
|
+
return { version: 1, sessions: {} };
|
|
5864
|
+
}
|
|
5865
|
+
function saveSessionRegistry(registry) {
|
|
5866
|
+
const dir = path3.dirname(REGISTRY_PATH);
|
|
5867
|
+
if (!fs4.existsSync(dir)) {
|
|
5868
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
5869
|
+
}
|
|
5870
|
+
const tempPath = REGISTRY_PATH + ".tmp";
|
|
5871
|
+
fs4.writeFileSync(tempPath, JSON.stringify(registry, null, 2));
|
|
5872
|
+
fs4.renameSync(tempPath, REGISTRY_PATH);
|
|
5873
|
+
}
|
|
5874
|
+
function registerSession(sessionId, projectPath) {
|
|
5875
|
+
const registry = loadSessionRegistry();
|
|
5876
|
+
registry.sessions[sessionId] = {
|
|
5877
|
+
projectPath: normalizePath(projectPath),
|
|
5878
|
+
projectHash: hashProjectPath(projectPath),
|
|
5879
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5880
|
+
};
|
|
5881
|
+
const entries = Object.entries(registry.sessions);
|
|
5882
|
+
if (entries.length > 1e3) {
|
|
5883
|
+
const sorted = entries.sort(
|
|
5884
|
+
(a, b) => new Date(b[1].registeredAt).getTime() - new Date(a[1].registeredAt).getTime()
|
|
5885
|
+
);
|
|
5886
|
+
registry.sessions = Object.fromEntries(sorted.slice(0, 1e3));
|
|
5887
|
+
}
|
|
5888
|
+
saveSessionRegistry(registry);
|
|
4589
5889
|
}
|
|
4590
|
-
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
4591
|
-
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
4592
5890
|
var MemoryService = class {
|
|
4593
5891
|
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4594
5892
|
sqliteStore;
|
|
@@ -4603,6 +5901,7 @@ var MemoryService = class {
|
|
|
4603
5901
|
vectorWorker = null;
|
|
4604
5902
|
graduationWorker = null;
|
|
4605
5903
|
initialized = false;
|
|
5904
|
+
ingestInterceptors = new IngestInterceptorRegistry();
|
|
4606
5905
|
// Endless Mode components
|
|
4607
5906
|
workingSetStore = null;
|
|
4608
5907
|
consolidatedStore = null;
|
|
@@ -4616,20 +5915,27 @@ var MemoryService = class {
|
|
|
4616
5915
|
sharedPromoter = null;
|
|
4617
5916
|
sharedStoreConfig = null;
|
|
4618
5917
|
projectHash = null;
|
|
5918
|
+
projectPath = null;
|
|
4619
5919
|
readOnly;
|
|
4620
5920
|
lightweightMode;
|
|
5921
|
+
mdMirror;
|
|
4621
5922
|
constructor(config) {
|
|
4622
5923
|
const storagePath = this.expandPath(config.storagePath);
|
|
4623
5924
|
this.readOnly = config.readOnly ?? false;
|
|
4624
5925
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
4625
|
-
|
|
4626
|
-
|
|
5926
|
+
this.mdMirror = new MarkdownMirror2(process.cwd());
|
|
5927
|
+
if (!this.readOnly && !fs4.existsSync(storagePath)) {
|
|
5928
|
+
fs4.mkdirSync(storagePath, { recursive: true });
|
|
4627
5929
|
}
|
|
4628
5930
|
this.projectHash = config.projectHash || null;
|
|
5931
|
+
this.projectPath = config.projectPath || null;
|
|
4629
5932
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
4630
5933
|
this.sqliteStore = new SQLiteEventStore(
|
|
4631
|
-
|
|
4632
|
-
{
|
|
5934
|
+
path3.join(storagePath, "events.sqlite"),
|
|
5935
|
+
{
|
|
5936
|
+
readonly: this.readOnly,
|
|
5937
|
+
markdownMirrorRoot: storagePath
|
|
5938
|
+
}
|
|
4633
5939
|
);
|
|
4634
5940
|
const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
|
|
4635
5941
|
if (!analyticsEnabled) {
|
|
@@ -4637,7 +5943,7 @@ var MemoryService = class {
|
|
|
4637
5943
|
} else if (this.readOnly) {
|
|
4638
5944
|
try {
|
|
4639
5945
|
this.analyticsStore = new EventStore(
|
|
4640
|
-
|
|
5946
|
+
path3.join(storagePath, "analytics.duckdb"),
|
|
4641
5947
|
{ readOnly: true }
|
|
4642
5948
|
);
|
|
4643
5949
|
} catch {
|
|
@@ -4645,11 +5951,11 @@ var MemoryService = class {
|
|
|
4645
5951
|
}
|
|
4646
5952
|
} else {
|
|
4647
5953
|
this.analyticsStore = new EventStore(
|
|
4648
|
-
|
|
5954
|
+
path3.join(storagePath, "analytics.duckdb"),
|
|
4649
5955
|
{ readOnly: false }
|
|
4650
5956
|
);
|
|
4651
5957
|
}
|
|
4652
|
-
this.vectorStore = new VectorStore(
|
|
5958
|
+
this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
|
|
4653
5959
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
4654
5960
|
this.matcher = getDefaultMatcher();
|
|
4655
5961
|
this.retriever = createRetriever(
|
|
@@ -4659,6 +5965,7 @@ var MemoryService = class {
|
|
|
4659
5965
|
this.embedder,
|
|
4660
5966
|
this.matcher
|
|
4661
5967
|
);
|
|
5968
|
+
this.retriever.setQueryRewriter((q) => this.rewriteQueryIntent(q));
|
|
4662
5969
|
this.graduation = createGraduationPipeline(this.sqliteStore);
|
|
4663
5970
|
}
|
|
4664
5971
|
/**
|
|
@@ -4718,16 +6025,16 @@ var MemoryService = class {
|
|
|
4718
6025
|
*/
|
|
4719
6026
|
async initializeSharedStore() {
|
|
4720
6027
|
const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
|
|
4721
|
-
if (!
|
|
4722
|
-
|
|
6028
|
+
if (!fs4.existsSync(sharedPath)) {
|
|
6029
|
+
fs4.mkdirSync(sharedPath, { recursive: true });
|
|
4723
6030
|
}
|
|
4724
6031
|
this.sharedEventStore = createSharedEventStore(
|
|
4725
|
-
|
|
6032
|
+
path3.join(sharedPath, "shared.duckdb")
|
|
4726
6033
|
);
|
|
4727
6034
|
await this.sharedEventStore.initialize();
|
|
4728
6035
|
this.sharedStore = createSharedStore(this.sharedEventStore);
|
|
4729
6036
|
this.sharedVectorStore = createSharedVectorStore(
|
|
4730
|
-
|
|
6037
|
+
path3.join(sharedPath, "vectors")
|
|
4731
6038
|
);
|
|
4732
6039
|
await this.sharedVectorStore.initialize();
|
|
4733
6040
|
this.sharedPromoter = createSharedPromoter(
|
|
@@ -4738,18 +6045,98 @@ var MemoryService = class {
|
|
|
4738
6045
|
);
|
|
4739
6046
|
this.retriever.setSharedStores(this.sharedStore, this.sharedVectorStore);
|
|
4740
6047
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
*/
|
|
4744
|
-
async startSession(sessionId, projectPath) {
|
|
4745
|
-
await this.initialize();
|
|
4746
|
-
await this.sqliteStore.upsertSession({
|
|
4747
|
-
id: sessionId,
|
|
4748
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
4749
|
-
projectPath
|
|
4750
|
-
});
|
|
6048
|
+
registerIngestBefore(interceptor) {
|
|
6049
|
+
return this.ingestInterceptors.registerBefore(interceptor);
|
|
4751
6050
|
}
|
|
4752
|
-
|
|
6051
|
+
registerIngestAfter(interceptor) {
|
|
6052
|
+
return this.ingestInterceptors.registerAfter(interceptor);
|
|
6053
|
+
}
|
|
6054
|
+
registerIngestOnError(interceptor) {
|
|
6055
|
+
return this.ingestInterceptors.registerOnError(interceptor);
|
|
6056
|
+
}
|
|
6057
|
+
async ingestWithInterceptors(operation, input, onSuccess) {
|
|
6058
|
+
const normalizedInput = {
|
|
6059
|
+
...input,
|
|
6060
|
+
metadata: mergeHierarchicalMetadata(
|
|
6061
|
+
{
|
|
6062
|
+
ingest: {
|
|
6063
|
+
operation,
|
|
6064
|
+
pipeline: "default",
|
|
6065
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
6066
|
+
},
|
|
6067
|
+
...this.projectHash ? {
|
|
6068
|
+
scope: {
|
|
6069
|
+
project: {
|
|
6070
|
+
hash: this.projectHash,
|
|
6071
|
+
...this.projectPath ? { path: this.projectPath } : {}
|
|
6072
|
+
}
|
|
6073
|
+
},
|
|
6074
|
+
tags: [`proj:${this.projectHash}`]
|
|
6075
|
+
} : {}
|
|
6076
|
+
},
|
|
6077
|
+
input.metadata
|
|
6078
|
+
)
|
|
6079
|
+
};
|
|
6080
|
+
if (this.projectHash && normalizedInput.metadata) {
|
|
6081
|
+
const meta = normalizedInput.metadata;
|
|
6082
|
+
const currentTags = Array.isArray(meta.tags) ? meta.tags.filter((x) => typeof x === "string") : [];
|
|
6083
|
+
const projectTag = `proj:${this.projectHash}`;
|
|
6084
|
+
if (!currentTags.includes(projectTag)) {
|
|
6085
|
+
meta.tags = [...currentTags, projectTag];
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
if (normalizedInput.metadata) {
|
|
6089
|
+
const meta = normalizedInput.metadata;
|
|
6090
|
+
const normalizedTags = normalizeTags(meta.tags);
|
|
6091
|
+
if (normalizedTags.length > 0) {
|
|
6092
|
+
meta.tags = normalizedTags;
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
await this.ingestInterceptors.run("before", {
|
|
6096
|
+
operation,
|
|
6097
|
+
sessionId: normalizedInput.sessionId,
|
|
6098
|
+
event: normalizedInput
|
|
6099
|
+
});
|
|
6100
|
+
try {
|
|
6101
|
+
const result = await this.sqliteStore.append(normalizedInput);
|
|
6102
|
+
if (result.success && !result.isDuplicate) {
|
|
6103
|
+
if (onSuccess) {
|
|
6104
|
+
await onSuccess(result.eventId);
|
|
6105
|
+
}
|
|
6106
|
+
try {
|
|
6107
|
+
await this.mdMirror.append(normalizedInput, result.eventId);
|
|
6108
|
+
} catch {
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
await this.ingestInterceptors.run("after", {
|
|
6112
|
+
operation,
|
|
6113
|
+
sessionId: normalizedInput.sessionId,
|
|
6114
|
+
event: normalizedInput
|
|
6115
|
+
});
|
|
6116
|
+
return result;
|
|
6117
|
+
} catch (error) {
|
|
6118
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
6119
|
+
await this.ingestInterceptors.run("error", {
|
|
6120
|
+
operation,
|
|
6121
|
+
sessionId: normalizedInput.sessionId,
|
|
6122
|
+
event: normalizedInput,
|
|
6123
|
+
error: normalizedError
|
|
6124
|
+
});
|
|
6125
|
+
throw error;
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
/**
|
|
6129
|
+
* Start a new session
|
|
6130
|
+
*/
|
|
6131
|
+
async startSession(sessionId, projectPath) {
|
|
6132
|
+
await this.initialize();
|
|
6133
|
+
await this.sqliteStore.upsertSession({
|
|
6134
|
+
id: sessionId,
|
|
6135
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
6136
|
+
projectPath
|
|
6137
|
+
});
|
|
6138
|
+
}
|
|
6139
|
+
/**
|
|
4753
6140
|
* End a session
|
|
4754
6141
|
*/
|
|
4755
6142
|
async endSession(sessionId, summary) {
|
|
@@ -4765,50 +6152,57 @@ var MemoryService = class {
|
|
|
4765
6152
|
*/
|
|
4766
6153
|
async storeUserPrompt(sessionId, content, metadata) {
|
|
4767
6154
|
await this.initialize();
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
6155
|
+
return this.ingestWithInterceptors(
|
|
6156
|
+
"user_prompt",
|
|
6157
|
+
{
|
|
6158
|
+
eventType: "user_prompt",
|
|
6159
|
+
sessionId,
|
|
6160
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
6161
|
+
content,
|
|
6162
|
+
metadata
|
|
6163
|
+
},
|
|
6164
|
+
async (eventId) => {
|
|
6165
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, content);
|
|
6166
|
+
}
|
|
6167
|
+
);
|
|
4779
6168
|
}
|
|
4780
6169
|
/**
|
|
4781
6170
|
* Store an agent response
|
|
4782
6171
|
*/
|
|
4783
6172
|
async storeAgentResponse(sessionId, content, metadata) {
|
|
4784
6173
|
await this.initialize();
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
6174
|
+
return this.ingestWithInterceptors(
|
|
6175
|
+
"agent_response",
|
|
6176
|
+
{
|
|
6177
|
+
eventType: "agent_response",
|
|
6178
|
+
sessionId,
|
|
6179
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
6180
|
+
content,
|
|
6181
|
+
metadata
|
|
6182
|
+
},
|
|
6183
|
+
async (eventId) => {
|
|
6184
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, content);
|
|
6185
|
+
}
|
|
6186
|
+
);
|
|
4796
6187
|
}
|
|
4797
6188
|
/**
|
|
4798
6189
|
* Store a session summary
|
|
4799
6190
|
*/
|
|
4800
|
-
async storeSessionSummary(sessionId, summary) {
|
|
6191
|
+
async storeSessionSummary(sessionId, summary, metadata) {
|
|
4801
6192
|
await this.initialize();
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
6193
|
+
return this.ingestWithInterceptors(
|
|
6194
|
+
"session_summary",
|
|
6195
|
+
{
|
|
6196
|
+
eventType: "session_summary",
|
|
6197
|
+
sessionId,
|
|
6198
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
6199
|
+
content: summary,
|
|
6200
|
+
metadata
|
|
6201
|
+
},
|
|
6202
|
+
async (eventId) => {
|
|
6203
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, summary);
|
|
6204
|
+
}
|
|
6205
|
+
);
|
|
4812
6206
|
}
|
|
4813
6207
|
/**
|
|
4814
6208
|
* Store a tool observation
|
|
@@ -4816,39 +6210,182 @@ var MemoryService = class {
|
|
|
4816
6210
|
async storeToolObservation(sessionId, payload) {
|
|
4817
6211
|
await this.initialize();
|
|
4818
6212
|
const content = JSON.stringify(payload);
|
|
4819
|
-
const
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
6213
|
+
const turnId = payload.metadata?.turnId;
|
|
6214
|
+
return this.ingestWithInterceptors(
|
|
6215
|
+
"tool_observation",
|
|
6216
|
+
{
|
|
6217
|
+
eventType: "tool_observation",
|
|
6218
|
+
sessionId,
|
|
6219
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
6220
|
+
content,
|
|
6221
|
+
metadata: {
|
|
6222
|
+
toolName: payload.toolName,
|
|
6223
|
+
success: payload.success,
|
|
6224
|
+
...turnId ? { turnId } : {}
|
|
6225
|
+
}
|
|
6226
|
+
},
|
|
6227
|
+
async (eventId) => {
|
|
6228
|
+
const embeddingContent = createToolObservationEmbedding(
|
|
6229
|
+
payload.toolName,
|
|
6230
|
+
payload.metadata || {},
|
|
6231
|
+
payload.success
|
|
6232
|
+
);
|
|
6233
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, embeddingContent);
|
|
4827
6234
|
}
|
|
4828
|
-
|
|
4829
|
-
if (result.success && !result.isDuplicate) {
|
|
4830
|
-
const embeddingContent = createToolObservationEmbedding(
|
|
4831
|
-
payload.toolName,
|
|
4832
|
-
payload.metadata || {},
|
|
4833
|
-
payload.success
|
|
4834
|
-
);
|
|
4835
|
-
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
4836
|
-
}
|
|
4837
|
-
return result;
|
|
6235
|
+
);
|
|
4838
6236
|
}
|
|
4839
6237
|
/**
|
|
4840
6238
|
* Retrieve relevant memories for a query
|
|
4841
6239
|
*/
|
|
4842
6240
|
async retrieveMemories(query, options) {
|
|
4843
6241
|
await this.initialize();
|
|
6242
|
+
const rerankWeights = await this.getRerankWeights(options?.adaptiveRerank === true);
|
|
6243
|
+
let result;
|
|
4844
6244
|
if (options?.includeShared && this.sharedStore) {
|
|
4845
|
-
|
|
6245
|
+
result = await this.retriever.retrieveUnified(query, {
|
|
4846
6246
|
...options,
|
|
6247
|
+
intentRewrite: options?.intentRewrite === true,
|
|
6248
|
+
rerankWeights,
|
|
4847
6249
|
includeShared: true,
|
|
4848
|
-
projectHash: this.projectHash || void 0
|
|
6250
|
+
projectHash: this.projectHash || void 0,
|
|
6251
|
+
projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? "strict" : "global"),
|
|
6252
|
+
allowedProjectHashes: options?.allowedProjectHashes
|
|
6253
|
+
});
|
|
6254
|
+
} else {
|
|
6255
|
+
result = await this.retriever.retrieve(query, {
|
|
6256
|
+
...options,
|
|
6257
|
+
intentRewrite: options?.intentRewrite === true,
|
|
6258
|
+
rerankWeights,
|
|
6259
|
+
projectHash: this.projectHash || void 0,
|
|
6260
|
+
projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? "strict" : "global"),
|
|
6261
|
+
allowedProjectHashes: options?.allowedProjectHashes
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
try {
|
|
6265
|
+
const selectedEventIds = result.memories.map((m) => m.event.id);
|
|
6266
|
+
const selectedDetails = (result.selectedDebug || []).map((d) => ({
|
|
6267
|
+
eventId: d.eventId,
|
|
6268
|
+
score: d.score,
|
|
6269
|
+
semanticScore: d.semanticScore,
|
|
6270
|
+
lexicalScore: d.lexicalScore,
|
|
6271
|
+
recencyScore: d.recencyScore
|
|
6272
|
+
}));
|
|
6273
|
+
const candidateDetails = (result.candidateDebug || []).map((d) => ({
|
|
6274
|
+
eventId: d.eventId,
|
|
6275
|
+
score: d.score,
|
|
6276
|
+
semanticScore: d.semanticScore,
|
|
6277
|
+
lexicalScore: d.lexicalScore,
|
|
6278
|
+
recencyScore: d.recencyScore
|
|
6279
|
+
}));
|
|
6280
|
+
const candidateEventIds = candidateDetails.length > 0 ? candidateDetails.map((d) => d.eventId) : selectedEventIds;
|
|
6281
|
+
await this.sqliteStore.recordRetrievalTrace({
|
|
6282
|
+
sessionId: options?.sessionId,
|
|
6283
|
+
projectHash: this.projectHash || void 0,
|
|
6284
|
+
queryText: query,
|
|
6285
|
+
strategy: options?.strategy || "auto",
|
|
6286
|
+
candidateEventIds,
|
|
6287
|
+
selectedEventIds,
|
|
6288
|
+
candidateDetails,
|
|
6289
|
+
selectedDetails,
|
|
6290
|
+
confidence: result.matchResult.confidence,
|
|
6291
|
+
fallbackTrace: result.fallbackTrace || []
|
|
6292
|
+
});
|
|
6293
|
+
} catch {
|
|
6294
|
+
}
|
|
6295
|
+
return result;
|
|
6296
|
+
}
|
|
6297
|
+
getConfiguredRerankWeights() {
|
|
6298
|
+
const semantic = Number(process.env.MEMORY_RERANK_WEIGHT_SEMANTIC ?? "");
|
|
6299
|
+
const lexical = Number(process.env.MEMORY_RERANK_WEIGHT_LEXICAL ?? "");
|
|
6300
|
+
const recency = Number(process.env.MEMORY_RERANK_WEIGHT_RECENCY ?? "");
|
|
6301
|
+
const allFinite = [semantic, lexical, recency].every((v) => Number.isFinite(v));
|
|
6302
|
+
if (!allFinite)
|
|
6303
|
+
return void 0;
|
|
6304
|
+
const nonNegative = [semantic, lexical, recency].every((v) => v >= 0);
|
|
6305
|
+
const total = semantic + lexical + recency;
|
|
6306
|
+
if (!nonNegative || total <= 0)
|
|
6307
|
+
return void 0;
|
|
6308
|
+
return {
|
|
6309
|
+
semantic: semantic / total,
|
|
6310
|
+
lexical: lexical / total,
|
|
6311
|
+
recency: recency / total
|
|
6312
|
+
};
|
|
6313
|
+
}
|
|
6314
|
+
async getRerankWeights(adaptive) {
|
|
6315
|
+
const configured = this.getConfiguredRerankWeights();
|
|
6316
|
+
if (configured)
|
|
6317
|
+
return configured;
|
|
6318
|
+
if (adaptive)
|
|
6319
|
+
return this.getAdaptiveRerankWeights();
|
|
6320
|
+
return void 0;
|
|
6321
|
+
}
|
|
6322
|
+
async rewriteQueryIntent(query) {
|
|
6323
|
+
if (process.env.MEMORY_INTENT_REWRITE_ENABLED !== "1")
|
|
6324
|
+
return null;
|
|
6325
|
+
const apiUrl = process.env.COMPANY_STOCK_API_URL || process.env.COMPANY_INT_API_URL;
|
|
6326
|
+
if (!apiUrl)
|
|
6327
|
+
return null;
|
|
6328
|
+
const controller = new AbortController();
|
|
6329
|
+
const timeoutMs = Number(process.env.MEMORY_INTENT_REWRITE_TIMEOUT_MS || 5e3);
|
|
6330
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
6331
|
+
try {
|
|
6332
|
+
const prompt = [
|
|
6333
|
+
"Rewrite user query for memory retrieval intent expansion.",
|
|
6334
|
+
"Return plain text only, one line, no markdown.",
|
|
6335
|
+
`Query: ${query}`
|
|
6336
|
+
].join("\n");
|
|
6337
|
+
const res = await fetch(apiUrl, {
|
|
6338
|
+
method: "POST",
|
|
6339
|
+
headers: {
|
|
6340
|
+
"Content-Type": "application/json",
|
|
6341
|
+
Accept: "*/*",
|
|
6342
|
+
Origin: process.env.COMPANY_INT_ORIGIN || "http://company-int.aplusai.ai",
|
|
6343
|
+
Referer: process.env.COMPANY_INT_REFERER || "http://company-int.aplusai.ai/"
|
|
6344
|
+
},
|
|
6345
|
+
body: JSON.stringify({
|
|
6346
|
+
question: prompt,
|
|
6347
|
+
company_name: null,
|
|
6348
|
+
conversation_id: null
|
|
6349
|
+
}),
|
|
6350
|
+
signal: controller.signal
|
|
4849
6351
|
});
|
|
6352
|
+
const text = (await res.text()).trim();
|
|
6353
|
+
if (!text)
|
|
6354
|
+
return null;
|
|
6355
|
+
const oneLine = text.replace(/^data:\s*/gm, "").split(/\r?\n/).map((x) => x.trim()).filter(Boolean).join(" ").slice(0, 240);
|
|
6356
|
+
if (!oneLine || oneLine.toLowerCase() === query.toLowerCase())
|
|
6357
|
+
return null;
|
|
6358
|
+
return oneLine;
|
|
6359
|
+
} catch {
|
|
6360
|
+
return null;
|
|
6361
|
+
} finally {
|
|
6362
|
+
clearTimeout(timeout);
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
async getAdaptiveRerankWeights() {
|
|
6366
|
+
try {
|
|
6367
|
+
const s = await this.sqliteStore.getHelpfulnessStats();
|
|
6368
|
+
if (s.totalEvaluated < 20)
|
|
6369
|
+
return void 0;
|
|
6370
|
+
let semantic = 0.7;
|
|
6371
|
+
let lexical = 0.2;
|
|
6372
|
+
let recency = 0.1;
|
|
6373
|
+
if (s.avgScore < 0.45) {
|
|
6374
|
+
semantic -= 0.1;
|
|
6375
|
+
lexical += 0.1;
|
|
6376
|
+
} else if (s.avgScore > 0.75) {
|
|
6377
|
+
semantic += 0.05;
|
|
6378
|
+
lexical -= 0.05;
|
|
6379
|
+
}
|
|
6380
|
+
if (s.unhelpful > s.helpful) {
|
|
6381
|
+
recency += 0.05;
|
|
6382
|
+
semantic -= 0.03;
|
|
6383
|
+
lexical -= 0.02;
|
|
6384
|
+
}
|
|
6385
|
+
return { semantic, lexical, recency };
|
|
6386
|
+
} catch {
|
|
6387
|
+
return void 0;
|
|
4850
6388
|
}
|
|
4851
|
-
return this.retriever.retrieve(query, options);
|
|
4852
6389
|
}
|
|
4853
6390
|
/**
|
|
4854
6391
|
* Fast keyword search using SQLite FTS5
|
|
@@ -4890,6 +6427,18 @@ var MemoryService = class {
|
|
|
4890
6427
|
/**
|
|
4891
6428
|
* Get memory statistics
|
|
4892
6429
|
*/
|
|
6430
|
+
async getOutboxStats() {
|
|
6431
|
+
await this.initialize();
|
|
6432
|
+
return this.sqliteStore.getOutboxStats();
|
|
6433
|
+
}
|
|
6434
|
+
async getRetrievalTraceStats() {
|
|
6435
|
+
await this.initialize();
|
|
6436
|
+
return this.sqliteStore.getRetrievalTraceStats();
|
|
6437
|
+
}
|
|
6438
|
+
async getRecentRetrievalTraces(limit = 50) {
|
|
6439
|
+
await this.initialize();
|
|
6440
|
+
return this.sqliteStore.getRecentRetrievalTraces(limit);
|
|
6441
|
+
}
|
|
4893
6442
|
async getStats() {
|
|
4894
6443
|
await this.initialize();
|
|
4895
6444
|
const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
|
|
@@ -5106,6 +6655,31 @@ var MemoryService = class {
|
|
|
5106
6655
|
return [];
|
|
5107
6656
|
return this.consolidatedStore.getAll({ limit });
|
|
5108
6657
|
}
|
|
6658
|
+
/**
|
|
6659
|
+
* Extract topic keywords from event content (markdown headings and key terms)
|
|
6660
|
+
*/
|
|
6661
|
+
extractTopicsFromContent(content) {
|
|
6662
|
+
const topics = /* @__PURE__ */ new Set();
|
|
6663
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm);
|
|
6664
|
+
if (headings) {
|
|
6665
|
+
for (const h of headings.slice(0, 5)) {
|
|
6666
|
+
const text = h.replace(/^#+\s+/, "").replace(/[*_`#]/g, "").trim();
|
|
6667
|
+
if (text.length > 2 && text.length < 50) {
|
|
6668
|
+
topics.add(text);
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
|
|
6673
|
+
if (boldTerms) {
|
|
6674
|
+
for (const b of boldTerms.slice(0, 5)) {
|
|
6675
|
+
const text = b.replace(/\*\*/g, "").trim();
|
|
6676
|
+
if (text.length > 2 && text.length < 30) {
|
|
6677
|
+
topics.add(text);
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
return Array.from(topics).slice(0, 5);
|
|
6682
|
+
}
|
|
5109
6683
|
/**
|
|
5110
6684
|
* Increment access count for memories that were used in prompts
|
|
5111
6685
|
*/
|
|
@@ -5129,8 +6703,7 @@ var MemoryService = class {
|
|
|
5129
6703
|
return events.map((event) => ({
|
|
5130
6704
|
memoryId: event.id,
|
|
5131
6705
|
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5132
|
-
topics:
|
|
5133
|
-
// Could extract topics from content if needed
|
|
6706
|
+
topics: this.extractTopicsFromContent(event.content),
|
|
5134
6707
|
accessCount: event.access_count || 0,
|
|
5135
6708
|
lastAccessed: event.last_accessed_at || null,
|
|
5136
6709
|
confidence: 1,
|
|
@@ -5151,6 +6724,34 @@ var MemoryService = class {
|
|
|
5151
6724
|
}
|
|
5152
6725
|
return [];
|
|
5153
6726
|
}
|
|
6727
|
+
/**
|
|
6728
|
+
* Record a memory retrieval for helpfulness tracking
|
|
6729
|
+
*/
|
|
6730
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
6731
|
+
await this.initialize();
|
|
6732
|
+
await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
|
|
6733
|
+
}
|
|
6734
|
+
/**
|
|
6735
|
+
* Evaluate helpfulness of retrievals in a session (called at session end)
|
|
6736
|
+
*/
|
|
6737
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
6738
|
+
await this.initialize();
|
|
6739
|
+
await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
|
|
6740
|
+
}
|
|
6741
|
+
/**
|
|
6742
|
+
* Get most helpful memories ranked by helpfulness score
|
|
6743
|
+
*/
|
|
6744
|
+
async getHelpfulMemories(limit = 10) {
|
|
6745
|
+
await this.initialize();
|
|
6746
|
+
return this.sqliteStore.getHelpfulMemories(limit);
|
|
6747
|
+
}
|
|
6748
|
+
/**
|
|
6749
|
+
* Get helpfulness statistics for dashboard
|
|
6750
|
+
*/
|
|
6751
|
+
async getHelpfulnessStats() {
|
|
6752
|
+
await this.initialize();
|
|
6753
|
+
return this.sqliteStore.getHelpfulnessStats();
|
|
6754
|
+
}
|
|
5154
6755
|
/**
|
|
5155
6756
|
* Mark a consolidated memory as accessed
|
|
5156
6757
|
*/
|
|
@@ -5214,6 +6815,44 @@ var MemoryService = class {
|
|
|
5214
6815
|
lastConsolidation
|
|
5215
6816
|
};
|
|
5216
6817
|
}
|
|
6818
|
+
// ============================================================
|
|
6819
|
+
// Turn Grouping Methods
|
|
6820
|
+
// ============================================================
|
|
6821
|
+
/**
|
|
6822
|
+
* Get events grouped by turn for a session
|
|
6823
|
+
*/
|
|
6824
|
+
async getSessionTurns(sessionId, options) {
|
|
6825
|
+
await this.initialize();
|
|
6826
|
+
return this.sqliteStore.getSessionTurns(sessionId, options);
|
|
6827
|
+
}
|
|
6828
|
+
/**
|
|
6829
|
+
* Get all events for a specific turn
|
|
6830
|
+
*/
|
|
6831
|
+
async getEventsByTurn(turnId) {
|
|
6832
|
+
await this.initialize();
|
|
6833
|
+
return this.sqliteStore.getEventsByTurn(turnId);
|
|
6834
|
+
}
|
|
6835
|
+
/**
|
|
6836
|
+
* Count total turns for a session
|
|
6837
|
+
*/
|
|
6838
|
+
async countSessionTurns(sessionId) {
|
|
6839
|
+
await this.initialize();
|
|
6840
|
+
return this.sqliteStore.countSessionTurns(sessionId);
|
|
6841
|
+
}
|
|
6842
|
+
/**
|
|
6843
|
+
* Backfill turn_ids from metadata for events stored before the migration
|
|
6844
|
+
*/
|
|
6845
|
+
async backfillTurnIds() {
|
|
6846
|
+
await this.initialize();
|
|
6847
|
+
return this.sqliteStore.backfillTurnIds();
|
|
6848
|
+
}
|
|
6849
|
+
/**
|
|
6850
|
+
* Delete all events for a session (for force reimport)
|
|
6851
|
+
*/
|
|
6852
|
+
async deleteSessionEvents(sessionId) {
|
|
6853
|
+
await this.initialize();
|
|
6854
|
+
return this.sqliteStore.deleteSessionEvents(sessionId);
|
|
6855
|
+
}
|
|
5217
6856
|
/**
|
|
5218
6857
|
* Format Endless Mode context for Claude
|
|
5219
6858
|
*/
|
|
@@ -5290,7 +6929,7 @@ var MemoryService = class {
|
|
|
5290
6929
|
*/
|
|
5291
6930
|
expandPath(p) {
|
|
5292
6931
|
if (p.startsWith("~")) {
|
|
5293
|
-
return
|
|
6932
|
+
return path3.join(os.homedir(), p.slice(1));
|
|
5294
6933
|
}
|
|
5295
6934
|
return p;
|
|
5296
6935
|
}
|
|
@@ -5326,6 +6965,7 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
5326
6965
|
serviceCache.set(hash, new MemoryService({
|
|
5327
6966
|
storagePath,
|
|
5328
6967
|
projectHash: hash,
|
|
6968
|
+
projectPath,
|
|
5329
6969
|
// Override shared store config - hooks don't need DuckDB
|
|
5330
6970
|
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
5331
6971
|
analyticsEnabled: false
|
|
@@ -5336,16 +6976,52 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
5336
6976
|
}
|
|
5337
6977
|
|
|
5338
6978
|
// src/services/session-history-importer.ts
|
|
5339
|
-
import * as
|
|
5340
|
-
import * as
|
|
6979
|
+
import * as fs5 from "fs";
|
|
6980
|
+
import * as path4 from "path";
|
|
5341
6981
|
import * as os2 from "os";
|
|
5342
6982
|
import * as readline from "readline";
|
|
6983
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
6984
|
+
function classifyEntry(entry) {
|
|
6985
|
+
if (entry.type !== "user" && entry.type !== "assistant") {
|
|
6986
|
+
return "skip";
|
|
6987
|
+
}
|
|
6988
|
+
const content = entry.message?.content;
|
|
6989
|
+
if (!content)
|
|
6990
|
+
return "skip";
|
|
6991
|
+
if (entry.type === "user") {
|
|
6992
|
+
if (typeof content === "string")
|
|
6993
|
+
return "user_prompt";
|
|
6994
|
+
if (Array.isArray(content)) {
|
|
6995
|
+
const hasToolResult = content.some((b) => b.type === "tool_result");
|
|
6996
|
+
if (hasToolResult)
|
|
6997
|
+
return "tool_result";
|
|
6998
|
+
const hasText = content.some((b) => b.type === "text" && b.text);
|
|
6999
|
+
if (hasText)
|
|
7000
|
+
return "user_prompt";
|
|
7001
|
+
}
|
|
7002
|
+
return "skip";
|
|
7003
|
+
}
|
|
7004
|
+
if (Array.isArray(content)) {
|
|
7005
|
+
const hasToolUse = content.some((b) => b.type === "tool_use");
|
|
7006
|
+
if (hasToolUse)
|
|
7007
|
+
return "tool_use";
|
|
7008
|
+
const hasText = content.some((b) => b.type === "text" && b.text);
|
|
7009
|
+
if (hasText)
|
|
7010
|
+
return "agent_text";
|
|
7011
|
+
const hasThinking = content.some((b) => b.type === "thinking");
|
|
7012
|
+
if (hasThinking)
|
|
7013
|
+
return "thinking";
|
|
7014
|
+
} else if (typeof content === "string" && content.length > 0) {
|
|
7015
|
+
return "agent_text";
|
|
7016
|
+
}
|
|
7017
|
+
return "skip";
|
|
7018
|
+
}
|
|
5343
7019
|
var SessionHistoryImporter = class {
|
|
5344
7020
|
memoryService;
|
|
5345
7021
|
claudeDir;
|
|
5346
7022
|
constructor(memoryService) {
|
|
5347
7023
|
this.memoryService = memoryService;
|
|
5348
|
-
this.claudeDir =
|
|
7024
|
+
this.claudeDir = path4.join(os2.homedir(), ".claude");
|
|
5349
7025
|
}
|
|
5350
7026
|
/**
|
|
5351
7027
|
* Import all sessions from a project
|
|
@@ -5359,6 +7035,8 @@ var SessionHistoryImporter = class {
|
|
|
5359
7035
|
skippedDuplicates: 0,
|
|
5360
7036
|
errors: []
|
|
5361
7037
|
};
|
|
7038
|
+
const onProgress = options.onProgress;
|
|
7039
|
+
onProgress?.({ phase: "scan", message: "Scanning for session files..." });
|
|
5362
7040
|
const projectDir = await this.findProjectDir(projectPath);
|
|
5363
7041
|
if (!projectDir) {
|
|
5364
7042
|
result.errors.push(`Project directory not found for: ${projectPath}`);
|
|
@@ -5366,16 +7044,29 @@ var SessionHistoryImporter = class {
|
|
|
5366
7044
|
}
|
|
5367
7045
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5368
7046
|
result.totalSessions = sessionFiles.length;
|
|
7047
|
+
onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} sessions in ${path4.basename(projectDir)}` });
|
|
5369
7048
|
if (options.verbose) {
|
|
5370
7049
|
console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
|
|
5371
7050
|
}
|
|
5372
|
-
for (
|
|
7051
|
+
for (let i = 0; i < sessionFiles.length; i++) {
|
|
7052
|
+
const sessionFile = sessionFiles[i];
|
|
5373
7053
|
try {
|
|
5374
|
-
|
|
7054
|
+
onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
|
|
7055
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
7056
|
+
...options,
|
|
7057
|
+
_sessionIndex: i
|
|
7058
|
+
});
|
|
5375
7059
|
result.totalMessages += sessionResult.totalMessages;
|
|
5376
7060
|
result.importedPrompts += sessionResult.importedPrompts;
|
|
5377
7061
|
result.importedResponses += sessionResult.importedResponses;
|
|
5378
7062
|
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
7063
|
+
onProgress?.({
|
|
7064
|
+
phase: "session-done",
|
|
7065
|
+
sessionIndex: i,
|
|
7066
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
7067
|
+
importedResponses: sessionResult.importedResponses,
|
|
7068
|
+
skipped: sessionResult.skippedDuplicates
|
|
7069
|
+
});
|
|
5379
7070
|
} catch (error) {
|
|
5380
7071
|
result.errors.push(`Failed to import ${sessionFile}: ${error}`);
|
|
5381
7072
|
}
|
|
@@ -5394,60 +7085,105 @@ var SessionHistoryImporter = class {
|
|
|
5394
7085
|
skippedDuplicates: 0,
|
|
5395
7086
|
errors: []
|
|
5396
7087
|
};
|
|
5397
|
-
if (!
|
|
7088
|
+
if (!fs5.existsSync(filePath)) {
|
|
5398
7089
|
result.errors.push(`File not found: ${filePath}`);
|
|
5399
7090
|
return result;
|
|
5400
7091
|
}
|
|
5401
|
-
const sessionId =
|
|
7092
|
+
const sessionId = path4.basename(filePath, ".jsonl");
|
|
7093
|
+
if (options.force) {
|
|
7094
|
+
const deleted = await this.memoryService.deleteSessionEvents(sessionId);
|
|
7095
|
+
if (options.verbose && deleted > 0) {
|
|
7096
|
+
console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
5402
7099
|
await this.memoryService.startSession(sessionId, options.projectPath);
|
|
5403
|
-
const fileStream =
|
|
7100
|
+
const fileStream = fs5.createReadStream(filePath);
|
|
5404
7101
|
const rl = readline.createInterface({
|
|
5405
7102
|
input: fileStream,
|
|
5406
7103
|
crlfDelay: Infinity
|
|
5407
7104
|
});
|
|
5408
7105
|
let lineCount = 0;
|
|
5409
7106
|
const limit = options.limit || Infinity;
|
|
7107
|
+
const onProgress = options.onProgress;
|
|
7108
|
+
const sessionIndex = options._sessionIndex ?? 0;
|
|
7109
|
+
let lastProgressAt = 0;
|
|
7110
|
+
let currentTurnId = null;
|
|
7111
|
+
let textBuffer = [];
|
|
7112
|
+
let lastTimestamp;
|
|
7113
|
+
const flushTextBuffer = async () => {
|
|
7114
|
+
if (textBuffer.length === 0 || !currentTurnId)
|
|
7115
|
+
return;
|
|
7116
|
+
const substantive = textBuffer.filter((t) => t.length >= 100);
|
|
7117
|
+
const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
|
|
7118
|
+
if (!merged) {
|
|
7119
|
+
textBuffer = [];
|
|
7120
|
+
return;
|
|
7121
|
+
}
|
|
7122
|
+
const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
|
|
7123
|
+
const appendResult = await this.memoryService.storeAgentResponse(
|
|
7124
|
+
sessionId,
|
|
7125
|
+
truncated,
|
|
7126
|
+
{ importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
|
|
7127
|
+
);
|
|
7128
|
+
if (appendResult.isDuplicate) {
|
|
7129
|
+
result.skippedDuplicates++;
|
|
7130
|
+
} else {
|
|
7131
|
+
result.importedResponses++;
|
|
7132
|
+
}
|
|
7133
|
+
lineCount++;
|
|
7134
|
+
textBuffer = [];
|
|
7135
|
+
};
|
|
5410
7136
|
for await (const line of rl) {
|
|
5411
7137
|
if (lineCount >= limit)
|
|
5412
7138
|
break;
|
|
5413
7139
|
try {
|
|
5414
7140
|
const entry = JSON.parse(line);
|
|
5415
7141
|
result.totalMessages++;
|
|
5416
|
-
|
|
7142
|
+
const msgClass = classifyEntry(entry);
|
|
7143
|
+
if (msgClass === "user_prompt") {
|
|
7144
|
+
await flushTextBuffer();
|
|
5417
7145
|
const content = this.extractContent(entry);
|
|
5418
7146
|
if (!content)
|
|
5419
7147
|
continue;
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
}
|
|
5431
|
-
} else if (entry.type === "assistant") {
|
|
5432
|
-
const truncatedContent = content.length > 5e3 ? content.slice(0, 5e3) + "...[truncated]" : content;
|
|
5433
|
-
const appendResult = await this.memoryService.storeAgentResponse(
|
|
5434
|
-
sessionId,
|
|
5435
|
-
truncatedContent,
|
|
5436
|
-
{ importedFrom: filePath, originalTimestamp: entry.timestamp }
|
|
5437
|
-
);
|
|
5438
|
-
if (appendResult.isDuplicate) {
|
|
5439
|
-
result.skippedDuplicates++;
|
|
5440
|
-
} else {
|
|
5441
|
-
result.importedResponses++;
|
|
5442
|
-
}
|
|
7148
|
+
currentTurnId = randomUUID9();
|
|
7149
|
+
const appendResult = await this.memoryService.storeUserPrompt(
|
|
7150
|
+
sessionId,
|
|
7151
|
+
content,
|
|
7152
|
+
{ importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
|
|
7153
|
+
);
|
|
7154
|
+
if (appendResult.isDuplicate) {
|
|
7155
|
+
result.skippedDuplicates++;
|
|
7156
|
+
} else {
|
|
7157
|
+
result.importedPrompts++;
|
|
5443
7158
|
}
|
|
5444
7159
|
lineCount++;
|
|
7160
|
+
} else if (msgClass === "agent_text") {
|
|
7161
|
+
const content = this.extractContent(entry);
|
|
7162
|
+
if (content) {
|
|
7163
|
+
textBuffer.push(content);
|
|
7164
|
+
lastTimestamp = entry.timestamp;
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
const now = Date.now();
|
|
7168
|
+
if (now - lastProgressAt > 200) {
|
|
7169
|
+
lastProgressAt = now;
|
|
7170
|
+
onProgress?.({
|
|
7171
|
+
phase: "session-progress",
|
|
7172
|
+
sessionIndex,
|
|
7173
|
+
messagesProcessed: result.totalMessages,
|
|
7174
|
+
imported: result.importedPrompts + result.importedResponses,
|
|
7175
|
+
skipped: result.skippedDuplicates
|
|
7176
|
+
});
|
|
5445
7177
|
}
|
|
5446
7178
|
} catch (parseError) {
|
|
5447
7179
|
result.errors.push(`Parse error on line: ${parseError}`);
|
|
5448
7180
|
}
|
|
5449
7181
|
}
|
|
7182
|
+
await flushTextBuffer();
|
|
5450
7183
|
await this.memoryService.endSession(sessionId);
|
|
7184
|
+
if (options.projectPath) {
|
|
7185
|
+
registerSession(sessionId, options.projectPath);
|
|
7186
|
+
}
|
|
5451
7187
|
if (options.verbose) {
|
|
5452
7188
|
console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
|
|
5453
7189
|
}
|
|
@@ -5465,29 +7201,46 @@ var SessionHistoryImporter = class {
|
|
|
5465
7201
|
skippedDuplicates: 0,
|
|
5466
7202
|
errors: []
|
|
5467
7203
|
};
|
|
5468
|
-
const
|
|
5469
|
-
|
|
7204
|
+
const onProgress = options.onProgress;
|
|
7205
|
+
const projectsDir = path4.join(this.claudeDir, "projects");
|
|
7206
|
+
if (!fs5.existsSync(projectsDir)) {
|
|
5470
7207
|
result.errors.push(`Projects directory not found: ${projectsDir}`);
|
|
5471
7208
|
return result;
|
|
5472
7209
|
}
|
|
5473
|
-
|
|
7210
|
+
onProgress?.({ phase: "scan", message: "Scanning all projects..." });
|
|
7211
|
+
const projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
|
|
7212
|
+
const allSessionFiles = [];
|
|
7213
|
+
for (const projectDir of projectDirs) {
|
|
7214
|
+
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
7215
|
+
allSessionFiles.push(...sessionFiles);
|
|
7216
|
+
}
|
|
7217
|
+
onProgress?.({ phase: "scan", message: `Found ${allSessionFiles.length} sessions across ${projectDirs.length} projects` });
|
|
5474
7218
|
if (options.verbose) {
|
|
5475
|
-
console.log(`Found ${projectDirs.length} project directories`);
|
|
7219
|
+
console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
|
|
5476
7220
|
}
|
|
5477
|
-
for (
|
|
7221
|
+
for (let i = 0; i < allSessionFiles.length; i++) {
|
|
7222
|
+
const sessionFile = allSessionFiles[i];
|
|
5478
7223
|
try {
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
7224
|
+
onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
|
|
7225
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
7226
|
+
...options,
|
|
7227
|
+
_sessionIndex: i
|
|
7228
|
+
});
|
|
7229
|
+
result.totalSessions++;
|
|
7230
|
+
result.totalMessages += sessionResult.totalMessages;
|
|
7231
|
+
result.importedPrompts += sessionResult.importedPrompts;
|
|
7232
|
+
result.importedResponses += sessionResult.importedResponses;
|
|
7233
|
+
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
7234
|
+
result.errors.push(...sessionResult.errors);
|
|
7235
|
+
onProgress?.({
|
|
7236
|
+
phase: "session-done",
|
|
7237
|
+
sessionIndex: i,
|
|
7238
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
7239
|
+
importedResponses: sessionResult.importedResponses,
|
|
7240
|
+
skipped: sessionResult.skippedDuplicates
|
|
7241
|
+
});
|
|
5489
7242
|
} catch (error) {
|
|
5490
|
-
result.errors.push(`Failed to process ${
|
|
7243
|
+
result.errors.push(`Failed to process ${sessionFile}: ${error}`);
|
|
5491
7244
|
}
|
|
5492
7245
|
}
|
|
5493
7246
|
return result;
|
|
@@ -5496,14 +7249,14 @@ var SessionHistoryImporter = class {
|
|
|
5496
7249
|
* Find project directory from project path
|
|
5497
7250
|
*/
|
|
5498
7251
|
async findProjectDir(projectPath) {
|
|
5499
|
-
const projectsDir =
|
|
5500
|
-
if (!
|
|
7252
|
+
const projectsDir = path4.join(this.claudeDir, "projects");
|
|
7253
|
+
if (!fs5.existsSync(projectsDir)) {
|
|
5501
7254
|
return null;
|
|
5502
7255
|
}
|
|
5503
|
-
const projectDirs =
|
|
7256
|
+
const projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
|
|
5504
7257
|
const normalizedPath = projectPath.replace(/\//g, "-").replace(/^-/, "");
|
|
5505
7258
|
for (const dir of projectDirs) {
|
|
5506
|
-
const dirName =
|
|
7259
|
+
const dirName = path4.basename(dir);
|
|
5507
7260
|
if (dirName.includes(normalizedPath) || normalizedPath.includes(dirName)) {
|
|
5508
7261
|
return dir;
|
|
5509
7262
|
}
|
|
@@ -5514,10 +7267,10 @@ var SessionHistoryImporter = class {
|
|
|
5514
7267
|
* Find all JSONL session files in a directory
|
|
5515
7268
|
*/
|
|
5516
7269
|
async findSessionFiles(dir) {
|
|
5517
|
-
if (!
|
|
7270
|
+
if (!fs5.existsSync(dir)) {
|
|
5518
7271
|
return [];
|
|
5519
7272
|
}
|
|
5520
|
-
return
|
|
7273
|
+
return fs5.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path4.join(dir, name)).filter((p) => fs5.statSync(p).isFile());
|
|
5521
7274
|
}
|
|
5522
7275
|
/**
|
|
5523
7276
|
* Extract text content from Claude message
|
|
@@ -5548,17 +7301,17 @@ var SessionHistoryImporter = class {
|
|
|
5548
7301
|
projectDirs = [projectDir];
|
|
5549
7302
|
}
|
|
5550
7303
|
} else {
|
|
5551
|
-
const projectsDir =
|
|
5552
|
-
if (
|
|
5553
|
-
projectDirs =
|
|
7304
|
+
const projectsDir = path4.join(this.claudeDir, "projects");
|
|
7305
|
+
if (fs5.existsSync(projectsDir)) {
|
|
7306
|
+
projectDirs = fs5.readdirSync(projectsDir).map((name) => path4.join(projectsDir, name)).filter((p) => fs5.statSync(p).isDirectory());
|
|
5554
7307
|
}
|
|
5555
7308
|
}
|
|
5556
7309
|
for (const projectDir of projectDirs) {
|
|
5557
7310
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5558
7311
|
for (const filePath of sessionFiles) {
|
|
5559
|
-
const stats =
|
|
7312
|
+
const stats = fs5.statSync(filePath);
|
|
5560
7313
|
sessions.push({
|
|
5561
|
-
sessionId:
|
|
7314
|
+
sessionId: path4.basename(filePath, ".jsonl"),
|
|
5562
7315
|
filePath,
|
|
5563
7316
|
size: stats.size,
|
|
5564
7317
|
modifiedAt: stats.mtime
|
|
@@ -5573,25 +7326,448 @@ function createSessionHistoryImporter(memoryService) {
|
|
|
5573
7326
|
return new SessionHistoryImporter(memoryService);
|
|
5574
7327
|
}
|
|
5575
7328
|
|
|
7329
|
+
// src/services/bootstrap-organizer.ts
|
|
7330
|
+
import * as fs6 from "node:fs";
|
|
7331
|
+
import * as path5 from "node:path";
|
|
7332
|
+
import { execSync } from "node:child_process";
|
|
7333
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", "memory"]);
|
|
7334
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7335
|
+
".ts",
|
|
7336
|
+
".tsx",
|
|
7337
|
+
".js",
|
|
7338
|
+
".jsx",
|
|
7339
|
+
".mjs",
|
|
7340
|
+
".cjs",
|
|
7341
|
+
".py",
|
|
7342
|
+
".go",
|
|
7343
|
+
".rs",
|
|
7344
|
+
".java",
|
|
7345
|
+
".kt",
|
|
7346
|
+
".swift",
|
|
7347
|
+
".rb",
|
|
7348
|
+
".php",
|
|
7349
|
+
".cs",
|
|
7350
|
+
".scala",
|
|
7351
|
+
".sh",
|
|
7352
|
+
".zsh",
|
|
7353
|
+
".yaml",
|
|
7354
|
+
".yml",
|
|
7355
|
+
".json",
|
|
7356
|
+
".sql",
|
|
7357
|
+
".md"
|
|
7358
|
+
]);
|
|
7359
|
+
function safeRel(base, target) {
|
|
7360
|
+
return path5.relative(base, target).replaceAll("\\", "/");
|
|
7361
|
+
}
|
|
7362
|
+
function mkdirp(dir) {
|
|
7363
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
7364
|
+
}
|
|
7365
|
+
function walkCodeFiles(root) {
|
|
7366
|
+
const out = [];
|
|
7367
|
+
function walk(dir) {
|
|
7368
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
7369
|
+
for (const e of entries) {
|
|
7370
|
+
const full = path5.join(dir, e.name);
|
|
7371
|
+
if (e.isDirectory()) {
|
|
7372
|
+
if (!EXCLUDED_DIRS.has(e.name))
|
|
7373
|
+
walk(full);
|
|
7374
|
+
} else if (e.isFile()) {
|
|
7375
|
+
const ext = path5.extname(e.name).toLowerCase();
|
|
7376
|
+
if (CODE_EXTENSIONS.has(ext))
|
|
7377
|
+
out.push(full);
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
walk(root);
|
|
7382
|
+
return out.sort();
|
|
7383
|
+
}
|
|
7384
|
+
function detectLanguage(file) {
|
|
7385
|
+
const ext = path5.extname(file).toLowerCase();
|
|
7386
|
+
const map = {
|
|
7387
|
+
".ts": "TypeScript",
|
|
7388
|
+
".tsx": "TypeScript",
|
|
7389
|
+
".js": "JavaScript",
|
|
7390
|
+
".jsx": "JavaScript",
|
|
7391
|
+
".mjs": "JavaScript",
|
|
7392
|
+
".cjs": "JavaScript",
|
|
7393
|
+
".py": "Python",
|
|
7394
|
+
".go": "Go",
|
|
7395
|
+
".rs": "Rust",
|
|
7396
|
+
".java": "Java",
|
|
7397
|
+
".kt": "Kotlin",
|
|
7398
|
+
".swift": "Swift",
|
|
7399
|
+
".rb": "Ruby",
|
|
7400
|
+
".php": "PHP",
|
|
7401
|
+
".cs": "C#",
|
|
7402
|
+
".scala": "Scala",
|
|
7403
|
+
".sh": "Shell",
|
|
7404
|
+
".zsh": "Shell",
|
|
7405
|
+
".yaml": "YAML",
|
|
7406
|
+
".yml": "YAML",
|
|
7407
|
+
".json": "JSON",
|
|
7408
|
+
".sql": "SQL",
|
|
7409
|
+
".md": "Markdown"
|
|
7410
|
+
};
|
|
7411
|
+
return map[ext] || "Other";
|
|
7412
|
+
}
|
|
7413
|
+
function summarizeModules(repoPath, files) {
|
|
7414
|
+
const modules = /* @__PURE__ */ new Map();
|
|
7415
|
+
for (const abs of files) {
|
|
7416
|
+
const rel = safeRel(repoPath, abs);
|
|
7417
|
+
const seg = rel.split("/").filter(Boolean);
|
|
7418
|
+
const top = seg[0] || "root";
|
|
7419
|
+
if (!modules.has(top))
|
|
7420
|
+
modules.set(top, { files: [], langs: /* @__PURE__ */ new Map() });
|
|
7421
|
+
const bucket = modules.get(top);
|
|
7422
|
+
bucket.files.push(rel);
|
|
7423
|
+
const lang = detectLanguage(abs);
|
|
7424
|
+
bucket.langs.set(lang, (bucket.langs.get(lang) || 0) + 1);
|
|
7425
|
+
}
|
|
7426
|
+
return [...modules.entries()].map(([name, data]) => ({
|
|
7427
|
+
name,
|
|
7428
|
+
root: name,
|
|
7429
|
+
fileCount: data.files.length,
|
|
7430
|
+
languages: [...data.langs.entries()].sort((a, b) => b[1] - a[1]).map(([l]) => l).slice(0, 5),
|
|
7431
|
+
entryCandidates: data.files.filter((f) => /(index|main|app|server|cli)\./i.test(path5.basename(f))).slice(0, 10)
|
|
7432
|
+
})).sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
|
|
7433
|
+
}
|
|
7434
|
+
function runGit(repoPath, command) {
|
|
7435
|
+
return execSync(`git -C ${JSON.stringify(repoPath)} ${command}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7436
|
+
}
|
|
7437
|
+
function getGitCommits(repoPath, since = "180 days ago", maxCommits = 1e3) {
|
|
7438
|
+
try {
|
|
7439
|
+
const raw = runGit(
|
|
7440
|
+
repoPath,
|
|
7441
|
+
`log --since=${JSON.stringify(since)} -n ${Math.max(1, maxCommits)} --date=short --pretty=format:%H%x09%ad%x09%an%x09%s --name-only --reverse`
|
|
7442
|
+
);
|
|
7443
|
+
const lines = raw.split(/\r?\n/);
|
|
7444
|
+
const commits = [];
|
|
7445
|
+
let current = null;
|
|
7446
|
+
for (const line of lines) {
|
|
7447
|
+
if (!line.trim()) {
|
|
7448
|
+
if (current) {
|
|
7449
|
+
commits.push(current);
|
|
7450
|
+
current = null;
|
|
7451
|
+
}
|
|
7452
|
+
continue;
|
|
7453
|
+
}
|
|
7454
|
+
if (line.includes(" ") && line.split(" ").length >= 4) {
|
|
7455
|
+
if (current)
|
|
7456
|
+
commits.push(current);
|
|
7457
|
+
const [hash, date, author, ...subjectRest] = line.split(" ");
|
|
7458
|
+
current = { hash, date, author, subject: subjectRest.join(" ").trim(), files: [] };
|
|
7459
|
+
} else if (current) {
|
|
7460
|
+
current.files.push(line.trim());
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
if (current)
|
|
7464
|
+
commits.push(current);
|
|
7465
|
+
return commits;
|
|
7466
|
+
} catch {
|
|
7467
|
+
return [];
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
function extractDecisions(commits) {
|
|
7471
|
+
const decisionPattern = /(refactor|migrate|deprecat|remove|replace|introduce|adopt|switch|upgrade|breaking|architecture|feat|fix)/i;
|
|
7472
|
+
return commits.filter((c) => decisionPattern.test(c.subject));
|
|
7473
|
+
}
|
|
7474
|
+
function buildTimeline(commits) {
|
|
7475
|
+
const timeline = /* @__PURE__ */ new Map();
|
|
7476
|
+
for (const c of commits) {
|
|
7477
|
+
const key = (c.date || "").slice(0, 7) || "unknown";
|
|
7478
|
+
if (!timeline.has(key))
|
|
7479
|
+
timeline.set(key, []);
|
|
7480
|
+
timeline.get(key).push(c);
|
|
7481
|
+
}
|
|
7482
|
+
return new Map([...timeline.entries()].sort((a, b) => a[0].localeCompare(b[0])));
|
|
7483
|
+
}
|
|
7484
|
+
function buildGlossary(files) {
|
|
7485
|
+
const stop = /* @__PURE__ */ new Set(["src", "test", "dist", "lib", "core", "index", "main", "app", "server", "client", "utils"]);
|
|
7486
|
+
const freq = /* @__PURE__ */ new Map();
|
|
7487
|
+
for (const f of files) {
|
|
7488
|
+
const base = path5.basename(f, path5.extname(f));
|
|
7489
|
+
const tokens = base.split(/[^a-zA-Z0-9]+/).flatMap((t) => t.split(/(?=[A-Z])/)).map((t) => t.toLowerCase()).filter((t) => t.length >= 3 && !stop.has(t));
|
|
7490
|
+
for (const t of tokens)
|
|
7491
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
7492
|
+
}
|
|
7493
|
+
return [...freq.entries()].filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 80).map(([term]) => term);
|
|
7494
|
+
}
|
|
7495
|
+
function writeFile(filePath, content) {
|
|
7496
|
+
mkdirp(path5.dirname(filePath));
|
|
7497
|
+
fs6.writeFileSync(filePath, content, "utf8");
|
|
7498
|
+
}
|
|
7499
|
+
function confidenceByEvidence(sourceCount) {
|
|
7500
|
+
if (sourceCount >= 3)
|
|
7501
|
+
return "high";
|
|
7502
|
+
if (sourceCount >= 1)
|
|
7503
|
+
return "mid";
|
|
7504
|
+
return "low";
|
|
7505
|
+
}
|
|
7506
|
+
function sourceLine(source) {
|
|
7507
|
+
return `- source: ${source}`;
|
|
7508
|
+
}
|
|
7509
|
+
function loadExistingManifest(outDir) {
|
|
7510
|
+
try {
|
|
7511
|
+
const p = path5.join(outDir, "sources", "manifest.json");
|
|
7512
|
+
if (!fs6.existsSync(p))
|
|
7513
|
+
return null;
|
|
7514
|
+
const data = JSON.parse(fs6.readFileSync(p, "utf8"));
|
|
7515
|
+
return data;
|
|
7516
|
+
} catch {
|
|
7517
|
+
return null;
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
function listMarkdownOutputs(outDir) {
|
|
7521
|
+
const out = [];
|
|
7522
|
+
const stack = [outDir];
|
|
7523
|
+
while (stack.length) {
|
|
7524
|
+
const dir = stack.pop();
|
|
7525
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
7526
|
+
const full = path5.join(dir, entry.name);
|
|
7527
|
+
if (entry.isDirectory())
|
|
7528
|
+
stack.push(full);
|
|
7529
|
+
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
7530
|
+
out.push(full);
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
7534
|
+
}
|
|
7535
|
+
async function bootstrapKnowledgeBase(options) {
|
|
7536
|
+
const repoPath = path5.resolve(options.repoPath);
|
|
7537
|
+
const outDir = path5.resolve(options.outDir);
|
|
7538
|
+
const maxCommits = options.maxCommits ?? 1e3;
|
|
7539
|
+
const existingManifest = options.incremental ? loadExistingManifest(outDir) : null;
|
|
7540
|
+
const incrementalSince = existingManifest?.lastCommitDate || existingManifest?.generatedAt;
|
|
7541
|
+
const since = options.since || incrementalSince || "180 days ago";
|
|
7542
|
+
const codeFiles = walkCodeFiles(repoPath);
|
|
7543
|
+
const modules = summarizeModules(repoPath, codeFiles);
|
|
7544
|
+
const commits = getGitCommits(repoPath, since, maxCommits);
|
|
7545
|
+
const decisions = extractDecisions(commits);
|
|
7546
|
+
const timeline = buildTimeline(commits);
|
|
7547
|
+
const glossary = buildGlossary(codeFiles);
|
|
7548
|
+
const generatedFiles = [];
|
|
7549
|
+
const sections = {
|
|
7550
|
+
overview: path5.join(outDir, "overview"),
|
|
7551
|
+
modules: path5.join(outDir, "modules"),
|
|
7552
|
+
decisions: path5.join(outDir, "decisions"),
|
|
7553
|
+
timeline: path5.join(outDir, "timeline"),
|
|
7554
|
+
glossary: path5.join(outDir, "glossary"),
|
|
7555
|
+
sources: path5.join(outDir, "sources")
|
|
7556
|
+
};
|
|
7557
|
+
for (const sectionDir of Object.values(sections)) {
|
|
7558
|
+
mkdirp(sectionDir);
|
|
7559
|
+
}
|
|
7560
|
+
const overviewPath = path5.join(sections.overview, "overview.md");
|
|
7561
|
+
const overview = [
|
|
7562
|
+
"# Codebase Overview",
|
|
7563
|
+
"",
|
|
7564
|
+
`- generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
7565
|
+
"- deterministicPipeline: true",
|
|
7566
|
+
`- repo: ${repoPath}`,
|
|
7567
|
+
`- filesAnalyzed: ${codeFiles.length}`,
|
|
7568
|
+
`- commitsAnalyzed: ${commits.length}`,
|
|
7569
|
+
`- confidence: ${confidenceByEvidence(modules.length > 0 ? 3 : 0)}`,
|
|
7570
|
+
"",
|
|
7571
|
+
"## Directory / Module Map",
|
|
7572
|
+
...modules.slice(0, 50).map((m) => `- ${m.name}: ${m.fileCount} files (${m.languages.join(", ") || "n/a"})`),
|
|
7573
|
+
"",
|
|
7574
|
+
"## Fact",
|
|
7575
|
+
"- Generated from deterministic file scan and git history parsing.",
|
|
7576
|
+
"",
|
|
7577
|
+
"## Inference",
|
|
7578
|
+
"- Module responsibilities should be reviewed by maintainers for nuanced boundaries.",
|
|
7579
|
+
"",
|
|
7580
|
+
"## Sources",
|
|
7581
|
+
sourceLine(`repo-scan:${repoPath}`),
|
|
7582
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
7583
|
+
""
|
|
7584
|
+
].join("\n");
|
|
7585
|
+
writeFile(overviewPath, overview);
|
|
7586
|
+
generatedFiles.push(overviewPath);
|
|
7587
|
+
const touchedRoots = new Set(
|
|
7588
|
+
commits.flatMap((c) => c.files).map((f) => f.split("/").filter(Boolean)[0]).filter(Boolean)
|
|
7589
|
+
);
|
|
7590
|
+
const moduleTargets = options.incremental && touchedRoots.size > 0 ? modules.filter((m) => touchedRoots.has(m.root)).slice(0, 200) : modules.slice(0, 200);
|
|
7591
|
+
for (const m of moduleTargets) {
|
|
7592
|
+
const relatedCommits = commits.filter((c) => c.files.some((f) => f.startsWith(`${m.root}/`))).slice(0, 15);
|
|
7593
|
+
const content = [
|
|
7594
|
+
`# Module: ${m.name}`,
|
|
7595
|
+
"",
|
|
7596
|
+
`- responsibility: inferred from top-level path \`${m.root}/\``,
|
|
7597
|
+
`- files: ${m.fileCount}`,
|
|
7598
|
+
`- languages: ${m.languages.join(", ") || "n/a"}`,
|
|
7599
|
+
`- confidence: ${confidenceByEvidence(relatedCommits.length)}`,
|
|
7600
|
+
"",
|
|
7601
|
+
"## Entry Candidates",
|
|
7602
|
+
...m.entryCandidates.length > 0 ? m.entryCandidates.map((f) => `- ${f}`) : ["- none detected"],
|
|
7603
|
+
"",
|
|
7604
|
+
"## Related Commits (recent sample)",
|
|
7605
|
+
...relatedCommits.length > 0 ? relatedCommits.map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`) : ["- none in selected range"],
|
|
7606
|
+
"",
|
|
7607
|
+
"## Sources",
|
|
7608
|
+
sourceLine(`repo-path:${m.root}/**`),
|
|
7609
|
+
...relatedCommits.map((c) => sourceLine(`commit:${c.hash}`)),
|
|
7610
|
+
""
|
|
7611
|
+
].join("\n");
|
|
7612
|
+
const modulePath = path5.join(sections.modules, `${m.name.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase()}.md`);
|
|
7613
|
+
writeFile(modulePath, content);
|
|
7614
|
+
generatedFiles.push(modulePath);
|
|
7615
|
+
}
|
|
7616
|
+
const decisionsPath = path5.join(sections.decisions, "decisions.md");
|
|
7617
|
+
const decisionsMd = [
|
|
7618
|
+
"# Decisions (extracted)",
|
|
7619
|
+
"",
|
|
7620
|
+
`- confidence: ${confidenceByEvidence(decisions.length)}`,
|
|
7621
|
+
"",
|
|
7622
|
+
...decisions.length > 0 ? decisions.slice(0, 500).map((d) => [
|
|
7623
|
+
`## ${d.date} | ${d.subject}`,
|
|
7624
|
+
"- status: active (inferred)",
|
|
7625
|
+
sourceLine(`commit:${d.hash}`),
|
|
7626
|
+
`- author: ${d.author}`,
|
|
7627
|
+
`- changedFiles: ${d.files.length}`,
|
|
7628
|
+
`- confidence: ${confidenceByEvidence(d.files.length > 0 ? 2 : 1)}`,
|
|
7629
|
+
""
|
|
7630
|
+
].join("\n")) : ["- No decision-like commits found in selected range.", ""],
|
|
7631
|
+
"## Sources",
|
|
7632
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
7633
|
+
""
|
|
7634
|
+
].join("\n");
|
|
7635
|
+
writeFile(decisionsPath, decisionsMd);
|
|
7636
|
+
generatedFiles.push(decisionsPath);
|
|
7637
|
+
const timelinePath = path5.join(sections.timeline, "timeline.md");
|
|
7638
|
+
const timelineMd = [
|
|
7639
|
+
"# Timeline",
|
|
7640
|
+
"",
|
|
7641
|
+
`- confidence: ${confidenceByEvidence(commits.length > 0 ? 2 : 0)}`,
|
|
7642
|
+
"",
|
|
7643
|
+
...[...timeline.entries()].flatMap(([month, list]) => [
|
|
7644
|
+
`## ${month}`,
|
|
7645
|
+
...list.slice(0, 40).map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`),
|
|
7646
|
+
""
|
|
7647
|
+
]),
|
|
7648
|
+
"## Sources",
|
|
7649
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
7650
|
+
""
|
|
7651
|
+
].join("\n");
|
|
7652
|
+
writeFile(timelinePath, timelineMd);
|
|
7653
|
+
generatedFiles.push(timelinePath);
|
|
7654
|
+
const glossaryPath = path5.join(sections.glossary, "glossary.md");
|
|
7655
|
+
const glossaryMd = [
|
|
7656
|
+
"# Glossary (auto-extracted)",
|
|
7657
|
+
"",
|
|
7658
|
+
`- confidence: ${confidenceByEvidence(glossary.length > 0 ? 1 : 0)}`,
|
|
7659
|
+
"",
|
|
7660
|
+
...glossary.map((t) => `- ${t}`),
|
|
7661
|
+
"",
|
|
7662
|
+
"## Sources",
|
|
7663
|
+
sourceLine(`repo-scan:${repoPath}`),
|
|
7664
|
+
""
|
|
7665
|
+
].join("\n");
|
|
7666
|
+
writeFile(glossaryPath, glossaryMd);
|
|
7667
|
+
generatedFiles.push(glossaryPath);
|
|
7668
|
+
const outputs = generatedFiles.map((f) => safeRel(outDir, f)).sort((a, b) => a.localeCompare(b));
|
|
7669
|
+
const allOutputs = listMarkdownOutputs(outDir).map((f) => safeRel(outDir, f));
|
|
7670
|
+
const sourceItems = [
|
|
7671
|
+
...codeFiles.slice(0, 200).map((f) => ({ type: "file", ref: safeRel(repoPath, f) })),
|
|
7672
|
+
...commits.slice(0, 400).map((c) => ({ type: "commit", ref: c.hash, date: c.date, subject: c.subject }))
|
|
7673
|
+
];
|
|
7674
|
+
const latestCommitDate = commits.length > 0 ? commits[commits.length - 1].date : void 0;
|
|
7675
|
+
const manifest = {
|
|
7676
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7677
|
+
deterministicPipeline: true,
|
|
7678
|
+
mode: options.incremental ? "incremental" : "full",
|
|
7679
|
+
repoPath,
|
|
7680
|
+
options: { since, maxCommits, incremental: Boolean(options.incremental) },
|
|
7681
|
+
stats: {
|
|
7682
|
+
filesAnalyzed: codeFiles.length,
|
|
7683
|
+
modules: modules.length,
|
|
7684
|
+
modulesGenerated: moduleTargets.length,
|
|
7685
|
+
commits: commits.length,
|
|
7686
|
+
decisions: decisions.length,
|
|
7687
|
+
glossaryTerms: glossary.length
|
|
7688
|
+
},
|
|
7689
|
+
lastCommitDate: latestCommitDate,
|
|
7690
|
+
outputs,
|
|
7691
|
+
allOutputs,
|
|
7692
|
+
sources: sourceItems
|
|
7693
|
+
};
|
|
7694
|
+
const manifestJsonPath = path5.join(sections.sources, "manifest.json");
|
|
7695
|
+
writeFile(manifestJsonPath, `${JSON.stringify(manifest, null, 2)}
|
|
7696
|
+
`);
|
|
7697
|
+
generatedFiles.push(manifestJsonPath);
|
|
7698
|
+
const manifestMdPath = path5.join(sections.sources, "manifest.md");
|
|
7699
|
+
const manifestMd = [
|
|
7700
|
+
"# Sources Manifest",
|
|
7701
|
+
"",
|
|
7702
|
+
"- deterministicPipeline: true",
|
|
7703
|
+
`- mode: ${options.incremental ? "incremental" : "full"}`,
|
|
7704
|
+
`- sourceCount: ${sourceItems.length}`,
|
|
7705
|
+
"",
|
|
7706
|
+
"## Outputs",
|
|
7707
|
+
...outputs.map((o) => `- ${o}`),
|
|
7708
|
+
"",
|
|
7709
|
+
"## Sources (sample)",
|
|
7710
|
+
...sourceItems.slice(0, 300).map((s) => `- ${s.type}:${s.ref}`),
|
|
7711
|
+
""
|
|
7712
|
+
].join("\n");
|
|
7713
|
+
writeFile(manifestMdPath, manifestMd);
|
|
7714
|
+
generatedFiles.push(manifestMdPath);
|
|
7715
|
+
return {
|
|
7716
|
+
outDir,
|
|
7717
|
+
fileCount: codeFiles.length,
|
|
7718
|
+
moduleCount: modules.length,
|
|
7719
|
+
commitCount: commits.length,
|
|
7720
|
+
generatedFiles: generatedFiles.sort((a, b) => a.localeCompare(b))
|
|
7721
|
+
};
|
|
7722
|
+
}
|
|
7723
|
+
|
|
5576
7724
|
// src/server/index.ts
|
|
5577
|
-
import { Hono as
|
|
7725
|
+
import { Hono as Hono11 } from "hono";
|
|
5578
7726
|
import { cors } from "hono/cors";
|
|
5579
7727
|
import { logger } from "hono/logger";
|
|
5580
7728
|
import { serve } from "@hono/node-server";
|
|
5581
7729
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5582
|
-
import * as
|
|
5583
|
-
import * as
|
|
7730
|
+
import * as path8 from "path";
|
|
7731
|
+
import * as fs8 from "fs";
|
|
5584
7732
|
|
|
5585
7733
|
// src/server/api/index.ts
|
|
5586
|
-
import { Hono as
|
|
7734
|
+
import { Hono as Hono10 } from "hono";
|
|
5587
7735
|
|
|
5588
7736
|
// src/server/api/sessions.ts
|
|
5589
7737
|
import { Hono } from "hono";
|
|
7738
|
+
|
|
7739
|
+
// src/server/api/utils.ts
|
|
7740
|
+
import * as path6 from "path";
|
|
7741
|
+
import * as os3 from "os";
|
|
7742
|
+
function getServiceFromQuery(c) {
|
|
7743
|
+
const project = c.req.query("project");
|
|
7744
|
+
if (project) {
|
|
7745
|
+
const isHash = /^[a-f0-9]{8}$/.test(project);
|
|
7746
|
+
let storagePath;
|
|
7747
|
+
if (isHash) {
|
|
7748
|
+
storagePath = path6.join(os3.homedir(), ".claude-code", "memory", "projects", project);
|
|
7749
|
+
} else {
|
|
7750
|
+
const crypto3 = __require("crypto");
|
|
7751
|
+
const normalized = project.replace(/\/+$/, "") || "/";
|
|
7752
|
+
const hash = crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
7753
|
+
storagePath = path6.join(os3.homedir(), ".claude-code", "memory", "projects", hash);
|
|
7754
|
+
}
|
|
7755
|
+
return new MemoryService({
|
|
7756
|
+
storagePath,
|
|
7757
|
+
readOnly: true,
|
|
7758
|
+
analyticsEnabled: false,
|
|
7759
|
+
sharedStoreConfig: { enabled: false }
|
|
7760
|
+
});
|
|
7761
|
+
}
|
|
7762
|
+
return getReadOnlyMemoryService();
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
// src/server/api/sessions.ts
|
|
5590
7766
|
var sessionsRouter = new Hono();
|
|
5591
7767
|
sessionsRouter.get("/", async (c) => {
|
|
5592
7768
|
const page = parseInt(c.req.query("page") || "1", 10);
|
|
5593
7769
|
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
5594
|
-
const memoryService =
|
|
7770
|
+
const memoryService = getServiceFromQuery(c);
|
|
5595
7771
|
try {
|
|
5596
7772
|
await memoryService.initialize();
|
|
5597
7773
|
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
@@ -5635,7 +7811,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
5635
7811
|
});
|
|
5636
7812
|
sessionsRouter.get("/:id", async (c) => {
|
|
5637
7813
|
const { id } = c.req.param();
|
|
5638
|
-
const memoryService =
|
|
7814
|
+
const memoryService = getServiceFromQuery(c);
|
|
5639
7815
|
try {
|
|
5640
7816
|
await memoryService.initialize();
|
|
5641
7817
|
const events = await memoryService.getSessionHistory(id);
|
|
@@ -5676,18 +7852,36 @@ var eventsRouter = new Hono2();
|
|
|
5676
7852
|
eventsRouter.get("/", async (c) => {
|
|
5677
7853
|
const sessionId = c.req.query("sessionId");
|
|
5678
7854
|
const eventType = c.req.query("type");
|
|
7855
|
+
const level = c.req.query("level");
|
|
7856
|
+
const sort = c.req.query("sort") || "recent";
|
|
5679
7857
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
5680
7858
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5681
|
-
const memoryService =
|
|
7859
|
+
const memoryService = getServiceFromQuery(c);
|
|
5682
7860
|
try {
|
|
5683
7861
|
await memoryService.initialize();
|
|
5684
|
-
let events
|
|
7862
|
+
let events;
|
|
7863
|
+
if (level) {
|
|
7864
|
+
events = await memoryService.getEventsByLevel(level, { limit: limit + offset + 1e3, offset: 0 });
|
|
7865
|
+
} else {
|
|
7866
|
+
events = await memoryService.getRecentEvents(limit + offset + 1e3);
|
|
7867
|
+
}
|
|
5685
7868
|
if (sessionId) {
|
|
5686
7869
|
events = events.filter((e) => e.sessionId === sessionId);
|
|
5687
7870
|
}
|
|
5688
7871
|
if (eventType) {
|
|
5689
7872
|
events = events.filter((e) => e.eventType === eventType);
|
|
5690
7873
|
}
|
|
7874
|
+
if (sort === "accessed") {
|
|
7875
|
+
events.sort((a, b) => {
|
|
7876
|
+
const aTime = a.last_accessed_at || "";
|
|
7877
|
+
const bTime = b.last_accessed_at || "";
|
|
7878
|
+
return bTime.localeCompare(aTime);
|
|
7879
|
+
});
|
|
7880
|
+
} else if (sort === "most-accessed") {
|
|
7881
|
+
events.sort((a, b) => (b.access_count || 0) - (a.access_count || 0));
|
|
7882
|
+
} else if (sort === "oldest") {
|
|
7883
|
+
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
7884
|
+
}
|
|
5691
7885
|
const total = events.length;
|
|
5692
7886
|
events = events.slice(offset, offset + limit);
|
|
5693
7887
|
return c.json({
|
|
@@ -5697,7 +7891,9 @@ eventsRouter.get("/", async (c) => {
|
|
|
5697
7891
|
timestamp: e.timestamp,
|
|
5698
7892
|
sessionId: e.sessionId,
|
|
5699
7893
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
5700
|
-
contentLength: e.content.length
|
|
7894
|
+
contentLength: e.content.length,
|
|
7895
|
+
accessCount: e.access_count || 0,
|
|
7896
|
+
lastAccessedAt: e.last_accessed_at || null
|
|
5701
7897
|
})),
|
|
5702
7898
|
total,
|
|
5703
7899
|
limit,
|
|
@@ -5712,7 +7908,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
5712
7908
|
});
|
|
5713
7909
|
eventsRouter.get("/:id", async (c) => {
|
|
5714
7910
|
const { id } = c.req.param();
|
|
5715
|
-
const memoryService =
|
|
7911
|
+
const memoryService = getServiceFromQuery(c);
|
|
5716
7912
|
try {
|
|
5717
7913
|
await memoryService.initialize();
|
|
5718
7914
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5752,7 +7948,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
5752
7948
|
import { Hono as Hono3 } from "hono";
|
|
5753
7949
|
var searchRouter = new Hono3();
|
|
5754
7950
|
searchRouter.post("/", async (c) => {
|
|
5755
|
-
const memoryService =
|
|
7951
|
+
const memoryService = getServiceFromQuery(c);
|
|
5756
7952
|
try {
|
|
5757
7953
|
const body = await c.req.json();
|
|
5758
7954
|
if (!body.query) {
|
|
@@ -5796,7 +7992,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5796
7992
|
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
5797
7993
|
}
|
|
5798
7994
|
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
5799
|
-
const memoryService =
|
|
7995
|
+
const memoryService = getServiceFromQuery(c);
|
|
5800
7996
|
try {
|
|
5801
7997
|
await memoryService.initialize();
|
|
5802
7998
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
@@ -5824,7 +8020,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5824
8020
|
import { Hono as Hono4 } from "hono";
|
|
5825
8021
|
var statsRouter = new Hono4();
|
|
5826
8022
|
statsRouter.get("/shared", async (c) => {
|
|
5827
|
-
const memoryService =
|
|
8023
|
+
const memoryService = getServiceFromQuery(c);
|
|
5828
8024
|
try {
|
|
5829
8025
|
await memoryService.initialize();
|
|
5830
8026
|
const sharedStats = await memoryService.getSharedStoreStats();
|
|
@@ -5881,7 +8077,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5881
8077
|
if (!validLevels.includes(level)) {
|
|
5882
8078
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
5883
8079
|
}
|
|
5884
|
-
const memoryService =
|
|
8080
|
+
const memoryService = getServiceFromQuery(c);
|
|
5885
8081
|
try {
|
|
5886
8082
|
await memoryService.initialize();
|
|
5887
8083
|
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
@@ -5928,7 +8124,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5928
8124
|
}
|
|
5929
8125
|
});
|
|
5930
8126
|
statsRouter.get("/", async (c) => {
|
|
5931
|
-
const memoryService =
|
|
8127
|
+
const memoryService = getServiceFromQuery(c);
|
|
5932
8128
|
try {
|
|
5933
8129
|
await memoryService.initialize();
|
|
5934
8130
|
const stats = await memoryService.getStats();
|
|
@@ -5945,6 +8141,7 @@ statsRouter.get("/", async (c) => {
|
|
|
5945
8141
|
acc[day] = (acc[day] || 0) + 1;
|
|
5946
8142
|
return acc;
|
|
5947
8143
|
}, {});
|
|
8144
|
+
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
5948
8145
|
return c.json({
|
|
5949
8146
|
storage: {
|
|
5950
8147
|
eventCount: stats.totalEvents,
|
|
@@ -5962,7 +8159,8 @@ statsRouter.get("/", async (c) => {
|
|
|
5962
8159
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
5963
8160
|
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
|
|
5964
8161
|
},
|
|
5965
|
-
levelStats: stats.levelStats
|
|
8162
|
+
levelStats: stats.levelStats,
|
|
8163
|
+
retrievalTrace
|
|
5966
8164
|
});
|
|
5967
8165
|
} catch (error) {
|
|
5968
8166
|
return c.json({ error: error.message }, 500);
|
|
@@ -5972,7 +8170,7 @@ statsRouter.get("/", async (c) => {
|
|
|
5972
8170
|
});
|
|
5973
8171
|
statsRouter.get("/most-accessed", async (c) => {
|
|
5974
8172
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
5975
|
-
const memoryService =
|
|
8173
|
+
const memoryService = getServiceFromQuery(c);
|
|
5976
8174
|
try {
|
|
5977
8175
|
await memoryService.initialize();
|
|
5978
8176
|
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
@@ -6003,7 +8201,7 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
6003
8201
|
});
|
|
6004
8202
|
statsRouter.get("/timeline", async (c) => {
|
|
6005
8203
|
const days = parseInt(c.req.query("days") || "7", 10);
|
|
6006
|
-
const memoryService =
|
|
8204
|
+
const memoryService = getServiceFromQuery(c);
|
|
6007
8205
|
try {
|
|
6008
8206
|
await memoryService.initialize();
|
|
6009
8207
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6033,8 +8231,75 @@ statsRouter.get("/timeline", async (c) => {
|
|
|
6033
8231
|
await memoryService.shutdown();
|
|
6034
8232
|
}
|
|
6035
8233
|
});
|
|
8234
|
+
statsRouter.get("/helpfulness", async (c) => {
|
|
8235
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
8236
|
+
const memoryService = getServiceFromQuery(c);
|
|
8237
|
+
try {
|
|
8238
|
+
await memoryService.initialize();
|
|
8239
|
+
const stats = await memoryService.getHelpfulnessStats();
|
|
8240
|
+
const topMemories = await memoryService.getHelpfulMemories(limit);
|
|
8241
|
+
return c.json({
|
|
8242
|
+
...stats,
|
|
8243
|
+
topMemories: topMemories.map((m) => ({
|
|
8244
|
+
eventId: m.eventId,
|
|
8245
|
+
summary: m.summary,
|
|
8246
|
+
helpfulnessScore: m.helpfulnessScore,
|
|
8247
|
+
accessCount: m.accessCount,
|
|
8248
|
+
evaluationCount: m.evaluationCount
|
|
8249
|
+
}))
|
|
8250
|
+
});
|
|
8251
|
+
} catch (error) {
|
|
8252
|
+
return c.json({
|
|
8253
|
+
avgScore: 0,
|
|
8254
|
+
totalEvaluated: 0,
|
|
8255
|
+
totalRetrievals: 0,
|
|
8256
|
+
helpful: 0,
|
|
8257
|
+
neutral: 0,
|
|
8258
|
+
unhelpful: 0,
|
|
8259
|
+
topMemories: []
|
|
8260
|
+
});
|
|
8261
|
+
} finally {
|
|
8262
|
+
await memoryService.shutdown();
|
|
8263
|
+
}
|
|
8264
|
+
});
|
|
8265
|
+
statsRouter.get("/retrieval-traces", async (c) => {
|
|
8266
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
8267
|
+
const memoryService = getServiceFromQuery(c);
|
|
8268
|
+
try {
|
|
8269
|
+
await memoryService.initialize();
|
|
8270
|
+
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
8271
|
+
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
8272
|
+
return c.json({
|
|
8273
|
+
stats: traceStats,
|
|
8274
|
+
traces: traces.map((t) => ({
|
|
8275
|
+
traceId: t.traceId,
|
|
8276
|
+
sessionId: t.sessionId || null,
|
|
8277
|
+
projectHash: t.projectHash || null,
|
|
8278
|
+
queryText: t.queryText,
|
|
8279
|
+
strategy: t.strategy || null,
|
|
8280
|
+
candidateEventIds: t.candidateEventIds,
|
|
8281
|
+
selectedEventIds: t.selectedEventIds,
|
|
8282
|
+
candidateDetails: t.candidateDetails || [],
|
|
8283
|
+
selectedDetails: t.selectedDetails || [],
|
|
8284
|
+
candidateCount: t.candidateCount,
|
|
8285
|
+
selectedCount: t.selectedCount,
|
|
8286
|
+
confidence: t.confidence || null,
|
|
8287
|
+
fallbackTrace: t.fallbackTrace,
|
|
8288
|
+
createdAt: t.createdAt.toISOString()
|
|
8289
|
+
}))
|
|
8290
|
+
});
|
|
8291
|
+
} catch (error) {
|
|
8292
|
+
return c.json({
|
|
8293
|
+
stats: { totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 },
|
|
8294
|
+
traces: [],
|
|
8295
|
+
error: error.message
|
|
8296
|
+
}, 500);
|
|
8297
|
+
} finally {
|
|
8298
|
+
await memoryService.shutdown();
|
|
8299
|
+
}
|
|
8300
|
+
});
|
|
6036
8301
|
statsRouter.post("/graduation/run", async (c) => {
|
|
6037
|
-
const memoryService =
|
|
8302
|
+
const memoryService = getServiceFromQuery(c);
|
|
6038
8303
|
try {
|
|
6039
8304
|
await memoryService.initialize();
|
|
6040
8305
|
const result = await memoryService.forceGraduation();
|
|
@@ -6095,7 +8360,7 @@ var citationsRouter = new Hono5();
|
|
|
6095
8360
|
citationsRouter.get("/:id", async (c) => {
|
|
6096
8361
|
const { id } = c.req.param();
|
|
6097
8362
|
const citationId = parseCitationId(id) || id;
|
|
6098
|
-
const memoryService =
|
|
8363
|
+
const memoryService = getServiceFromQuery(c);
|
|
6099
8364
|
try {
|
|
6100
8365
|
await memoryService.initialize();
|
|
6101
8366
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6129,7 +8394,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
6129
8394
|
citationsRouter.get("/:id/related", async (c) => {
|
|
6130
8395
|
const { id } = c.req.param();
|
|
6131
8396
|
const citationId = parseCitationId(id) || id;
|
|
6132
|
-
const memoryService =
|
|
8397
|
+
const memoryService = getServiceFromQuery(c);
|
|
6133
8398
|
try {
|
|
6134
8399
|
await memoryService.initialize();
|
|
6135
8400
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6165,23 +8430,415 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
6165
8430
|
}
|
|
6166
8431
|
});
|
|
6167
8432
|
|
|
8433
|
+
// src/server/api/turns.ts
|
|
8434
|
+
import { Hono as Hono6 } from "hono";
|
|
8435
|
+
var turnsRouter = new Hono6();
|
|
8436
|
+
turnsRouter.get("/", async (c) => {
|
|
8437
|
+
const sessionId = c.req.query("sessionId");
|
|
8438
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
8439
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
8440
|
+
if (!sessionId) {
|
|
8441
|
+
return c.json({ error: "sessionId is required" }, 400);
|
|
8442
|
+
}
|
|
8443
|
+
const memoryService = getServiceFromQuery(c);
|
|
8444
|
+
try {
|
|
8445
|
+
await memoryService.initialize();
|
|
8446
|
+
const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
|
|
8447
|
+
const totalTurns = await memoryService.countSessionTurns(sessionId);
|
|
8448
|
+
return c.json({
|
|
8449
|
+
turns: turns.map((t) => ({
|
|
8450
|
+
turnId: t.turnId,
|
|
8451
|
+
startedAt: t.startedAt.toISOString(),
|
|
8452
|
+
promptPreview: t.promptPreview,
|
|
8453
|
+
eventCount: t.eventCount,
|
|
8454
|
+
toolCount: t.toolCount,
|
|
8455
|
+
hasResponse: t.hasResponse,
|
|
8456
|
+
events: t.events.map((e) => ({
|
|
8457
|
+
id: e.id,
|
|
8458
|
+
eventType: e.eventType,
|
|
8459
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
8460
|
+
preview: e.content.slice(0, 300) + (e.content.length > 300 ? "..." : ""),
|
|
8461
|
+
contentLength: e.content.length
|
|
8462
|
+
}))
|
|
8463
|
+
})),
|
|
8464
|
+
total: totalTurns,
|
|
8465
|
+
limit,
|
|
8466
|
+
offset,
|
|
8467
|
+
hasMore: offset + limit < totalTurns
|
|
8468
|
+
});
|
|
8469
|
+
} catch (error) {
|
|
8470
|
+
return c.json({ error: error.message }, 500);
|
|
8471
|
+
} finally {
|
|
8472
|
+
await memoryService.shutdown();
|
|
8473
|
+
}
|
|
8474
|
+
});
|
|
8475
|
+
turnsRouter.get("/:turnId", async (c) => {
|
|
8476
|
+
const { turnId } = c.req.param();
|
|
8477
|
+
const memoryService = getServiceFromQuery(c);
|
|
8478
|
+
try {
|
|
8479
|
+
await memoryService.initialize();
|
|
8480
|
+
const events = await memoryService.getEventsByTurn(turnId);
|
|
8481
|
+
if (events.length === 0) {
|
|
8482
|
+
return c.json({ error: "Turn not found" }, 404);
|
|
8483
|
+
}
|
|
8484
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
8485
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
8486
|
+
const responseEvents = events.filter((e) => e.eventType === "agent_response");
|
|
8487
|
+
return c.json({
|
|
8488
|
+
turnId,
|
|
8489
|
+
sessionId: events[0].sessionId,
|
|
8490
|
+
startedAt: events[0].timestamp instanceof Date ? events[0].timestamp.toISOString() : events[0].timestamp,
|
|
8491
|
+
prompt: promptEvent ? {
|
|
8492
|
+
id: promptEvent.id,
|
|
8493
|
+
content: promptEvent.content,
|
|
8494
|
+
timestamp: promptEvent.timestamp instanceof Date ? promptEvent.timestamp.toISOString() : promptEvent.timestamp
|
|
8495
|
+
} : null,
|
|
8496
|
+
tools: toolEvents.map((e) => {
|
|
8497
|
+
let toolName = "";
|
|
8498
|
+
let success = true;
|
|
8499
|
+
try {
|
|
8500
|
+
const parsed = JSON.parse(e.content);
|
|
8501
|
+
toolName = parsed.toolName || "";
|
|
8502
|
+
success = parsed.success !== false;
|
|
8503
|
+
} catch {
|
|
8504
|
+
}
|
|
8505
|
+
return {
|
|
8506
|
+
id: e.id,
|
|
8507
|
+
toolName,
|
|
8508
|
+
success,
|
|
8509
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
8510
|
+
preview: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : "")
|
|
8511
|
+
};
|
|
8512
|
+
}),
|
|
8513
|
+
responses: responseEvents.map((e) => ({
|
|
8514
|
+
id: e.id,
|
|
8515
|
+
content: e.content,
|
|
8516
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
|
|
8517
|
+
})),
|
|
8518
|
+
totalEvents: events.length
|
|
8519
|
+
});
|
|
8520
|
+
} catch (error) {
|
|
8521
|
+
return c.json({ error: error.message }, 500);
|
|
8522
|
+
} finally {
|
|
8523
|
+
await memoryService.shutdown();
|
|
8524
|
+
}
|
|
8525
|
+
});
|
|
8526
|
+
turnsRouter.post("/backfill", async (c) => {
|
|
8527
|
+
const memoryService = getServiceFromQuery(c);
|
|
8528
|
+
try {
|
|
8529
|
+
await memoryService.initialize();
|
|
8530
|
+
const updated = await memoryService.backfillTurnIds();
|
|
8531
|
+
return c.json({
|
|
8532
|
+
success: true,
|
|
8533
|
+
updated,
|
|
8534
|
+
message: `Backfilled turn_id for ${updated} events`
|
|
8535
|
+
});
|
|
8536
|
+
} catch (error) {
|
|
8537
|
+
return c.json({
|
|
8538
|
+
success: false,
|
|
8539
|
+
error: error.message
|
|
8540
|
+
}, 500);
|
|
8541
|
+
} finally {
|
|
8542
|
+
await memoryService.shutdown();
|
|
8543
|
+
}
|
|
8544
|
+
});
|
|
8545
|
+
|
|
8546
|
+
// src/server/api/projects.ts
|
|
8547
|
+
import { Hono as Hono7 } from "hono";
|
|
8548
|
+
import * as fs7 from "fs";
|
|
8549
|
+
import * as path7 from "path";
|
|
8550
|
+
import * as os4 from "os";
|
|
8551
|
+
var projectsRouter = new Hono7();
|
|
8552
|
+
projectsRouter.get("/", async (c) => {
|
|
8553
|
+
try {
|
|
8554
|
+
const projectsDir = path7.join(os4.homedir(), ".claude-code", "memory", "projects");
|
|
8555
|
+
if (!fs7.existsSync(projectsDir)) {
|
|
8556
|
+
return c.json({ projects: [] });
|
|
8557
|
+
}
|
|
8558
|
+
const projectHashes = fs7.readdirSync(projectsDir).filter((name) => {
|
|
8559
|
+
const fullPath = path7.join(projectsDir, name);
|
|
8560
|
+
return fs7.statSync(fullPath).isDirectory();
|
|
8561
|
+
});
|
|
8562
|
+
const registry = loadSessionRegistry();
|
|
8563
|
+
const hashToPath = /* @__PURE__ */ new Map();
|
|
8564
|
+
for (const entry of Object.values(registry.sessions)) {
|
|
8565
|
+
if (!hashToPath.has(entry.projectHash)) {
|
|
8566
|
+
hashToPath.set(entry.projectHash, entry.projectPath);
|
|
8567
|
+
}
|
|
8568
|
+
}
|
|
8569
|
+
const projects = projectHashes.map((hash) => {
|
|
8570
|
+
const dirPath = path7.join(projectsDir, hash);
|
|
8571
|
+
const dbPath = path7.join(dirPath, "events.sqlite");
|
|
8572
|
+
let dbSize = 0;
|
|
8573
|
+
if (fs7.existsSync(dbPath)) {
|
|
8574
|
+
dbSize = fs7.statSync(dbPath).size;
|
|
8575
|
+
}
|
|
8576
|
+
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
8577
|
+
return {
|
|
8578
|
+
hash,
|
|
8579
|
+
projectPath,
|
|
8580
|
+
projectName: path7.basename(projectPath),
|
|
8581
|
+
dbSize,
|
|
8582
|
+
dbSizeHuman: formatBytes(dbSize)
|
|
8583
|
+
};
|
|
8584
|
+
});
|
|
8585
|
+
projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
|
|
8586
|
+
return c.json({ projects });
|
|
8587
|
+
} catch (error) {
|
|
8588
|
+
return c.json({ projects: [], error: error.message }, 500);
|
|
8589
|
+
}
|
|
8590
|
+
});
|
|
8591
|
+
function formatBytes(bytes) {
|
|
8592
|
+
if (bytes === 0)
|
|
8593
|
+
return "0 B";
|
|
8594
|
+
const k = 1024;
|
|
8595
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
8596
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
8597
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
// src/server/api/chat.ts
|
|
8601
|
+
import { Hono as Hono8 } from "hono";
|
|
8602
|
+
import { streamSSE } from "hono/streaming";
|
|
8603
|
+
import { spawn } from "child_process";
|
|
8604
|
+
var chatRouter = new Hono8();
|
|
8605
|
+
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
8606
|
+
chatRouter.post("/", async (c) => {
|
|
8607
|
+
let body;
|
|
8608
|
+
try {
|
|
8609
|
+
body = await c.req.json();
|
|
8610
|
+
} catch {
|
|
8611
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8612
|
+
}
|
|
8613
|
+
if (!body.message?.trim()) {
|
|
8614
|
+
return c.json({ error: "Message is required" }, 400);
|
|
8615
|
+
}
|
|
8616
|
+
const memoryService = getServiceFromQuery(c);
|
|
8617
|
+
try {
|
|
8618
|
+
await memoryService.initialize();
|
|
8619
|
+
let memoryContext = "";
|
|
8620
|
+
let statsContext = "";
|
|
8621
|
+
try {
|
|
8622
|
+
const result = await memoryService.retrieveMemories(body.message, {
|
|
8623
|
+
topK: 8,
|
|
8624
|
+
minScore: 0.5
|
|
8625
|
+
});
|
|
8626
|
+
if (result.memories.length > 0) {
|
|
8627
|
+
const parts = ["## Relevant Memories\n"];
|
|
8628
|
+
for (const m of result.memories) {
|
|
8629
|
+
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
8630
|
+
const content = m.event.content.slice(0, 500);
|
|
8631
|
+
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
8632
|
+
parts.push(content);
|
|
8633
|
+
if (m.sessionContext) {
|
|
8634
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
8635
|
+
}
|
|
8636
|
+
parts.push("");
|
|
8637
|
+
}
|
|
8638
|
+
memoryContext = parts.join("\n");
|
|
8639
|
+
}
|
|
8640
|
+
} catch {
|
|
8641
|
+
}
|
|
8642
|
+
try {
|
|
8643
|
+
const stats = await memoryService.getStats();
|
|
8644
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
8645
|
+
statsContext = [
|
|
8646
|
+
"## Memory Stats",
|
|
8647
|
+
`- Total events: ${stats.totalEvents}`,
|
|
8648
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
8649
|
+
`- By level: ${levels}`
|
|
8650
|
+
].join("\n");
|
|
8651
|
+
} catch {
|
|
8652
|
+
}
|
|
8653
|
+
const fullPrompt = buildPrompt(
|
|
8654
|
+
statsContext,
|
|
8655
|
+
memoryContext,
|
|
8656
|
+
body.history || [],
|
|
8657
|
+
body.message
|
|
8658
|
+
);
|
|
8659
|
+
return streamSSE(c, async (stream) => {
|
|
8660
|
+
try {
|
|
8661
|
+
await streamClaudeResponse(fullPrompt, stream);
|
|
8662
|
+
} catch (err) {
|
|
8663
|
+
await stream.writeSSE({
|
|
8664
|
+
event: "error",
|
|
8665
|
+
data: JSON.stringify({ error: err.message })
|
|
8666
|
+
});
|
|
8667
|
+
}
|
|
8668
|
+
});
|
|
8669
|
+
} catch (error) {
|
|
8670
|
+
return c.json({ error: error.message }, 500);
|
|
8671
|
+
} finally {
|
|
8672
|
+
await memoryService.shutdown();
|
|
8673
|
+
}
|
|
8674
|
+
});
|
|
8675
|
+
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
8676
|
+
const parts = [];
|
|
8677
|
+
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
8678
|
+
parts.push("The memory system tracks coding sessions, tool usage, prompts, and responses.");
|
|
8679
|
+
parts.push("Answer concisely based on the memory context below. If you don't have enough data, say so.");
|
|
8680
|
+
parts.push("Use markdown formatting in your responses.\n");
|
|
8681
|
+
if (statsContext) {
|
|
8682
|
+
parts.push(statsContext);
|
|
8683
|
+
parts.push("");
|
|
8684
|
+
}
|
|
8685
|
+
if (memoryContext) {
|
|
8686
|
+
parts.push(memoryContext);
|
|
8687
|
+
} else {
|
|
8688
|
+
parts.push("No directly relevant memories found for this query.");
|
|
8689
|
+
parts.push("Answer based on general knowledge or suggest the user rephrase.\n");
|
|
8690
|
+
}
|
|
8691
|
+
parts.push("---\n");
|
|
8692
|
+
const recentHistory = history.slice(-10);
|
|
8693
|
+
if (recentHistory.length > 0) {
|
|
8694
|
+
parts.push("## Conversation History\n");
|
|
8695
|
+
for (const msg of recentHistory) {
|
|
8696
|
+
const prefix = msg.role === "user" ? "User" : "Assistant";
|
|
8697
|
+
parts.push(`**${prefix}:** ${msg.content}
|
|
8698
|
+
`);
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
parts.push(`**User:** ${currentMessage}`);
|
|
8702
|
+
return parts.join("\n");
|
|
8703
|
+
}
|
|
8704
|
+
function streamClaudeResponse(prompt, stream) {
|
|
8705
|
+
return new Promise((resolve4, reject) => {
|
|
8706
|
+
const proc = spawn("claude", [
|
|
8707
|
+
"-p",
|
|
8708
|
+
"--output-format",
|
|
8709
|
+
"stream-json",
|
|
8710
|
+
"--verbose"
|
|
8711
|
+
], {
|
|
8712
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8713
|
+
env: { ...process.env }
|
|
8714
|
+
});
|
|
8715
|
+
const timeout = setTimeout(() => {
|
|
8716
|
+
proc.kill("SIGTERM");
|
|
8717
|
+
reject(new Error("Chat response timed out after 2 minutes"));
|
|
8718
|
+
}, CLAUDE_TIMEOUT_MS);
|
|
8719
|
+
proc.stdin.write(prompt);
|
|
8720
|
+
proc.stdin.end();
|
|
8721
|
+
let buffer = "";
|
|
8722
|
+
let lastSentText = "";
|
|
8723
|
+
proc.stdout.on("data", async (chunk) => {
|
|
8724
|
+
buffer += chunk.toString();
|
|
8725
|
+
const lines = buffer.split("\n");
|
|
8726
|
+
buffer = lines.pop() || "";
|
|
8727
|
+
for (const line of lines) {
|
|
8728
|
+
if (!line.trim())
|
|
8729
|
+
continue;
|
|
8730
|
+
try {
|
|
8731
|
+
const parsed = JSON.parse(line);
|
|
8732
|
+
if (parsed.type === "assistant" && parsed.message?.content) {
|
|
8733
|
+
const textBlocks = parsed.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
8734
|
+
if (textBlocks.length > lastSentText.length) {
|
|
8735
|
+
const delta = textBlocks.slice(lastSentText.length);
|
|
8736
|
+
lastSentText = textBlocks;
|
|
8737
|
+
await stream.writeSSE({
|
|
8738
|
+
event: "message",
|
|
8739
|
+
data: JSON.stringify({ content: delta })
|
|
8740
|
+
});
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
if (parsed.type === "result") {
|
|
8744
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
8745
|
+
}
|
|
8746
|
+
} catch {
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
});
|
|
8750
|
+
proc.stderr.on("data", (chunk) => {
|
|
8751
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
8752
|
+
console.error("[chat] claude stderr:", chunk.toString());
|
|
8753
|
+
}
|
|
8754
|
+
});
|
|
8755
|
+
proc.on("error", (err) => {
|
|
8756
|
+
clearTimeout(timeout);
|
|
8757
|
+
if (err.code === "ENOENT") {
|
|
8758
|
+
reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
|
|
8759
|
+
} else {
|
|
8760
|
+
reject(err);
|
|
8761
|
+
}
|
|
8762
|
+
});
|
|
8763
|
+
proc.on("close", async (code) => {
|
|
8764
|
+
clearTimeout(timeout);
|
|
8765
|
+
if (buffer.trim()) {
|
|
8766
|
+
try {
|
|
8767
|
+
const parsed = JSON.parse(buffer);
|
|
8768
|
+
if (parsed.type === "result") {
|
|
8769
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
8770
|
+
}
|
|
8771
|
+
} catch {
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
if (code !== 0 && code !== null) {
|
|
8775
|
+
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
8776
|
+
} else {
|
|
8777
|
+
resolve4();
|
|
8778
|
+
}
|
|
8779
|
+
});
|
|
8780
|
+
});
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8783
|
+
// src/server/api/health.ts
|
|
8784
|
+
import { Hono as Hono9 } from "hono";
|
|
8785
|
+
var healthRouter = new Hono9();
|
|
8786
|
+
healthRouter.get("/", async (c) => {
|
|
8787
|
+
const memoryService = getServiceFromQuery(c);
|
|
8788
|
+
try {
|
|
8789
|
+
await memoryService.initialize();
|
|
8790
|
+
const [stats, outbox] = await Promise.all([
|
|
8791
|
+
memoryService.getStats(),
|
|
8792
|
+
memoryService.getOutboxStats()
|
|
8793
|
+
]);
|
|
8794
|
+
const outboxPending = outbox.embedding.pending + outbox.vector.pending;
|
|
8795
|
+
const outboxFailed = outbox.embedding.failed + outbox.vector.failed;
|
|
8796
|
+
const status = outboxFailed > 0 ? "needs-attention" : "ok";
|
|
8797
|
+
return c.json({
|
|
8798
|
+
status,
|
|
8799
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8800
|
+
storage: {
|
|
8801
|
+
totalEvents: stats.totalEvents,
|
|
8802
|
+
vectorCount: stats.vectorCount
|
|
8803
|
+
},
|
|
8804
|
+
outbox: {
|
|
8805
|
+
embedding: outbox.embedding,
|
|
8806
|
+
vector: outbox.vector,
|
|
8807
|
+
totals: {
|
|
8808
|
+
pending: outboxPending,
|
|
8809
|
+
failed: outboxFailed
|
|
8810
|
+
}
|
|
8811
|
+
},
|
|
8812
|
+
levelStats: stats.levelStats
|
|
8813
|
+
});
|
|
8814
|
+
} catch (error) {
|
|
8815
|
+
return c.json({
|
|
8816
|
+
status: "error",
|
|
8817
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8818
|
+
error: error.message
|
|
8819
|
+
}, 500);
|
|
8820
|
+
} finally {
|
|
8821
|
+
await memoryService.shutdown();
|
|
8822
|
+
}
|
|
8823
|
+
});
|
|
8824
|
+
|
|
6168
8825
|
// src/server/api/index.ts
|
|
6169
|
-
var apiRouter = new
|
|
8826
|
+
var apiRouter = new Hono10().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter);
|
|
6170
8827
|
|
|
6171
8828
|
// src/server/index.ts
|
|
6172
|
-
var app = new
|
|
8829
|
+
var app = new Hono11();
|
|
6173
8830
|
app.use("/*", cors());
|
|
6174
8831
|
app.use("/*", logger());
|
|
6175
8832
|
app.route("/api", apiRouter);
|
|
6176
8833
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
6177
|
-
var uiPath =
|
|
6178
|
-
if (
|
|
8834
|
+
var uiPath = path8.join(__dirname, "../../dist/ui");
|
|
8835
|
+
if (fs8.existsSync(uiPath)) {
|
|
6179
8836
|
app.use("/*", serveStatic({ root: uiPath }));
|
|
6180
8837
|
}
|
|
6181
8838
|
app.get("*", (c) => {
|
|
6182
|
-
const indexPath =
|
|
6183
|
-
if (
|
|
6184
|
-
return c.html(
|
|
8839
|
+
const indexPath = path8.join(uiPath, "index.html");
|
|
8840
|
+
if (fs8.existsSync(indexPath)) {
|
|
8841
|
+
return c.html(fs8.readFileSync(indexPath, "utf-8"));
|
|
6185
8842
|
}
|
|
6186
8843
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
6187
8844
|
});
|
|
@@ -6218,29 +8875,303 @@ if (isMainModule) {
|
|
|
6218
8875
|
startServer(port);
|
|
6219
8876
|
}
|
|
6220
8877
|
|
|
8878
|
+
// src/core/mongo-sync-worker.ts
|
|
8879
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
8880
|
+
import * as os5 from "os";
|
|
8881
|
+
import { MongoClient } from "mongodb";
|
|
8882
|
+
function redactMongoUri(uri) {
|
|
8883
|
+
const schemeIdx = uri.indexOf("://");
|
|
8884
|
+
if (schemeIdx === -1)
|
|
8885
|
+
return uri;
|
|
8886
|
+
const atIdx = uri.indexOf("@", schemeIdx + 3);
|
|
8887
|
+
if (atIdx === -1)
|
|
8888
|
+
return uri;
|
|
8889
|
+
const creds = uri.slice(schemeIdx + 3, atIdx);
|
|
8890
|
+
const colonIdx = creds.indexOf(":");
|
|
8891
|
+
if (colonIdx === -1)
|
|
8892
|
+
return uri;
|
|
8893
|
+
const prefix = uri.slice(0, schemeIdx + 3 + colonIdx + 1);
|
|
8894
|
+
const suffix = uri.slice(atIdx);
|
|
8895
|
+
return `${prefix}***${suffix}`;
|
|
8896
|
+
}
|
|
8897
|
+
function parseIntOrZero(value) {
|
|
8898
|
+
if (!value)
|
|
8899
|
+
return 0;
|
|
8900
|
+
const n = parseInt(value, 10);
|
|
8901
|
+
return Number.isFinite(n) ? n : 0;
|
|
8902
|
+
}
|
|
8903
|
+
var MongoSyncWorker = class {
|
|
8904
|
+
constructor(sqliteStore, config) {
|
|
8905
|
+
this.sqliteStore = sqliteStore;
|
|
8906
|
+
this.config = {
|
|
8907
|
+
uri: config.uri,
|
|
8908
|
+
dbName: config.dbName,
|
|
8909
|
+
projectKey: config.projectKey,
|
|
8910
|
+
direction: config.direction ?? "both",
|
|
8911
|
+
intervalMs: config.intervalMs ?? 3e4,
|
|
8912
|
+
batchSize: config.batchSize ?? 500,
|
|
8913
|
+
instanceId: config.instanceId ?? randomUUID10()
|
|
8914
|
+
};
|
|
8915
|
+
}
|
|
8916
|
+
config;
|
|
8917
|
+
intervalHandle = null;
|
|
8918
|
+
running = false;
|
|
8919
|
+
client = null;
|
|
8920
|
+
db = null;
|
|
8921
|
+
counters = null;
|
|
8922
|
+
events = null;
|
|
8923
|
+
indexesEnsured = false;
|
|
8924
|
+
stats = {
|
|
8925
|
+
lastSyncAt: null,
|
|
8926
|
+
pushedEvents: 0,
|
|
8927
|
+
pulledEvents: 0,
|
|
8928
|
+
errors: 0,
|
|
8929
|
+
status: "idle"
|
|
8930
|
+
};
|
|
8931
|
+
start() {
|
|
8932
|
+
if (this.running)
|
|
8933
|
+
return;
|
|
8934
|
+
this.running = true;
|
|
8935
|
+
this.stats.status = "idle";
|
|
8936
|
+
this.syncNow().catch((err) => {
|
|
8937
|
+
console.error("[MongoSyncWorker] Initial sync failed:", err);
|
|
8938
|
+
});
|
|
8939
|
+
this.intervalHandle = setInterval(() => {
|
|
8940
|
+
this.syncNow().catch((err) => {
|
|
8941
|
+
console.error("[MongoSyncWorker] Periodic sync failed:", err);
|
|
8942
|
+
});
|
|
8943
|
+
}, this.config.intervalMs);
|
|
8944
|
+
}
|
|
8945
|
+
stop() {
|
|
8946
|
+
this.running = false;
|
|
8947
|
+
this.stats.status = "stopped";
|
|
8948
|
+
if (this.intervalHandle) {
|
|
8949
|
+
clearInterval(this.intervalHandle);
|
|
8950
|
+
this.intervalHandle = null;
|
|
8951
|
+
}
|
|
8952
|
+
}
|
|
8953
|
+
async shutdown() {
|
|
8954
|
+
this.stop();
|
|
8955
|
+
await this.disconnect();
|
|
8956
|
+
}
|
|
8957
|
+
getStats() {
|
|
8958
|
+
return { ...this.stats };
|
|
8959
|
+
}
|
|
8960
|
+
isRunning() {
|
|
8961
|
+
return this.running;
|
|
8962
|
+
}
|
|
8963
|
+
async syncNow() {
|
|
8964
|
+
if (this.stats.status === "syncing")
|
|
8965
|
+
return { pushed: 0, pulled: 0 };
|
|
8966
|
+
this.stats.status = "syncing";
|
|
8967
|
+
let pushed = 0;
|
|
8968
|
+
let pulled = 0;
|
|
8969
|
+
try {
|
|
8970
|
+
await this.sqliteStore.initialize();
|
|
8971
|
+
await this.ensureConnected();
|
|
8972
|
+
await this.ensureIndexes();
|
|
8973
|
+
if (this.config.direction === "push" || this.config.direction === "both") {
|
|
8974
|
+
pushed = await this.pushEvents();
|
|
8975
|
+
this.stats.pushedEvents += pushed;
|
|
8976
|
+
}
|
|
8977
|
+
if (this.config.direction === "pull" || this.config.direction === "both") {
|
|
8978
|
+
pulled = await this.pullEvents();
|
|
8979
|
+
this.stats.pulledEvents += pulled;
|
|
8980
|
+
}
|
|
8981
|
+
this.stats.lastSyncAt = /* @__PURE__ */ new Date();
|
|
8982
|
+
this.stats.status = "idle";
|
|
8983
|
+
return { pushed, pulled };
|
|
8984
|
+
} catch (error) {
|
|
8985
|
+
this.stats.errors++;
|
|
8986
|
+
this.stats.status = "error";
|
|
8987
|
+
throw error;
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
async ensureConnected() {
|
|
8991
|
+
if (this.client && this.db && this.counters && this.events)
|
|
8992
|
+
return;
|
|
8993
|
+
try {
|
|
8994
|
+
this.client = new MongoClient(this.config.uri, {
|
|
8995
|
+
appName: "claude-memory-layer",
|
|
8996
|
+
serverSelectionTimeoutMS: 5e3
|
|
8997
|
+
});
|
|
8998
|
+
await this.client.connect();
|
|
8999
|
+
this.db = this.client.db(this.config.dbName);
|
|
9000
|
+
this.counters = this.db.collection("cml_counters");
|
|
9001
|
+
this.events = this.db.collection("cml_events");
|
|
9002
|
+
} catch (err) {
|
|
9003
|
+
const safeUri = redactMongoUri(this.config.uri);
|
|
9004
|
+
throw new Error(`MongoDB connection failed (${safeUri}, db=${this.config.dbName}): ${String(err)}`);
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
async disconnect() {
|
|
9008
|
+
try {
|
|
9009
|
+
await this.client?.close();
|
|
9010
|
+
} finally {
|
|
9011
|
+
this.client = null;
|
|
9012
|
+
this.db = null;
|
|
9013
|
+
this.counters = null;
|
|
9014
|
+
this.events = null;
|
|
9015
|
+
this.indexesEnsured = false;
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
async ensureIndexes() {
|
|
9019
|
+
if (this.indexesEnsured)
|
|
9020
|
+
return;
|
|
9021
|
+
if (!this.events || !this.counters)
|
|
9022
|
+
throw new Error("Mongo not connected");
|
|
9023
|
+
try {
|
|
9024
|
+
await this.events.createIndex({ projectKey: 1, seq: 1 }, { unique: true });
|
|
9025
|
+
await this.events.createIndex({ projectKey: 1, eventId: 1 }, { unique: true });
|
|
9026
|
+
await this.events.createIndex({ projectKey: 1, dedupeKey: 1 });
|
|
9027
|
+
} catch (err) {
|
|
9028
|
+
console.warn("[MongoSyncWorker] Failed to ensure indexes (continuing):", err);
|
|
9029
|
+
}
|
|
9030
|
+
this.indexesEnsured = true;
|
|
9031
|
+
}
|
|
9032
|
+
counterKey(kind) {
|
|
9033
|
+
return `${kind}:${this.config.projectKey}`;
|
|
9034
|
+
}
|
|
9035
|
+
async allocateSeqRange(kind, count) {
|
|
9036
|
+
if (!this.counters)
|
|
9037
|
+
throw new Error("Mongo not connected");
|
|
9038
|
+
if (count <= 0)
|
|
9039
|
+
return 1;
|
|
9040
|
+
const key = this.counterKey(kind);
|
|
9041
|
+
const doc = await this.counters.findOneAndUpdate(
|
|
9042
|
+
{ _id: key },
|
|
9043
|
+
{ $inc: { seq: count } },
|
|
9044
|
+
{ upsert: true, returnDocument: "after" }
|
|
9045
|
+
);
|
|
9046
|
+
const endSeq = doc?.seq;
|
|
9047
|
+
if (typeof endSeq !== "number") {
|
|
9048
|
+
throw new Error(`Failed to allocate seq range for ${key}`);
|
|
9049
|
+
}
|
|
9050
|
+
return endSeq - count + 1;
|
|
9051
|
+
}
|
|
9052
|
+
pushTargetName() {
|
|
9053
|
+
return `mongo_push_events_rowid:${this.config.projectKey}`;
|
|
9054
|
+
}
|
|
9055
|
+
pullTargetName() {
|
|
9056
|
+
return `mongo_pull_events_seq:${this.config.projectKey}`;
|
|
9057
|
+
}
|
|
9058
|
+
async pushEvents() {
|
|
9059
|
+
if (!this.events)
|
|
9060
|
+
throw new Error("Mongo not connected");
|
|
9061
|
+
const position = await this.sqliteStore.getSyncPosition(this.pushTargetName());
|
|
9062
|
+
let lastRowid = parseIntOrZero(position.lastEventId);
|
|
9063
|
+
let pushed = 0;
|
|
9064
|
+
while (true) {
|
|
9065
|
+
const batch = await this.sqliteStore.getEventsSinceRowid(lastRowid, this.config.batchSize);
|
|
9066
|
+
if (batch.length === 0)
|
|
9067
|
+
break;
|
|
9068
|
+
const startSeq = await this.allocateSeqRange("events", batch.length);
|
|
9069
|
+
const now = /* @__PURE__ */ new Date();
|
|
9070
|
+
const hostname2 = os5.hostname();
|
|
9071
|
+
const ops = batch.map((item, idx) => {
|
|
9072
|
+
const event = item.event;
|
|
9073
|
+
const seq = startSeq + idx;
|
|
9074
|
+
const docId = `${this.config.projectKey}:${event.id}`;
|
|
9075
|
+
return {
|
|
9076
|
+
updateOne: {
|
|
9077
|
+
filter: { _id: docId },
|
|
9078
|
+
update: {
|
|
9079
|
+
$setOnInsert: {
|
|
9080
|
+
_id: docId,
|
|
9081
|
+
projectKey: this.config.projectKey,
|
|
9082
|
+
seq,
|
|
9083
|
+
eventId: event.id,
|
|
9084
|
+
eventType: event.eventType,
|
|
9085
|
+
sessionId: event.sessionId,
|
|
9086
|
+
timestamp: event.timestamp,
|
|
9087
|
+
content: event.content,
|
|
9088
|
+
canonicalKey: event.canonicalKey,
|
|
9089
|
+
dedupeKey: event.dedupeKey,
|
|
9090
|
+
metadata: event.metadata ?? null,
|
|
9091
|
+
insertedAt: now,
|
|
9092
|
+
updatedAt: now,
|
|
9093
|
+
source: { hostname: hostname2, instanceId: this.config.instanceId }
|
|
9094
|
+
}
|
|
9095
|
+
},
|
|
9096
|
+
upsert: true
|
|
9097
|
+
}
|
|
9098
|
+
};
|
|
9099
|
+
});
|
|
9100
|
+
await this.events.bulkWrite(ops, { ordered: false });
|
|
9101
|
+
const last = batch[batch.length - 1];
|
|
9102
|
+
lastRowid = last.rowid;
|
|
9103
|
+
await this.sqliteStore.updateSyncPosition(
|
|
9104
|
+
this.pushTargetName(),
|
|
9105
|
+
String(lastRowid),
|
|
9106
|
+
last.event.timestamp.toISOString()
|
|
9107
|
+
);
|
|
9108
|
+
pushed += batch.length;
|
|
9109
|
+
if (batch.length < this.config.batchSize)
|
|
9110
|
+
break;
|
|
9111
|
+
}
|
|
9112
|
+
return pushed;
|
|
9113
|
+
}
|
|
9114
|
+
async pullEvents() {
|
|
9115
|
+
if (!this.events)
|
|
9116
|
+
throw new Error("Mongo not connected");
|
|
9117
|
+
const position = await this.sqliteStore.getSyncPosition(this.pullTargetName());
|
|
9118
|
+
let lastSeq = parseIntOrZero(position.lastEventId);
|
|
9119
|
+
let pulled = 0;
|
|
9120
|
+
while (true) {
|
|
9121
|
+
const docs = await this.events.find(
|
|
9122
|
+
{ projectKey: this.config.projectKey, seq: { $gt: lastSeq } },
|
|
9123
|
+
{ sort: { seq: 1 }, limit: this.config.batchSize }
|
|
9124
|
+
).toArray();
|
|
9125
|
+
if (docs.length === 0)
|
|
9126
|
+
break;
|
|
9127
|
+
const events = docs.map((d) => ({
|
|
9128
|
+
id: d.eventId,
|
|
9129
|
+
eventType: d.eventType,
|
|
9130
|
+
sessionId: d.sessionId,
|
|
9131
|
+
timestamp: d.timestamp instanceof Date ? d.timestamp : new Date(d.timestamp),
|
|
9132
|
+
content: d.content,
|
|
9133
|
+
canonicalKey: d.canonicalKey,
|
|
9134
|
+
dedupeKey: d.dedupeKey,
|
|
9135
|
+
metadata: d.metadata ?? void 0
|
|
9136
|
+
}));
|
|
9137
|
+
const result = await this.sqliteStore.importEvents(events);
|
|
9138
|
+
pulled += result.inserted;
|
|
9139
|
+
lastSeq = docs[docs.length - 1].seq;
|
|
9140
|
+
await this.sqliteStore.updateSyncPosition(
|
|
9141
|
+
this.pullTargetName(),
|
|
9142
|
+
String(lastSeq),
|
|
9143
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
9144
|
+
);
|
|
9145
|
+
if (docs.length < this.config.batchSize)
|
|
9146
|
+
break;
|
|
9147
|
+
}
|
|
9148
|
+
return pulled;
|
|
9149
|
+
}
|
|
9150
|
+
};
|
|
9151
|
+
|
|
6221
9152
|
// src/cli/index.ts
|
|
6222
|
-
var CLAUDE_SETTINGS_PATH =
|
|
9153
|
+
var CLAUDE_SETTINGS_PATH = path9.join(os6.homedir(), ".claude", "settings.json");
|
|
6223
9154
|
function getPluginPath() {
|
|
6224
9155
|
const possiblePaths = [
|
|
6225
|
-
|
|
9156
|
+
path9.join(__dirname, ".."),
|
|
6226
9157
|
// When running from dist/cli
|
|
6227
|
-
|
|
9158
|
+
path9.join(__dirname, "../..", "dist"),
|
|
6228
9159
|
// When running from src
|
|
6229
|
-
|
|
9160
|
+
path9.join(process.cwd(), "dist")
|
|
6230
9161
|
// Current working directory
|
|
6231
9162
|
];
|
|
6232
9163
|
for (const p of possiblePaths) {
|
|
6233
|
-
const hooksPath =
|
|
6234
|
-
if (
|
|
9164
|
+
const hooksPath = path9.join(p, "hooks", "user-prompt-submit.js");
|
|
9165
|
+
if (fs9.existsSync(hooksPath)) {
|
|
6235
9166
|
return p;
|
|
6236
9167
|
}
|
|
6237
9168
|
}
|
|
6238
|
-
return
|
|
9169
|
+
return path9.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
|
|
6239
9170
|
}
|
|
6240
9171
|
function loadClaudeSettings() {
|
|
6241
9172
|
try {
|
|
6242
|
-
if (
|
|
6243
|
-
const content =
|
|
9173
|
+
if (fs9.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
9174
|
+
const content = fs9.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
6244
9175
|
return JSON.parse(content);
|
|
6245
9176
|
}
|
|
6246
9177
|
} catch (error) {
|
|
@@ -6249,13 +9180,13 @@ function loadClaudeSettings() {
|
|
|
6249
9180
|
return {};
|
|
6250
9181
|
}
|
|
6251
9182
|
function saveClaudeSettings(settings) {
|
|
6252
|
-
const dir =
|
|
6253
|
-
if (!
|
|
6254
|
-
|
|
9183
|
+
const dir = path9.dirname(CLAUDE_SETTINGS_PATH);
|
|
9184
|
+
if (!fs9.existsSync(dir)) {
|
|
9185
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
6255
9186
|
}
|
|
6256
9187
|
const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
|
|
6257
|
-
|
|
6258
|
-
|
|
9188
|
+
fs9.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
9189
|
+
fs9.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
6259
9190
|
}
|
|
6260
9191
|
function getHooksConfig(pluginPath) {
|
|
6261
9192
|
return {
|
|
@@ -6265,7 +9196,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6265
9196
|
hooks: [
|
|
6266
9197
|
{
|
|
6267
9198
|
type: "command",
|
|
6268
|
-
command: `node ${
|
|
9199
|
+
command: `node ${path9.join(pluginPath, "hooks", "user-prompt-submit.js")}`
|
|
6269
9200
|
}
|
|
6270
9201
|
]
|
|
6271
9202
|
}
|
|
@@ -6276,7 +9207,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6276
9207
|
hooks: [
|
|
6277
9208
|
{
|
|
6278
9209
|
type: "command",
|
|
6279
|
-
command: `node ${
|
|
9210
|
+
command: `node ${path9.join(pluginPath, "hooks", "post-tool-use.js")}`
|
|
6280
9211
|
}
|
|
6281
9212
|
]
|
|
6282
9213
|
}
|
|
@@ -6284,12 +9215,12 @@ function getHooksConfig(pluginPath) {
|
|
|
6284
9215
|
};
|
|
6285
9216
|
}
|
|
6286
9217
|
var program = new Command();
|
|
6287
|
-
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.
|
|
9218
|
+
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.12");
|
|
6288
9219
|
program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
|
|
6289
9220
|
try {
|
|
6290
9221
|
const pluginPath = options.path || getPluginPath();
|
|
6291
|
-
const userPromptHook =
|
|
6292
|
-
if (!
|
|
9222
|
+
const userPromptHook = path9.join(pluginPath, "hooks", "user-prompt-submit.js");
|
|
9223
|
+
if (!fs9.existsSync(userPromptHook)) {
|
|
6293
9224
|
console.error(`
|
|
6294
9225
|
\u274C Hook files not found at: ${pluginPath}`);
|
|
6295
9226
|
console.error(' Make sure you have built the plugin with "npm run build"');
|
|
@@ -6355,7 +9286,7 @@ program.command("status").description("Check plugin installation status").action
|
|
|
6355
9286
|
console.log("Hooks:");
|
|
6356
9287
|
console.log(` UserPromptSubmit: ${hasUserPromptHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6357
9288
|
console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6358
|
-
const hooksExist =
|
|
9289
|
+
const hooksExist = fs9.existsSync(path9.join(pluginPath, "hooks", "user-prompt-submit.js"));
|
|
6359
9290
|
console.log(`
|
|
6360
9291
|
Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
|
|
6361
9292
|
console.log(` Path: ${pluginPath}`);
|
|
@@ -6483,95 +9414,352 @@ program.command("process").description("Process pending embeddings").option("-p,
|
|
|
6483
9414
|
process.exit(1);
|
|
6484
9415
|
}
|
|
6485
9416
|
});
|
|
6486
|
-
program.command("
|
|
9417
|
+
program.command("mongo-sync").description("Sync events with MongoDB for multi-server collaboration (optional)").option("-p, --project <path>", "Project path (defaults to cwd)").option("--mongo-uri <uri>", "MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)").option("--mongo-db <name>", "MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)").option("--mongo-project <key>", "Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))").option("--direction <dir>", "push|pull|both", "both").option("--batch-size <n>", "Batch size", "500").option("--interval <ms>", "Watch interval ms", "30000").option("--watch", "Run continuously").action(async (options) => {
|
|
9418
|
+
const projectPath = options.project || process.cwd();
|
|
9419
|
+
const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
|
|
9420
|
+
const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
|
|
9421
|
+
const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path9.basename(projectPath);
|
|
9422
|
+
const direction = String(options.direction || "both").toLowerCase();
|
|
9423
|
+
if (!mongoUri || !mongoDb) {
|
|
9424
|
+
console.error("\n\u274C MongoDB sync is not configured.");
|
|
9425
|
+
console.error(" Set --mongo-uri/--mongo-db or env CLAUDE_MEMORY_MONGO_URI/CLAUDE_MEMORY_MONGO_DB.\n");
|
|
9426
|
+
process.exit(1);
|
|
9427
|
+
}
|
|
9428
|
+
if (!["push", "pull", "both"].includes(direction)) {
|
|
9429
|
+
console.error("\n\u274C Invalid --direction. Use: push | pull | both\n");
|
|
9430
|
+
process.exit(1);
|
|
9431
|
+
}
|
|
9432
|
+
const storagePath = getProjectStoragePath(projectPath);
|
|
9433
|
+
if (!fs9.existsSync(storagePath)) {
|
|
9434
|
+
fs9.mkdirSync(storagePath, { recursive: true });
|
|
9435
|
+
}
|
|
9436
|
+
const batchSizeParsed = parseInt(options.batchSize, 10);
|
|
9437
|
+
const intervalParsed = parseInt(options.interval, 10);
|
|
9438
|
+
const batchSize = Number.isFinite(batchSizeParsed) && batchSizeParsed > 0 ? batchSizeParsed : 500;
|
|
9439
|
+
const intervalMs = Number.isFinite(intervalParsed) && intervalParsed > 0 ? intervalParsed : 3e4;
|
|
9440
|
+
const sqliteStore = new SQLiteEventStore(path9.join(storagePath, "events.sqlite"));
|
|
9441
|
+
const worker = new MongoSyncWorker(sqliteStore, {
|
|
9442
|
+
uri: mongoUri,
|
|
9443
|
+
dbName: mongoDb,
|
|
9444
|
+
projectKey,
|
|
9445
|
+
direction,
|
|
9446
|
+
batchSize,
|
|
9447
|
+
intervalMs
|
|
9448
|
+
});
|
|
9449
|
+
const runOnce = async () => {
|
|
9450
|
+
const { pushed, pulled } = await worker.syncNow();
|
|
9451
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
9452
|
+
process.stdout.write(`[mongo-sync] ${ts} project=${projectKey} pushed=${pushed} pulled=${pulled}
|
|
9453
|
+
`);
|
|
9454
|
+
};
|
|
9455
|
+
try {
|
|
9456
|
+
if (!options.watch) {
|
|
9457
|
+
await runOnce();
|
|
9458
|
+
await worker.shutdown();
|
|
9459
|
+
sqliteStore.close();
|
|
9460
|
+
return;
|
|
9461
|
+
}
|
|
9462
|
+
console.log(`[mongo-sync] Watch mode started (interval=${intervalMs}ms, project=${projectKey})`);
|
|
9463
|
+
const handle = setInterval(() => {
|
|
9464
|
+
runOnce().catch((err) => {
|
|
9465
|
+
console.error("[mongo-sync] Sync failed:", err);
|
|
9466
|
+
});
|
|
9467
|
+
}, intervalMs);
|
|
9468
|
+
const shutdown = async () => {
|
|
9469
|
+
clearInterval(handle);
|
|
9470
|
+
console.log("\n[mongo-sync] Shutting down...");
|
|
9471
|
+
try {
|
|
9472
|
+
await worker.shutdown();
|
|
9473
|
+
} finally {
|
|
9474
|
+
sqliteStore.close();
|
|
9475
|
+
}
|
|
9476
|
+
process.exit(0);
|
|
9477
|
+
};
|
|
9478
|
+
process.on("SIGINT", () => {
|
|
9479
|
+
void shutdown();
|
|
9480
|
+
});
|
|
9481
|
+
process.on("SIGTERM", () => {
|
|
9482
|
+
void shutdown();
|
|
9483
|
+
});
|
|
9484
|
+
await runOnce();
|
|
9485
|
+
await new Promise(() => {
|
|
9486
|
+
});
|
|
9487
|
+
} catch (error) {
|
|
9488
|
+
console.error("[mongo-sync] Failed:", error);
|
|
9489
|
+
process.exit(1);
|
|
9490
|
+
}
|
|
9491
|
+
});
|
|
9492
|
+
function renderProgress(event) {
|
|
9493
|
+
switch (event.phase) {
|
|
9494
|
+
case "scan":
|
|
9495
|
+
console.log(` \u{1F50D} ${event.message}`);
|
|
9496
|
+
break;
|
|
9497
|
+
case "session-start": {
|
|
9498
|
+
const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
|
|
9499
|
+
const sessionName = path9.basename(event.filePath, ".jsonl").slice(0, 8);
|
|
9500
|
+
process.stdout.write(
|
|
9501
|
+
`\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
|
|
9502
|
+
);
|
|
9503
|
+
break;
|
|
9504
|
+
}
|
|
9505
|
+
case "session-progress": {
|
|
9506
|
+
process.stdout.write(
|
|
9507
|
+
`\r \u{1F4C4} [${event.sessionIndex + 1}/...] ${event.messagesProcessed} msgs | +${event.imported} imported, ~${event.skipped} skipped `
|
|
9508
|
+
);
|
|
9509
|
+
break;
|
|
9510
|
+
}
|
|
9511
|
+
case "session-done": {
|
|
9512
|
+
const imported = event.importedPrompts + event.importedResponses;
|
|
9513
|
+
if (imported > 0) {
|
|
9514
|
+
process.stdout.write(
|
|
9515
|
+
`\r \u2705 [${event.sessionIndex + 1}] +${event.importedPrompts} prompts, +${event.importedResponses} responses${event.skipped > 0 ? `, ~${event.skipped} skipped` : ""}
|
|
9516
|
+
`
|
|
9517
|
+
);
|
|
9518
|
+
} else if (event.skipped > 0) {
|
|
9519
|
+
process.stdout.write(
|
|
9520
|
+
`\r \u23ED\uFE0F [${event.sessionIndex + 1}] All ${event.skipped} already imported
|
|
9521
|
+
`
|
|
9522
|
+
);
|
|
9523
|
+
} else {
|
|
9524
|
+
process.stdout.write(
|
|
9525
|
+
`\r \u23ED\uFE0F [${event.sessionIndex + 1}] Empty session
|
|
9526
|
+
`
|
|
9527
|
+
);
|
|
9528
|
+
}
|
|
9529
|
+
break;
|
|
9530
|
+
}
|
|
9531
|
+
case "embedding":
|
|
9532
|
+
process.stdout.write(
|
|
9533
|
+
`\r \u{1F9E0} Embeddings: ${event.processed}/${event.total} processed `
|
|
9534
|
+
);
|
|
9535
|
+
if (event.processed >= event.total) {
|
|
9536
|
+
process.stdout.write("\n");
|
|
9537
|
+
}
|
|
9538
|
+
break;
|
|
9539
|
+
case "done":
|
|
9540
|
+
break;
|
|
9541
|
+
}
|
|
9542
|
+
}
|
|
9543
|
+
function printImportSummary(result, embedCount) {
|
|
9544
|
+
console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
9545
|
+
console.log("\u2502 \u2705 Import Complete \u2502");
|
|
9546
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
9547
|
+
console.log(`\u2502 Sessions processed: ${String(result.totalSessions).padStart(8)} \u2502`);
|
|
9548
|
+
console.log(`\u2502 Total messages: ${String(result.totalMessages).padStart(8)} \u2502`);
|
|
9549
|
+
console.log(`\u2502 Imported prompts: ${String(result.importedPrompts).padStart(8)} \u2502`);
|
|
9550
|
+
console.log(`\u2502 Imported responses: ${String(result.importedResponses).padStart(8)} \u2502`);
|
|
9551
|
+
console.log(`\u2502 Skipped duplicates: ${String(result.skippedDuplicates).padStart(8)} \u2502`);
|
|
9552
|
+
console.log(`\u2502 Embeddings queued: ${String(embedCount).padStart(8)} \u2502`);
|
|
9553
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
9554
|
+
if (result.errors.length > 0) {
|
|
9555
|
+
console.log(`
|
|
9556
|
+
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
9557
|
+
for (const error of result.errors.slice(0, 5)) {
|
|
9558
|
+
console.log(` - ${error}`);
|
|
9559
|
+
}
|
|
9560
|
+
if (result.errors.length > 5) {
|
|
9561
|
+
console.log(` ... and ${result.errors.length - 5} more`);
|
|
9562
|
+
}
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
function sanitizeSegment3(input, fallback) {
|
|
9566
|
+
const v = (input || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9567
|
+
return v || fallback;
|
|
9568
|
+
}
|
|
9569
|
+
async function listMarkdownFiles(root) {
|
|
9570
|
+
const out = [];
|
|
9571
|
+
const stack = [root];
|
|
9572
|
+
while (stack.length > 0) {
|
|
9573
|
+
const dir = stack.pop();
|
|
9574
|
+
const entries = await fs9.promises.readdir(dir, { withFileTypes: true });
|
|
9575
|
+
for (const e of entries) {
|
|
9576
|
+
const full = path9.join(dir, e.name);
|
|
9577
|
+
if (e.isDirectory())
|
|
9578
|
+
stack.push(full);
|
|
9579
|
+
else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md")
|
|
9580
|
+
out.push(full);
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
return out.sort();
|
|
9584
|
+
}
|
|
9585
|
+
function deriveNamespaceCategory(sourceRoot, filePath) {
|
|
9586
|
+
const rel = path9.relative(sourceRoot, filePath);
|
|
9587
|
+
const dirSeg = path9.dirname(rel).split(path9.sep).filter(Boolean);
|
|
9588
|
+
if (dirSeg.length >= 2) {
|
|
9589
|
+
const namespace = sanitizeSegment3(dirSeg[0], "default");
|
|
9590
|
+
const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
|
|
9591
|
+
return { namespace, categoryPath: categoryPath.length > 0 ? categoryPath : ["uncategorized"] };
|
|
9592
|
+
}
|
|
9593
|
+
return { namespace: "default", categoryPath: ["uncategorized"] };
|
|
9594
|
+
}
|
|
9595
|
+
function extractImportEvidence(markdown) {
|
|
9596
|
+
const confidenceMatch = markdown.match(/^-\s*confidence:\s*([^\n]+)/m);
|
|
9597
|
+
const sources = markdown.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("- source:")).map((line) => line.replace(/^-\s*source:\s*/i, "").trim()).filter(Boolean).slice(0, 30);
|
|
9598
|
+
return {
|
|
9599
|
+
confidence: confidenceMatch ? confidenceMatch[1].trim() : void 0,
|
|
9600
|
+
sources
|
|
9601
|
+
};
|
|
9602
|
+
}
|
|
9603
|
+
program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
|
|
9604
|
+
const projectPath = options.project || process.cwd();
|
|
9605
|
+
const sessionId = options.session || "import:organized";
|
|
9606
|
+
const sourceRoot = path9.resolve(sourceDir || options.out || projectPath);
|
|
9607
|
+
const repoPath = path9.resolve(options.repo || projectPath);
|
|
9608
|
+
if (!fs9.existsSync(sourceRoot)) {
|
|
9609
|
+
fs9.mkdirSync(sourceRoot, { recursive: true });
|
|
9610
|
+
}
|
|
9611
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
9612
|
+
try {
|
|
9613
|
+
let activeSourceRoot = sourceRoot;
|
|
9614
|
+
let importRoot = sourceRoot;
|
|
9615
|
+
let files = await listMarkdownFiles(importRoot);
|
|
9616
|
+
const hasMarkdown = files.length > 0;
|
|
9617
|
+
const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
|
|
9618
|
+
if (shouldBootstrap) {
|
|
9619
|
+
const outDir = path9.resolve(options.out || path9.join(sourceRoot, "bootstrap-kb"));
|
|
9620
|
+
const since = options.since || "180 days ago";
|
|
9621
|
+
const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
|
|
9622
|
+
console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
|
|
9623
|
+
const bootstrap = await bootstrapKnowledgeBase({
|
|
9624
|
+
repoPath,
|
|
9625
|
+
outDir,
|
|
9626
|
+
since,
|
|
9627
|
+
maxCommits,
|
|
9628
|
+
incremental: options.incremental
|
|
9629
|
+
});
|
|
9630
|
+
console.log(` Repo: ${repoPath}`);
|
|
9631
|
+
console.log(` Output: ${bootstrap.outDir}`);
|
|
9632
|
+
console.log(` Files analyzed: ${bootstrap.fileCount}`);
|
|
9633
|
+
console.log(` Commits analyzed: ${bootstrap.commitCount}`);
|
|
9634
|
+
console.log(` Modules: ${bootstrap.moduleCount}`);
|
|
9635
|
+
activeSourceRoot = outDir;
|
|
9636
|
+
importRoot = outDir;
|
|
9637
|
+
files = await listMarkdownFiles(importRoot);
|
|
9638
|
+
}
|
|
9639
|
+
if (files.length === 0) {
|
|
9640
|
+
console.error("\n\u274C organize-import found no markdown files to import.\n");
|
|
9641
|
+
process.exit(1);
|
|
9642
|
+
}
|
|
9643
|
+
const limit = options.limit ? Math.max(1, parseInt(options.limit, 10)) : files.length;
|
|
9644
|
+
const targets = files.slice(0, limit);
|
|
9645
|
+
console.log(`
|
|
9646
|
+
\u{1F4E6} organize-import`);
|
|
9647
|
+
console.log(` Source: ${activeSourceRoot}`);
|
|
9648
|
+
console.log(` Project: ${projectPath}`);
|
|
9649
|
+
console.log(` Files: ${targets.length}${targets.length < files.length ? `/${files.length}` : ""}`);
|
|
9650
|
+
console.log(` Dry-run: ${options.dryRun ? "yes" : "no"}
|
|
9651
|
+
`);
|
|
9652
|
+
if (!options.dryRun) {
|
|
9653
|
+
await service.initialize();
|
|
9654
|
+
}
|
|
9655
|
+
let imported = 0;
|
|
9656
|
+
let skipped = 0;
|
|
9657
|
+
for (const file of targets) {
|
|
9658
|
+
const text = await fs9.promises.readFile(file, "utf8");
|
|
9659
|
+
if (!text.trim()) {
|
|
9660
|
+
skipped += 1;
|
|
9661
|
+
continue;
|
|
9662
|
+
}
|
|
9663
|
+
const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
|
|
9664
|
+
const rel = path9.relative(activeSourceRoot, file);
|
|
9665
|
+
const evidence = extractImportEvidence(text);
|
|
9666
|
+
if (options.dryRun) {
|
|
9667
|
+
console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);
|
|
9668
|
+
continue;
|
|
9669
|
+
}
|
|
9670
|
+
await service.storeSessionSummary(sessionId, text, {
|
|
9671
|
+
namespace,
|
|
9672
|
+
categoryPath,
|
|
9673
|
+
confidence: evidence.confidence,
|
|
9674
|
+
sources: evidence.sources,
|
|
9675
|
+
import: {
|
|
9676
|
+
sourceFile: rel,
|
|
9677
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9678
|
+
bootstrap: shouldBootstrap === true
|
|
9679
|
+
}
|
|
9680
|
+
});
|
|
9681
|
+
imported += 1;
|
|
9682
|
+
}
|
|
9683
|
+
if (!options.dryRun) {
|
|
9684
|
+
const embed = await service.processPendingEmbeddings();
|
|
9685
|
+
await service.shutdown();
|
|
9686
|
+
console.log(`
|
|
9687
|
+
\u2705 Imported: ${imported}, skipped-empty: ${skipped}, embeddings: ${embed}
|
|
9688
|
+
`);
|
|
9689
|
+
} else {
|
|
9690
|
+
console.log(`
|
|
9691
|
+
\u2705 Dry-run complete (planned imports: ${targets.length - skipped}, skipped-empty: ${skipped})
|
|
9692
|
+
`);
|
|
9693
|
+
}
|
|
9694
|
+
} catch (error) {
|
|
9695
|
+
console.error("\n\u274C organize-import failed:", error);
|
|
9696
|
+
process.exit(1);
|
|
9697
|
+
}
|
|
9698
|
+
});
|
|
9699
|
+
program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-f, --force", "Force reimport: delete existing events and reimport with turn_id grouping").option("-v, --verbose", "Show detailed progress").action(async (options) => {
|
|
9700
|
+
const startTime = Date.now();
|
|
6487
9701
|
const targetProjectPath = options.project || process.cwd();
|
|
6488
9702
|
const service = getMemoryServiceForProject(targetProjectPath);
|
|
6489
9703
|
const importer = createSessionHistoryImporter(service);
|
|
9704
|
+
const importOpts = {
|
|
9705
|
+
limit: options.limit ? parseInt(options.limit) : void 0,
|
|
9706
|
+
force: options.force,
|
|
9707
|
+
verbose: options.verbose,
|
|
9708
|
+
onProgress: renderProgress
|
|
9709
|
+
};
|
|
6490
9710
|
try {
|
|
9711
|
+
console.log("\n\u23F3 Initializing memory service...");
|
|
6491
9712
|
await service.initialize();
|
|
9713
|
+
console.log(" \u2705 Ready\n");
|
|
9714
|
+
if (options.force) {
|
|
9715
|
+
console.log("\u{1F504} Force mode: existing events will be deleted and reimported with turn_id grouping\n");
|
|
9716
|
+
}
|
|
6492
9717
|
let result;
|
|
6493
9718
|
if (options.session) {
|
|
6494
|
-
console.log(`
|
|
6495
|
-
|
|
6496
|
-
console.log(` Target project: ${targetProjectPath}
|
|
9719
|
+
console.log(`\u{1F4E5} Importing session: ${options.session}`);
|
|
9720
|
+
console.log(` Target: ${targetProjectPath}
|
|
6497
9721
|
`);
|
|
6498
9722
|
result = await importer.importSessionFile(options.session, {
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
verbose: options.verbose
|
|
9723
|
+
...importOpts,
|
|
9724
|
+
projectPath: targetProjectPath
|
|
6502
9725
|
});
|
|
6503
9726
|
} else if (options.project) {
|
|
6504
|
-
console.log(
|
|
6505
|
-
\u{1F4E5} Importing project: ${options.project}
|
|
9727
|
+
console.log(`\u{1F4E5} Importing project: ${options.project}
|
|
6506
9728
|
`);
|
|
6507
|
-
result = await importer.importProject(options.project,
|
|
6508
|
-
limit: options.limit ? parseInt(options.limit) : void 0,
|
|
6509
|
-
verbose: options.verbose
|
|
6510
|
-
});
|
|
9729
|
+
result = await importer.importProject(options.project, importOpts);
|
|
6511
9730
|
} else if (options.all) {
|
|
6512
|
-
console.log("\
|
|
9731
|
+
console.log("\u{1F4E5} Importing all sessions from all projects");
|
|
6513
9732
|
console.log(" \u26A0\uFE0F Using global storage (use -p for project-specific)\n");
|
|
6514
9733
|
const globalService = getDefaultMemoryService();
|
|
6515
9734
|
const globalImporter = createSessionHistoryImporter(globalService);
|
|
6516
9735
|
await globalService.initialize();
|
|
6517
|
-
result = await globalImporter.importAll(
|
|
6518
|
-
|
|
6519
|
-
verbose: options.verbose
|
|
6520
|
-
});
|
|
6521
|
-
console.log("\n\u23F3 Processing embeddings...");
|
|
9736
|
+
result = await globalImporter.importAll(importOpts);
|
|
9737
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6522
9738
|
const embedCount2 = await globalService.processPendingEmbeddings();
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
console.log(`
|
|
6526
|
-
|
|
6527
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6528
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6529
|
-
console.log(`Embeddings processed: ${embedCount2}`);
|
|
6530
|
-
if (result.errors.length > 0) {
|
|
6531
|
-
console.log(`
|
|
6532
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6533
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6534
|
-
console.log(` - ${error}`);
|
|
6535
|
-
}
|
|
6536
|
-
if (result.errors.length > 5) {
|
|
6537
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6538
|
-
}
|
|
6539
|
-
}
|
|
9739
|
+
const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
9740
|
+
printImportSummary(result, embedCount2);
|
|
9741
|
+
console.log(`
|
|
9742
|
+
\u23F1\uFE0F Completed in ${elapsed2}s`);
|
|
6540
9743
|
await globalService.shutdown();
|
|
6541
9744
|
return;
|
|
6542
9745
|
} else {
|
|
6543
9746
|
const cwd = process.cwd();
|
|
6544
|
-
console.log(
|
|
6545
|
-
\u{1F4E5} Importing sessions for current project: ${cwd}
|
|
9747
|
+
console.log(`\u{1F4E5} Importing sessions for: ${cwd}
|
|
6546
9748
|
`);
|
|
6547
9749
|
result = await importer.importProject(cwd, {
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
verbose: options.verbose
|
|
9750
|
+
...importOpts,
|
|
9751
|
+
projectPath: cwd
|
|
6551
9752
|
});
|
|
6552
9753
|
}
|
|
6553
|
-
console.log("\n\
|
|
9754
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6554
9755
|
const embedCount = await service.processPendingEmbeddings();
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
console.log(`
|
|
6558
|
-
|
|
6559
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6560
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6561
|
-
console.log(`Embeddings processed: ${embedCount}`);
|
|
6562
|
-
if (result.errors.length > 0) {
|
|
6563
|
-
console.log(`
|
|
6564
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6565
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6566
|
-
console.log(` - ${error}`);
|
|
6567
|
-
}
|
|
6568
|
-
if (result.errors.length > 5) {
|
|
6569
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6570
|
-
}
|
|
6571
|
-
}
|
|
9756
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
9757
|
+
printImportSummary(result, embedCount);
|
|
9758
|
+
console.log(`
|
|
9759
|
+
\u23F1\uFE0F Completed in ${elapsed}s`);
|
|
6572
9760
|
await service.shutdown();
|
|
6573
9761
|
} catch (error) {
|
|
6574
|
-
console.error("Import failed:", error);
|
|
9762
|
+
console.error("\n\u274C Import failed:", error);
|
|
6575
9763
|
process.exit(1);
|
|
6576
9764
|
}
|
|
6577
9765
|
});
|