feique 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +220 -0
- package/README.md +265 -0
- package/dist/backend/claude.d.ts +36 -0
- package/dist/backend/claude.js +358 -0
- package/dist/backend/claude.js.map +1 -0
- package/dist/backend/codex.d.ts +31 -0
- package/dist/backend/codex.js +100 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +9 -0
- package/dist/backend/factory.js +56 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/types.d.ts +54 -0
- package/dist/backend/types.js +2 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/bridge/commands.d.ts +135 -0
- package/dist/bridge/commands.js +860 -0
- package/dist/bridge/commands.js.map +1 -0
- package/dist/bridge/service.d.ts +160 -0
- package/dist/bridge/service.js +3785 -0
- package/dist/bridge/service.js.map +1 -0
- package/dist/bridge/task-queue.d.ts +14 -0
- package/dist/bridge/task-queue.js +81 -0
- package/dist/bridge/task-queue.js.map +1 -0
- package/dist/bridge/types.d.ts +39 -0
- package/dist/bridge/types.js +2 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1199 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/capabilities.d.ts +20 -0
- package/dist/codex/capabilities.js +41 -0
- package/dist/codex/capabilities.js.map +1 -0
- package/dist/codex/runner.d.ts +47 -0
- package/dist/codex/runner.js +294 -0
- package/dist/codex/runner.js.map +1 -0
- package/dist/codex/session-index.d.ts +22 -0
- package/dist/codex/session-index.js +205 -0
- package/dist/codex/session-index.js.map +1 -0
- package/dist/collaboration/awareness.d.ts +36 -0
- package/dist/collaboration/awareness.js +107 -0
- package/dist/collaboration/awareness.js.map +1 -0
- package/dist/collaboration/digest.d.ts +65 -0
- package/dist/collaboration/digest.js +178 -0
- package/dist/collaboration/digest.js.map +1 -0
- package/dist/collaboration/handoff.d.ts +66 -0
- package/dist/collaboration/handoff.js +94 -0
- package/dist/collaboration/handoff.js.map +1 -0
- package/dist/collaboration/insights.d.ts +24 -0
- package/dist/collaboration/insights.js +243 -0
- package/dist/collaboration/insights.js.map +1 -0
- package/dist/collaboration/knowledge.d.ts +26 -0
- package/dist/collaboration/knowledge.js +105 -0
- package/dist/collaboration/knowledge.js.map +1 -0
- package/dist/collaboration/timeline.d.ts +31 -0
- package/dist/collaboration/timeline.js +150 -0
- package/dist/collaboration/timeline.js.map +1 -0
- package/dist/collaboration/trust.d.ts +49 -0
- package/dist/collaboration/trust.js +176 -0
- package/dist/collaboration/trust.js.map +1 -0
- package/dist/config/codex-skill.d.ts +7 -0
- package/dist/config/codex-skill.js +44 -0
- package/dist/config/codex-skill.js.map +1 -0
- package/dist/config/doctor.d.ts +12 -0
- package/dist/config/doctor.js +314 -0
- package/dist/config/doctor.js.map +1 -0
- package/dist/config/init.d.ts +3 -0
- package/dist/config/init.js +123 -0
- package/dist/config/init.js.map +1 -0
- package/dist/config/load.d.ts +33 -0
- package/dist/config/load.js +252 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/mutate.d.ts +21 -0
- package/dist/config/mutate.js +86 -0
- package/dist/config/mutate.js.map +1 -0
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +308 -0
- package/dist/config/schema.js +250 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/control-plane/project-session.d.ts +67 -0
- package/dist/control-plane/project-session.js +234 -0
- package/dist/control-plane/project-session.js.map +1 -0
- package/dist/feishu/base.d.ts +19 -0
- package/dist/feishu/base.js +93 -0
- package/dist/feishu/base.js.map +1 -0
- package/dist/feishu/cards.d.ts +22 -0
- package/dist/feishu/cards.js +144 -0
- package/dist/feishu/cards.js.map +1 -0
- package/dist/feishu/client.d.ts +61 -0
- package/dist/feishu/client.js +315 -0
- package/dist/feishu/client.js.map +1 -0
- package/dist/feishu/diagnostics.d.ts +42 -0
- package/dist/feishu/diagnostics.js +194 -0
- package/dist/feishu/diagnostics.js.map +1 -0
- package/dist/feishu/doc.d.ts +13 -0
- package/dist/feishu/doc.js +59 -0
- package/dist/feishu/doc.js.map +1 -0
- package/dist/feishu/extractors.d.ts +7 -0
- package/dist/feishu/extractors.js +215 -0
- package/dist/feishu/extractors.js.map +1 -0
- package/dist/feishu/long-connection.d.ts +12 -0
- package/dist/feishu/long-connection.js +41 -0
- package/dist/feishu/long-connection.js.map +1 -0
- package/dist/feishu/message-resource.d.ts +14 -0
- package/dist/feishu/message-resource.js +309 -0
- package/dist/feishu/message-resource.js.map +1 -0
- package/dist/feishu/replay.d.ts +37 -0
- package/dist/feishu/replay.js +114 -0
- package/dist/feishu/replay.js.map +1 -0
- package/dist/feishu/task.d.ts +18 -0
- package/dist/feishu/task.js +86 -0
- package/dist/feishu/task.js.map +1 -0
- package/dist/feishu/text.d.ts +23 -0
- package/dist/feishu/text.js +155 -0
- package/dist/feishu/text.js.map +1 -0
- package/dist/feishu/webhook.d.ts +23 -0
- package/dist/feishu/webhook.js +130 -0
- package/dist/feishu/webhook.js.map +1 -0
- package/dist/feishu/wiki.d.ts +52 -0
- package/dist/feishu/wiki.js +300 -0
- package/dist/feishu/wiki.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/search.d.ts +11 -0
- package/dist/knowledge/search.js +83 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/logging.d.ts +3 -0
- package/dist/logging.js +40 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.js +1196 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/embedding-factory.d.ts +6 -0
- package/dist/memory/embedding-factory.js +20 -0
- package/dist/memory/embedding-factory.js.map +1 -0
- package/dist/memory/embeddings.d.ts +40 -0
- package/dist/memory/embeddings.js +150 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/ollama-embeddings.d.ts +63 -0
- package/dist/memory/ollama-embeddings.js +215 -0
- package/dist/memory/ollama-embeddings.js.map +1 -0
- package/dist/memory/retrieve.d.ts +17 -0
- package/dist/memory/retrieve.js +29 -0
- package/dist/memory/retrieve.js.map +1 -0
- package/dist/memory/summarize.d.ts +13 -0
- package/dist/memory/summarize.js +58 -0
- package/dist/memory/summarize.js.map +1 -0
- package/dist/observability/cost.d.ts +12 -0
- package/dist/observability/cost.js +22 -0
- package/dist/observability/cost.js.map +1 -0
- package/dist/observability/dashboard-html.d.ts +5 -0
- package/dist/observability/dashboard-html.js +304 -0
- package/dist/observability/dashboard-html.js.map +1 -0
- package/dist/observability/metrics.d.ts +36 -0
- package/dist/observability/metrics.js +230 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/readiness.d.ts +31 -0
- package/dist/observability/readiness.js +57 -0
- package/dist/observability/readiness.js.map +1 -0
- package/dist/observability/server.d.ts +84 -0
- package/dist/observability/server.js +181 -0
- package/dist/observability/server.js.map +1 -0
- package/dist/projects/paths.d.ts +9 -0
- package/dist/projects/paths.js +30 -0
- package/dist/projects/paths.js.map +1 -0
- package/dist/runtime/instance-lock.d.ts +12 -0
- package/dist/runtime/instance-lock.js +99 -0
- package/dist/runtime/instance-lock.js.map +1 -0
- package/dist/runtime/process.d.ts +2 -0
- package/dist/runtime/process.js +43 -0
- package/dist/runtime/process.js.map +1 -0
- package/dist/runtime/shutdown.d.ts +11 -0
- package/dist/runtime/shutdown.js +38 -0
- package/dist/runtime/shutdown.js.map +1 -0
- package/dist/security/access.d.ts +13 -0
- package/dist/security/access.js +160 -0
- package/dist/security/access.js.map +1 -0
- package/dist/service/install.d.ts +19 -0
- package/dist/service/install.js +35 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/templates.d.ts +22 -0
- package/dist/service/templates.js +118 -0
- package/dist/service/templates.js.map +1 -0
- package/dist/state/audit-log.d.ts +33 -0
- package/dist/state/audit-log.js +116 -0
- package/dist/state/audit-log.js.map +1 -0
- package/dist/state/config-history-store.d.ts +27 -0
- package/dist/state/config-history-store.js +65 -0
- package/dist/state/config-history-store.js.map +1 -0
- package/dist/state/handoff-store.d.ts +20 -0
- package/dist/state/handoff-store.js +97 -0
- package/dist/state/handoff-store.js.map +1 -0
- package/dist/state/idempotency-store.d.ts +19 -0
- package/dist/state/idempotency-store.js +84 -0
- package/dist/state/idempotency-store.js.map +1 -0
- package/dist/state/memory-store.d.ts +137 -0
- package/dist/state/memory-store.js +713 -0
- package/dist/state/memory-store.js.map +1 -0
- package/dist/state/pending-command-store.d.ts +30 -0
- package/dist/state/pending-command-store.js +108 -0
- package/dist/state/pending-command-store.js.map +1 -0
- package/dist/state/run-state-store.d.ts +58 -0
- package/dist/state/run-state-store.js +269 -0
- package/dist/state/run-state-store.js.map +1 -0
- package/dist/state/session-store.d.ts +56 -0
- package/dist/state/session-store.js +275 -0
- package/dist/state/session-store.js.map +1 -0
- package/dist/state/trust-store.d.ts +15 -0
- package/dist/state/trust-store.js +53 -0
- package/dist/state/trust-store.js.map +1 -0
- package/dist/utils/fs.d.ts +4 -0
- package/dist/utils/fs.js +26 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +9 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/path.d.ts +3 -0
- package/dist/utils/path.js +22 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/serial-executor.d.ts +5 -0
- package/dist/utils/serial-executor.js +12 -0
- package/dist/utils/serial-executor.js.map +1 -0
- package/package.json +71 -0
- package/skills/feique-session/SKILL.md +27 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { ensureDir } from '../utils/fs.js';
|
|
6
|
+
import { SerialExecutor } from '../utils/serial-executor.js';
|
|
7
|
+
import { LocalEmbeddingProvider, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from '../memory/embeddings.js';
|
|
8
|
+
const MEMORY_SCHEMA_VERSION = 5;
|
|
9
|
+
export class MemoryStore {
|
|
10
|
+
dbPath;
|
|
11
|
+
serial = new SerialExecutor();
|
|
12
|
+
embedder;
|
|
13
|
+
_db = null;
|
|
14
|
+
_schemaReady = false;
|
|
15
|
+
_lastTs = 0;
|
|
16
|
+
constructor(stateDir, embedder) {
|
|
17
|
+
this.dbPath = path.join(stateDir, 'memory.db');
|
|
18
|
+
this.embedder = embedder ?? new LocalEmbeddingProvider();
|
|
19
|
+
}
|
|
20
|
+
/** Return an ISO timestamp that is strictly greater than any previous call. */
|
|
21
|
+
monotonicNow() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
this._lastTs = now > this._lastTs ? now : this._lastTs + 1;
|
|
24
|
+
return new Date(this._lastTs).toISOString();
|
|
25
|
+
}
|
|
26
|
+
async upsertThreadSummary(record) {
|
|
27
|
+
return this.serial.run(async () => {
|
|
28
|
+
const now = this.monotonicNow();
|
|
29
|
+
return this.withDb((db) => {
|
|
30
|
+
db.prepare(`
|
|
31
|
+
INSERT INTO thread_summaries (
|
|
32
|
+
conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
|
|
33
|
+
files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
|
|
34
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
|
+
ON CONFLICT(conversation_key, project_alias, thread_id) DO UPDATE SET
|
|
36
|
+
summary = excluded.summary,
|
|
37
|
+
recent_prompt = excluded.recent_prompt,
|
|
38
|
+
recent_response_excerpt = excluded.recent_response_excerpt,
|
|
39
|
+
files_touched_json = excluded.files_touched_json,
|
|
40
|
+
open_tasks_json = excluded.open_tasks_json,
|
|
41
|
+
decisions_json = excluded.decisions_json,
|
|
42
|
+
updated_at = excluded.updated_at
|
|
43
|
+
`).run(record.conversation_key, record.project_alias, record.thread_id, record.summary, record.recent_prompt ?? null, record.recent_response_excerpt ?? null, JSON.stringify(record.files_touched), JSON.stringify(record.open_tasks), JSON.stringify(record.decisions), now, now);
|
|
44
|
+
const row = db.prepare(`
|
|
45
|
+
SELECT conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
|
|
46
|
+
files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
|
|
47
|
+
FROM thread_summaries
|
|
48
|
+
WHERE conversation_key = ? AND project_alias = ? AND thread_id = ?
|
|
49
|
+
`).get(record.conversation_key, record.project_alias, record.thread_id);
|
|
50
|
+
return mapThreadSummaryRow(row);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async getThreadSummary(conversationKey, projectAlias, threadId) {
|
|
55
|
+
await this.serial.wait();
|
|
56
|
+
return this.withDb((db) => {
|
|
57
|
+
const row = db.prepare(`
|
|
58
|
+
SELECT conversation_key, project_alias, thread_id, summary, recent_prompt, recent_response_excerpt,
|
|
59
|
+
files_touched_json, open_tasks_json, decisions_json, created_at, updated_at
|
|
60
|
+
FROM thread_summaries
|
|
61
|
+
WHERE conversation_key = ? AND project_alias = ? AND thread_id = ?
|
|
62
|
+
`).get(conversationKey, projectAlias, threadId);
|
|
63
|
+
return row ? mapThreadSummaryRow(row) : null;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async saveMemory(input) {
|
|
67
|
+
return this.serial.run(async () => {
|
|
68
|
+
const now = this.monotonicNow();
|
|
69
|
+
const record = {
|
|
70
|
+
id: randomUUID(),
|
|
71
|
+
scope: input.scope,
|
|
72
|
+
project_alias: input.project_alias,
|
|
73
|
+
chat_id: input.scope === 'group' ? input.chat_id : undefined,
|
|
74
|
+
title: input.title,
|
|
75
|
+
content: input.content,
|
|
76
|
+
tags: input.tags ?? [],
|
|
77
|
+
source: input.source ?? 'manual',
|
|
78
|
+
pinned: input.pinned ?? false,
|
|
79
|
+
confidence: input.confidence ?? 1,
|
|
80
|
+
created_by: input.created_by,
|
|
81
|
+
created_at: now,
|
|
82
|
+
updated_at: now,
|
|
83
|
+
last_accessed_at: now,
|
|
84
|
+
expires_at: input.expires_at,
|
|
85
|
+
};
|
|
86
|
+
const embeddingText = `${record.title} ${record.content} ${record.tags.join(' ')}`;
|
|
87
|
+
const embeddingJson = serializeEmbedding(await this.embedder.embed(embeddingText));
|
|
88
|
+
return this.withDb((db) => {
|
|
89
|
+
db.prepare(`
|
|
90
|
+
INSERT INTO project_memories (
|
|
91
|
+
id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
|
|
92
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
`).run(record.id, record.scope, record.project_alias, record.chat_id ?? null, record.title, record.content, JSON.stringify(record.tags), record.source, record.pinned ? 1 : 0, record.confidence, record.created_by ?? null, record.created_at, record.updated_at, record.last_accessed_at ?? null, record.expires_at ?? null, embeddingJson);
|
|
94
|
+
return record;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async saveProjectMemory(input) {
|
|
99
|
+
return this.saveMemory({ ...input, scope: 'project' });
|
|
100
|
+
}
|
|
101
|
+
async saveGroupMemory(input) {
|
|
102
|
+
return this.saveMemory({ ...input, scope: 'group' });
|
|
103
|
+
}
|
|
104
|
+
async searchMemories(selector, query, limit, filters) {
|
|
105
|
+
await this.serial.wait();
|
|
106
|
+
const touchedAt = this.monotonicNow();
|
|
107
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
108
|
+
return this.withDb((db) => {
|
|
109
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector, filters);
|
|
110
|
+
// Phase 1: FTS5 keyword search (works for both Chinese and English)
|
|
111
|
+
const ftsQuery = buildFtsQuery(query);
|
|
112
|
+
let ftsRows = [];
|
|
113
|
+
if (ftsQuery) {
|
|
114
|
+
const { whereClause: joinWhere, params: joinParams } = buildMemorySelectorWhere(selector, filters, undefined, 'pm');
|
|
115
|
+
ftsRows = db.prepare(`
|
|
116
|
+
SELECT pm.id, pm.scope, pm.project_alias, pm.chat_id, pm.title, pm.content, pm.tags_json, pm.source, pm.pinned, pm.confidence, pm.created_by, pm.created_at, pm.updated_at, pm.last_accessed_at, pm.expires_at, pm.embedding_json
|
|
117
|
+
FROM memory_fts
|
|
118
|
+
JOIN project_memories pm ON pm.rowid = memory_fts.rowid
|
|
119
|
+
WHERE ${joinWhere}
|
|
120
|
+
AND memory_fts MATCH ?
|
|
121
|
+
ORDER BY pm.pinned DESC, bm25(memory_fts), pm.updated_at DESC
|
|
122
|
+
LIMIT ?
|
|
123
|
+
`).all(...joinParams, ftsQuery, limit * 2);
|
|
124
|
+
}
|
|
125
|
+
// Phase 2: Vector similarity search (semantic matching)
|
|
126
|
+
const allRows = db.prepare(`
|
|
127
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
|
|
128
|
+
FROM project_memories
|
|
129
|
+
WHERE ${whereClause}
|
|
130
|
+
AND embedding_json IS NOT NULL
|
|
131
|
+
ORDER BY updated_at DESC
|
|
132
|
+
LIMIT ?
|
|
133
|
+
`).all(...params, 200);
|
|
134
|
+
const vectorScored = [];
|
|
135
|
+
for (const row of allRows) {
|
|
136
|
+
const embJson = row.embedding_json;
|
|
137
|
+
if (!embJson)
|
|
138
|
+
continue;
|
|
139
|
+
const embedding = deserializeEmbedding(embJson);
|
|
140
|
+
if (!embedding)
|
|
141
|
+
continue;
|
|
142
|
+
const score = cosineSimilarity(queryEmbedding, embedding);
|
|
143
|
+
if (score > 0.35) {
|
|
144
|
+
vectorScored.push({ row, score });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
vectorScored.sort((a, b) => b.score - a.score);
|
|
148
|
+
// Phase 3: Merge and deduplicate
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const merged = [];
|
|
151
|
+
// FTS results first (keyword relevance)
|
|
152
|
+
for (const row of ftsRows) {
|
|
153
|
+
if (!seen.has(row.id)) {
|
|
154
|
+
seen.add(row.id);
|
|
155
|
+
merged.push(row);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Then vector results (semantic relevance)
|
|
159
|
+
for (const { row } of vectorScored) {
|
|
160
|
+
if (!seen.has(row.id)) {
|
|
161
|
+
seen.add(row.id);
|
|
162
|
+
merged.push(row);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Fallback: LIKE search if both FTS and vector returned nothing
|
|
166
|
+
if (merged.length === 0) {
|
|
167
|
+
const normalized = `%${query.trim().toLowerCase()}%`;
|
|
168
|
+
const fallbackRows = db.prepare(`
|
|
169
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at, embedding_json
|
|
170
|
+
FROM project_memories
|
|
171
|
+
WHERE ${whereClause}
|
|
172
|
+
AND (
|
|
173
|
+
lower(title) LIKE ?
|
|
174
|
+
OR lower(content) LIKE ?
|
|
175
|
+
OR lower(tags_json) LIKE ?
|
|
176
|
+
)
|
|
177
|
+
ORDER BY pinned DESC, updated_at DESC
|
|
178
|
+
LIMIT ?
|
|
179
|
+
`).all(...params, normalized, normalized, normalized, limit);
|
|
180
|
+
return touchRowsAndMap(db, fallbackRows, touchedAt);
|
|
181
|
+
}
|
|
182
|
+
// Pinned first, then limit
|
|
183
|
+
merged.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0));
|
|
184
|
+
const records = touchRowsAndMap(db, merged.slice(0, limit), touchedAt);
|
|
185
|
+
// Post-filter on mapped records: ensure expired/archived records don't leak
|
|
186
|
+
const now = new Date().toISOString();
|
|
187
|
+
return records.filter((r) => !r.archived_at && (!r.expires_at || r.expires_at > now));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async searchProjectMemories(projectAlias, query, limit, filters) {
|
|
191
|
+
return this.searchMemories({ scope: 'project', project_alias: projectAlias }, query, limit, filters);
|
|
192
|
+
}
|
|
193
|
+
async searchGroupMemories(projectAlias, chatId, query, limit, filters) {
|
|
194
|
+
return this.searchMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId }, query, limit, filters);
|
|
195
|
+
}
|
|
196
|
+
async getMemory(selector, id) {
|
|
197
|
+
await this.serial.wait();
|
|
198
|
+
return this.withDb((db) => {
|
|
199
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector);
|
|
200
|
+
const row = db.prepare(`
|
|
201
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
|
|
202
|
+
FROM project_memories
|
|
203
|
+
WHERE id = ?
|
|
204
|
+
AND ${whereClause}
|
|
205
|
+
`).get(id, ...params);
|
|
206
|
+
return row ? mapMemoryRow(row) : null;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async getMemoryById(selector, id, options) {
|
|
210
|
+
await this.serial.wait();
|
|
211
|
+
return this.withDb((db) => {
|
|
212
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector, undefined, options);
|
|
213
|
+
const row = db.prepare(`
|
|
214
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
|
|
215
|
+
FROM project_memories
|
|
216
|
+
WHERE id = ?
|
|
217
|
+
AND ${whereClause}
|
|
218
|
+
`).get(id, ...params);
|
|
219
|
+
return row ? mapMemoryRow(row) : null;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async listRecentMemories(selector, limit, filters) {
|
|
223
|
+
await this.serial.wait();
|
|
224
|
+
const touchedAt = this.monotonicNow();
|
|
225
|
+
return this.withDb((db) => {
|
|
226
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector, filters);
|
|
227
|
+
const rows = db.prepare(`
|
|
228
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
|
|
229
|
+
FROM project_memories
|
|
230
|
+
WHERE ${whereClause}
|
|
231
|
+
ORDER BY pinned DESC, updated_at DESC
|
|
232
|
+
LIMIT ?
|
|
233
|
+
`).all(...params, limit);
|
|
234
|
+
return touchRowsAndMap(db, rows, touchedAt);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async listRecentProjectMemories(projectAlias, limit, filters) {
|
|
238
|
+
return this.listRecentMemories({ scope: 'project', project_alias: projectAlias }, limit, filters);
|
|
239
|
+
}
|
|
240
|
+
async listRecentGroupMemories(projectAlias, chatId, limit, filters) {
|
|
241
|
+
return this.listRecentMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId }, limit, filters);
|
|
242
|
+
}
|
|
243
|
+
async countMemories(selector) {
|
|
244
|
+
await this.serial.wait();
|
|
245
|
+
return this.withDb((db) => {
|
|
246
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector);
|
|
247
|
+
const row = db.prepare(`
|
|
248
|
+
SELECT COUNT(*) AS count
|
|
249
|
+
FROM project_memories
|
|
250
|
+
WHERE ${whereClause}
|
|
251
|
+
`).get(...params);
|
|
252
|
+
return Number(row.count ?? 0);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async countProjectMemories(projectAlias) {
|
|
256
|
+
return this.countMemories({ scope: 'project', project_alias: projectAlias });
|
|
257
|
+
}
|
|
258
|
+
async countGroupMemories(projectAlias, chatId) {
|
|
259
|
+
return this.countMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId });
|
|
260
|
+
}
|
|
261
|
+
async countPinnedMemories(selector) {
|
|
262
|
+
await this.serial.wait();
|
|
263
|
+
return this.withDb((db) => {
|
|
264
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector);
|
|
265
|
+
const row = db.prepare(`
|
|
266
|
+
SELECT COUNT(*) AS count
|
|
267
|
+
FROM project_memories
|
|
268
|
+
WHERE ${whereClause}
|
|
269
|
+
AND pinned = 1
|
|
270
|
+
`).get(...params);
|
|
271
|
+
return Number(row.count ?? 0);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async countPinnedProjectMemories(projectAlias) {
|
|
275
|
+
return this.countPinnedMemories({ scope: 'project', project_alias: projectAlias });
|
|
276
|
+
}
|
|
277
|
+
async countPinnedGroupMemories(projectAlias, chatId) {
|
|
278
|
+
return this.countPinnedMemories({ scope: 'group', project_alias: projectAlias, chat_id: chatId });
|
|
279
|
+
}
|
|
280
|
+
async getMemoryStats(selector) {
|
|
281
|
+
await this.serial.wait();
|
|
282
|
+
return this.withDb((db) => {
|
|
283
|
+
const now = this.monotonicNow();
|
|
284
|
+
const params = [selector.scope, selector.project_alias];
|
|
285
|
+
const baseClauses = ['scope = ?', 'project_alias = ?'];
|
|
286
|
+
if (selector.scope === 'group') {
|
|
287
|
+
baseClauses.push('chat_id = ?');
|
|
288
|
+
params.push(selector.chat_id ?? null);
|
|
289
|
+
}
|
|
290
|
+
const baseWhere = baseClauses.join(' AND ');
|
|
291
|
+
const row = db.prepare(`
|
|
292
|
+
SELECT
|
|
293
|
+
SUM(CASE WHEN archived_at IS NULL AND (expires_at IS NULL OR expires_at > ?) THEN 1 ELSE 0 END) AS active_count,
|
|
294
|
+
SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS archived_count,
|
|
295
|
+
SUM(CASE WHEN archived_at IS NULL AND expires_at IS NOT NULL AND expires_at <= ? THEN 1 ELSE 0 END) AS expired_count,
|
|
296
|
+
SUM(CASE WHEN archived_at IS NULL AND pinned = 1 AND (expires_at IS NULL OR expires_at > ?) THEN 1 ELSE 0 END) AS pinned_count,
|
|
297
|
+
MAX(updated_at) AS latest_updated_at,
|
|
298
|
+
MAX(last_accessed_at) AS latest_accessed_at,
|
|
299
|
+
MAX(archived_at) AS latest_archived_at
|
|
300
|
+
FROM project_memories
|
|
301
|
+
WHERE ${baseWhere}
|
|
302
|
+
`).get(now, now, now, ...params);
|
|
303
|
+
return {
|
|
304
|
+
active_count: Number(row.active_count ?? 0),
|
|
305
|
+
archived_count: Number(row.archived_count ?? 0),
|
|
306
|
+
expired_count: Number(row.expired_count ?? 0),
|
|
307
|
+
pinned_count: Number(row.pinned_count ?? 0),
|
|
308
|
+
latest_updated_at: row.latest_updated_at ?? undefined,
|
|
309
|
+
latest_accessed_at: row.latest_accessed_at ?? undefined,
|
|
310
|
+
latest_archived_at: row.latest_archived_at ?? undefined,
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async cleanupExpiredMemories(selector) {
|
|
315
|
+
return this.serial.run(async () => {
|
|
316
|
+
return this.withDb((db) => {
|
|
317
|
+
const now = this.monotonicNow();
|
|
318
|
+
const clauses = ['archived_at IS NULL', 'expires_at IS NOT NULL', 'expires_at <= ?'];
|
|
319
|
+
const params = [now];
|
|
320
|
+
if (selector) {
|
|
321
|
+
clauses.push('scope = ?');
|
|
322
|
+
params.push(selector.scope);
|
|
323
|
+
clauses.push('project_alias = ?');
|
|
324
|
+
params.push(selector.project_alias);
|
|
325
|
+
if (selector.scope === 'group') {
|
|
326
|
+
clauses.push('chat_id = ?');
|
|
327
|
+
params.push(selector.chat_id ?? null);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const result = db.prepare(`
|
|
331
|
+
UPDATE project_memories
|
|
332
|
+
SET archived_at = ?, archived_by = ?, archive_reason = ?, pinned = 0, updated_at = ?
|
|
333
|
+
WHERE ${clauses.join(' AND ')}
|
|
334
|
+
`).run(now, 'system', 'expired', now, ...params);
|
|
335
|
+
return Number(result.changes ?? 0);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async getOldestPinnedMemory(selector, basis = 'updated_at') {
|
|
340
|
+
await this.serial.wait();
|
|
341
|
+
return this.withDb((db) => {
|
|
342
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector, {});
|
|
343
|
+
const orderBy = basis === 'last_accessed_at'
|
|
344
|
+
? 'COALESCE(last_accessed_at, updated_at, created_at) ASC, updated_at ASC, rowid ASC'
|
|
345
|
+
: 'updated_at ASC, created_at ASC, rowid ASC';
|
|
346
|
+
const row = db.prepare(`
|
|
347
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
|
|
348
|
+
FROM project_memories
|
|
349
|
+
WHERE ${whereClause}
|
|
350
|
+
AND pinned = 1
|
|
351
|
+
ORDER BY ${orderBy}
|
|
352
|
+
LIMIT 1
|
|
353
|
+
`).get(...params);
|
|
354
|
+
return row ? mapMemoryRow(row) : null;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async setMemoryPinned(selector, id, pinned) {
|
|
358
|
+
return this.serial.run(async () => {
|
|
359
|
+
const now = this.monotonicNow();
|
|
360
|
+
return this.withDb((db) => {
|
|
361
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector);
|
|
362
|
+
const result = db.prepare(`
|
|
363
|
+
UPDATE project_memories
|
|
364
|
+
SET pinned = ?, updated_at = ?, last_accessed_at = ?
|
|
365
|
+
WHERE id = ?
|
|
366
|
+
AND ${whereClause}
|
|
367
|
+
`).run(pinned ? 1 : 0, now, now, id, ...params);
|
|
368
|
+
if (result.changes === 0) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const row = db.prepare(`
|
|
372
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, expires_at
|
|
373
|
+
FROM project_memories
|
|
374
|
+
WHERE id = ?
|
|
375
|
+
AND ${whereClause}
|
|
376
|
+
`).get(id, ...params);
|
|
377
|
+
return row ? mapMemoryRow(row) : null;
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async archiveMemory(selector, id, input) {
|
|
382
|
+
return this.serial.run(async () => {
|
|
383
|
+
const now = this.monotonicNow();
|
|
384
|
+
return this.withDb((db) => {
|
|
385
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector);
|
|
386
|
+
const result = db.prepare(`
|
|
387
|
+
UPDATE project_memories
|
|
388
|
+
SET archived_at = ?, archived_by = ?, archive_reason = ?, pinned = 0, updated_at = ?
|
|
389
|
+
WHERE id = ?
|
|
390
|
+
AND ${whereClause}
|
|
391
|
+
`).run(now, input?.archived_by ?? null, input?.reason ?? 'manual', now, id, ...params);
|
|
392
|
+
if (result.changes === 0) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const row = db.prepare(`
|
|
396
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
|
|
397
|
+
FROM project_memories
|
|
398
|
+
WHERE id = ?
|
|
399
|
+
AND scope = ?
|
|
400
|
+
AND project_alias = ?
|
|
401
|
+
${selector.scope === 'group' ? 'AND chat_id = ?' : ''}
|
|
402
|
+
`).get(id, selector.scope, selector.project_alias, ...(selector.scope === 'group' ? [selector.chat_id ?? null] : []));
|
|
403
|
+
return row ? mapMemoryRow(row) : null;
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async restoreMemory(selector, id, restoredBy) {
|
|
408
|
+
return this.serial.run(async () => {
|
|
409
|
+
const now = this.monotonicNow();
|
|
410
|
+
return this.withDb((db) => {
|
|
411
|
+
const { whereClause, params } = buildMemorySelectorWhere(selector, undefined, { includeArchived: true, includeExpired: true });
|
|
412
|
+
const result = db.prepare(`
|
|
413
|
+
UPDATE project_memories
|
|
414
|
+
SET archived_at = NULL,
|
|
415
|
+
archived_by = NULL,
|
|
416
|
+
archive_reason = NULL,
|
|
417
|
+
updated_at = ?,
|
|
418
|
+
last_accessed_at = ?,
|
|
419
|
+
expires_at = CASE
|
|
420
|
+
WHEN expires_at IS NOT NULL AND expires_at <= ? THEN NULL
|
|
421
|
+
ELSE expires_at
|
|
422
|
+
END
|
|
423
|
+
WHERE id = ?
|
|
424
|
+
AND archived_at IS NOT NULL
|
|
425
|
+
AND ${whereClause}
|
|
426
|
+
`).run(now, now, now, id, ...params);
|
|
427
|
+
if (result.changes === 0) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const restored = db.prepare(`
|
|
431
|
+
SELECT id, scope, project_alias, chat_id, title, content, tags_json, source, pinned, confidence, created_by, created_at, updated_at, last_accessed_at, archived_at, archived_by, archive_reason, expires_at
|
|
432
|
+
FROM project_memories
|
|
433
|
+
WHERE id = ?
|
|
434
|
+
AND ${whereClause}
|
|
435
|
+
`).get(id, ...params);
|
|
436
|
+
if (restoredBy) {
|
|
437
|
+
void restoredBy;
|
|
438
|
+
}
|
|
439
|
+
return restored ? mapMemoryRow(restored) : null;
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async ensureReady() {
|
|
444
|
+
await this.serial.run(async () => {
|
|
445
|
+
await ensureDir(path.dirname(this.dbPath));
|
|
446
|
+
this.getDb();
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
close() {
|
|
450
|
+
if (this._db) {
|
|
451
|
+
this._db.close();
|
|
452
|
+
this._db = null;
|
|
453
|
+
this._schemaReady = false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
getDb() {
|
|
457
|
+
if (!this._db) {
|
|
458
|
+
fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
|
|
459
|
+
this._db = new DatabaseSync(this.dbPath);
|
|
460
|
+
}
|
|
461
|
+
if (!this._schemaReady) {
|
|
462
|
+
initializeSchema(this._db);
|
|
463
|
+
this._schemaReady = true;
|
|
464
|
+
}
|
|
465
|
+
return this._db;
|
|
466
|
+
}
|
|
467
|
+
withDb(callback) {
|
|
468
|
+
return callback(this.getDb());
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function initializeSchema(db) {
|
|
472
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
473
|
+
db.exec(`
|
|
474
|
+
CREATE TABLE IF NOT EXISTS thread_summaries (
|
|
475
|
+
conversation_key TEXT NOT NULL,
|
|
476
|
+
project_alias TEXT NOT NULL,
|
|
477
|
+
thread_id TEXT NOT NULL,
|
|
478
|
+
summary TEXT NOT NULL,
|
|
479
|
+
recent_prompt TEXT,
|
|
480
|
+
recent_response_excerpt TEXT,
|
|
481
|
+
files_touched_json TEXT NOT NULL DEFAULT '[]',
|
|
482
|
+
open_tasks_json TEXT NOT NULL DEFAULT '[]',
|
|
483
|
+
decisions_json TEXT NOT NULL DEFAULT '[]',
|
|
484
|
+
created_at TEXT NOT NULL,
|
|
485
|
+
updated_at TEXT NOT NULL,
|
|
486
|
+
PRIMARY KEY (conversation_key, project_alias, thread_id)
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
CREATE TABLE IF NOT EXISTS project_memories (
|
|
490
|
+
id TEXT PRIMARY KEY,
|
|
491
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
492
|
+
project_alias TEXT NOT NULL,
|
|
493
|
+
chat_id TEXT,
|
|
494
|
+
title TEXT NOT NULL,
|
|
495
|
+
content TEXT NOT NULL,
|
|
496
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
497
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
498
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
499
|
+
confidence REAL NOT NULL DEFAULT 1,
|
|
500
|
+
created_by TEXT,
|
|
501
|
+
created_at TEXT NOT NULL,
|
|
502
|
+
updated_at TEXT NOT NULL,
|
|
503
|
+
last_accessed_at TEXT,
|
|
504
|
+
archived_at TEXT,
|
|
505
|
+
archived_by TEXT,
|
|
506
|
+
archive_reason TEXT,
|
|
507
|
+
expires_at TEXT
|
|
508
|
+
);
|
|
509
|
+
`);
|
|
510
|
+
const userVersion = getUserVersion(db);
|
|
511
|
+
if (userVersion < MEMORY_SCHEMA_VERSION) {
|
|
512
|
+
migrateProjectMemoriesSchema(db);
|
|
513
|
+
db.exec(`
|
|
514
|
+
DROP TRIGGER IF EXISTS project_memories_ai;
|
|
515
|
+
DROP TRIGGER IF EXISTS project_memories_ad;
|
|
516
|
+
DROP TRIGGER IF EXISTS project_memories_au;
|
|
517
|
+
DROP TABLE IF EXISTS memory_fts;
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
db.exec(`
|
|
521
|
+
CREATE INDEX IF NOT EXISTS idx_project_memories_scope_project_updated
|
|
522
|
+
ON project_memories(scope, project_alias, updated_at DESC);
|
|
523
|
+
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_project_memories_scope_project_chat_updated
|
|
525
|
+
ON project_memories(scope, project_alias, chat_id, updated_at DESC);
|
|
526
|
+
|
|
527
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
528
|
+
title,
|
|
529
|
+
content,
|
|
530
|
+
tags_json,
|
|
531
|
+
content='project_memories',
|
|
532
|
+
content_rowid='rowid',
|
|
533
|
+
tokenize='unicode61'
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
CREATE TRIGGER IF NOT EXISTS project_memories_ai AFTER INSERT ON project_memories BEGIN
|
|
537
|
+
INSERT INTO memory_fts(rowid, title, content, tags_json)
|
|
538
|
+
VALUES (new.rowid, new.title, new.content, new.tags_json);
|
|
539
|
+
END;
|
|
540
|
+
|
|
541
|
+
CREATE TRIGGER IF NOT EXISTS project_memories_ad AFTER DELETE ON project_memories BEGIN
|
|
542
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, content, tags_json)
|
|
543
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.tags_json);
|
|
544
|
+
END;
|
|
545
|
+
|
|
546
|
+
CREATE TRIGGER IF NOT EXISTS project_memories_au AFTER UPDATE ON project_memories BEGIN
|
|
547
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, content, tags_json)
|
|
548
|
+
VALUES ('delete', old.rowid, old.title, old.content, old.tags_json);
|
|
549
|
+
INSERT INTO memory_fts(rowid, title, content, tags_json)
|
|
550
|
+
VALUES (new.rowid, new.title, new.content, new.tags_json);
|
|
551
|
+
END;
|
|
552
|
+
`);
|
|
553
|
+
if (userVersion < MEMORY_SCHEMA_VERSION) {
|
|
554
|
+
db.prepare("INSERT INTO memory_fts(memory_fts) VALUES ('rebuild')").run();
|
|
555
|
+
db.exec(`PRAGMA user_version = ${MEMORY_SCHEMA_VERSION};`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function migrateProjectMemoriesSchema(db) {
|
|
559
|
+
const columns = db.prepare('PRAGMA table_info(project_memories)').all();
|
|
560
|
+
const names = new Set(columns.map((column) => column.name));
|
|
561
|
+
if (!names.has('scope')) {
|
|
562
|
+
db.exec("ALTER TABLE project_memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'");
|
|
563
|
+
}
|
|
564
|
+
if (!names.has('chat_id')) {
|
|
565
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN chat_id TEXT');
|
|
566
|
+
}
|
|
567
|
+
if (!names.has('last_accessed_at')) {
|
|
568
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN last_accessed_at TEXT');
|
|
569
|
+
db.exec('UPDATE project_memories SET last_accessed_at = updated_at WHERE last_accessed_at IS NULL');
|
|
570
|
+
}
|
|
571
|
+
if (!names.has('archived_at')) {
|
|
572
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN archived_at TEXT');
|
|
573
|
+
}
|
|
574
|
+
if (!names.has('archived_by')) {
|
|
575
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN archived_by TEXT');
|
|
576
|
+
}
|
|
577
|
+
if (!names.has('archive_reason')) {
|
|
578
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN archive_reason TEXT');
|
|
579
|
+
}
|
|
580
|
+
if (!names.has('embedding_json')) {
|
|
581
|
+
db.exec('ALTER TABLE project_memories ADD COLUMN embedding_json TEXT');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function getUserVersion(db) {
|
|
585
|
+
const row = db.prepare('PRAGMA user_version').get();
|
|
586
|
+
return Number(row?.user_version ?? 0);
|
|
587
|
+
}
|
|
588
|
+
function buildMemorySelectorWhere(selector, filters, options, tablePrefix) {
|
|
589
|
+
const p = tablePrefix ? `${tablePrefix}.` : '';
|
|
590
|
+
const params = [selector.scope, selector.project_alias];
|
|
591
|
+
const clauses = [
|
|
592
|
+
`${p}scope = ?`,
|
|
593
|
+
`${p}project_alias = ?`,
|
|
594
|
+
];
|
|
595
|
+
if (!options?.includeExpired) {
|
|
596
|
+
clauses.push(`(${p}expires_at IS NULL OR ${p}expires_at > ?)`);
|
|
597
|
+
params.push(new Date().toISOString());
|
|
598
|
+
}
|
|
599
|
+
if (!options?.includeArchived) {
|
|
600
|
+
clauses.push(`${p}archived_at IS NULL`);
|
|
601
|
+
}
|
|
602
|
+
if (selector.scope === 'group') {
|
|
603
|
+
clauses.push(`${p}chat_id = ?`);
|
|
604
|
+
params.push(selector.chat_id ?? null);
|
|
605
|
+
}
|
|
606
|
+
if (filters?.source) {
|
|
607
|
+
clauses.push(`${p}source = ?`);
|
|
608
|
+
params.push(filters.source);
|
|
609
|
+
}
|
|
610
|
+
if (filters?.created_by) {
|
|
611
|
+
clauses.push(`${p}created_by = ?`);
|
|
612
|
+
params.push(filters.created_by);
|
|
613
|
+
}
|
|
614
|
+
if (filters?.tag) {
|
|
615
|
+
clauses.push(`lower(${p}tags_json) LIKE ?`);
|
|
616
|
+
params.push(`%${filters.tag.toLowerCase()}%`);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
whereClause: clauses.join(' AND '),
|
|
620
|
+
params,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Build FTS5 query supporting both English and Chinese text.
|
|
625
|
+
* English: word prefix matching (e.g., "auth*")
|
|
626
|
+
* Chinese: character-level matching (FTS5 unicode61 tokenizes CJK per-character)
|
|
627
|
+
*/
|
|
628
|
+
function buildFtsQuery(input) {
|
|
629
|
+
const terms = [];
|
|
630
|
+
// Extract English/numeric tokens → prefix match
|
|
631
|
+
const englishTokens = input.toLowerCase().match(/[a-z][a-z0-9_-]*/g);
|
|
632
|
+
if (englishTokens) {
|
|
633
|
+
for (const token of englishTokens) {
|
|
634
|
+
if (token.length > 1) {
|
|
635
|
+
terms.push(`${token}*`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Extract CJK characters → exact character match
|
|
640
|
+
// FTS5 with unicode61 tokenizes CJK text per-character, so each character is a token
|
|
641
|
+
const cjkChars = input.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g);
|
|
642
|
+
if (cjkChars) {
|
|
643
|
+
for (const char of cjkChars) {
|
|
644
|
+
terms.push(char);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (terms.length === 0) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
// Use OR for mixed queries (find docs matching any term)
|
|
651
|
+
// then rely on BM25 scoring to rank by relevance
|
|
652
|
+
return terms.join(' OR ');
|
|
653
|
+
}
|
|
654
|
+
function mapThreadSummaryRow(row) {
|
|
655
|
+
return {
|
|
656
|
+
conversation_key: row.conversation_key,
|
|
657
|
+
project_alias: row.project_alias,
|
|
658
|
+
thread_id: row.thread_id,
|
|
659
|
+
summary: row.summary,
|
|
660
|
+
recent_prompt: row.recent_prompt ?? undefined,
|
|
661
|
+
recent_response_excerpt: row.recent_response_excerpt ?? undefined,
|
|
662
|
+
files_touched: parseJsonArray(row.files_touched_json),
|
|
663
|
+
open_tasks: parseJsonArray(row.open_tasks_json),
|
|
664
|
+
decisions: parseJsonArray(row.decisions_json),
|
|
665
|
+
created_at: row.created_at,
|
|
666
|
+
updated_at: row.updated_at,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function mapMemoryRow(row) {
|
|
670
|
+
return {
|
|
671
|
+
id: row.id,
|
|
672
|
+
scope: row.scope === 'group' ? 'group' : 'project',
|
|
673
|
+
project_alias: row.project_alias,
|
|
674
|
+
chat_id: row.chat_id ?? undefined,
|
|
675
|
+
title: row.title,
|
|
676
|
+
content: row.content,
|
|
677
|
+
tags: parseJsonArray(row.tags_json),
|
|
678
|
+
source: row.source,
|
|
679
|
+
pinned: row.pinned === 1,
|
|
680
|
+
confidence: row.confidence,
|
|
681
|
+
created_by: row.created_by ?? undefined,
|
|
682
|
+
created_at: row.created_at,
|
|
683
|
+
updated_at: row.updated_at,
|
|
684
|
+
last_accessed_at: row.last_accessed_at ?? undefined,
|
|
685
|
+
archived_at: row.archived_at ?? undefined,
|
|
686
|
+
archived_by: row.archived_by ?? undefined,
|
|
687
|
+
archive_reason: row.archive_reason ?? undefined,
|
|
688
|
+
expires_at: row.expires_at ?? undefined,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function touchRowsAndMap(db, rows, touchedAt) {
|
|
692
|
+
if (rows.length === 0) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
const ids = rows.map((row) => row.id);
|
|
696
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
697
|
+
db.prepare(`
|
|
698
|
+
UPDATE project_memories
|
|
699
|
+
SET last_accessed_at = ?
|
|
700
|
+
WHERE id IN (${placeholders})
|
|
701
|
+
`).run(touchedAt, ...ids);
|
|
702
|
+
return rows.map((row) => mapMemoryRow({ ...row, last_accessed_at: touchedAt }));
|
|
703
|
+
}
|
|
704
|
+
function parseJsonArray(value) {
|
|
705
|
+
try {
|
|
706
|
+
const parsed = JSON.parse(value);
|
|
707
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
//# sourceMappingURL=memory-store.js.map
|