@thread-memory/core 0.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/dist/events.d.ts +18 -0
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -0
- package/dist/events.test.d.ts +1 -0
- package/dist/events.test.js +46 -0
- package/dist/events.test.js.map +1 -0
- package/dist/feedback-guard.d.ts +3 -0
- package/dist/feedback-guard.js +33 -0
- package/dist/feedback-guard.js.map +1 -0
- package/dist/feedback-guard.test.d.ts +1 -0
- package/dist/feedback-guard.test.js +67 -0
- package/dist/feedback-guard.test.js.map +1 -0
- package/dist/governor.d.ts +16 -0
- package/dist/governor.js +25 -0
- package/dist/governor.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +8 -0
- package/dist/index.test.js.map +1 -0
- package/dist/isolation.test.d.ts +1 -0
- package/dist/isolation.test.js +102 -0
- package/dist/isolation.test.js.map +1 -0
- package/dist/light-confirm.d.ts +37 -0
- package/dist/light-confirm.js +149 -0
- package/dist/light-confirm.js.map +1 -0
- package/dist/light-confirm.test.d.ts +1 -0
- package/dist/light-confirm.test.js +154 -0
- package/dist/light-confirm.test.js.map +1 -0
- package/dist/lineage.test.d.ts +1 -0
- package/dist/lineage.test.js +58 -0
- package/dist/lineage.test.js.map +1 -0
- package/dist/migrate.d.ts +45 -0
- package/dist/migrate.js +349 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrate.test.d.ts +1 -0
- package/dist/migrate.test.js +111 -0
- package/dist/migrate.test.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +16 -0
- package/dist/paths.js.map +1 -0
- package/dist/project-key.d.ts +3 -0
- package/dist/project-key.js +48 -0
- package/dist/project-key.js.map +1 -0
- package/dist/project-key.test.d.ts +1 -0
- package/dist/project-key.test.js +29 -0
- package/dist/project-key.test.js.map +1 -0
- package/dist/query.d.ts +49 -0
- package/dist/query.js +165 -0
- package/dist/query.js.map +1 -0
- package/dist/query.test.d.ts +1 -0
- package/dist/query.test.js +162 -0
- package/dist/query.test.js.map +1 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +92 -0
- package/dist/schema.js.map +1 -0
- package/dist/schema.test.d.ts +1 -0
- package/dist/schema.test.js +80 -0
- package/dist/schema.test.js.map +1 -0
- package/dist/state.d.ts +4 -0
- package/dist/state.js +15 -0
- package/dist/state.js.map +1 -0
- package/dist/status-card.d.ts +10 -0
- package/dist/status-card.js +65 -0
- package/dist/status-card.js.map +1 -0
- package/dist/status-card.test.d.ts +1 -0
- package/dist/status-card.test.js +53 -0
- package/dist/status-card.test.js.map +1 -0
- package/dist/store.d.ts +170 -0
- package/dist/store.js +664 -0
- package/dist/store.js.map +1 -0
- package/dist/store.test.d.ts +1 -0
- package/dist/store.test.js +284 -0
- package/dist/store.test.js.map +1 -0
- package/package.json +29 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { MAX_BODY_CHARS, truncateBody } from "./events.js";
|
|
4
|
+
import { assertTransition } from "./state.js";
|
|
5
|
+
import { ensureSchema } from "./schema.js";
|
|
6
|
+
import { SpillPolicy, isIndexable } from "./governor.js";
|
|
7
|
+
export const EVENTS_SCHEMA = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
session_id TEXT NOT NULL,
|
|
11
|
+
kind TEXT NOT NULL,
|
|
12
|
+
ts TEXT NOT NULL,
|
|
13
|
+
seq INTEGER NOT NULL,
|
|
14
|
+
body TEXT NOT NULL,
|
|
15
|
+
meta TEXT,
|
|
16
|
+
truncated INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
project_key TEXT,
|
|
18
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
19
|
+
origin TEXT,
|
|
20
|
+
spilled INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
isolation INTEGER NOT NULL DEFAULT 0
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, seq);
|
|
24
|
+
|
|
25
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
26
|
+
body,
|
|
27
|
+
content='events',
|
|
28
|
+
content_rowid='id',
|
|
29
|
+
tokenize='unicode61'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
session_id TEXT NOT NULL,
|
|
35
|
+
seq_start INTEGER NOT NULL,
|
|
36
|
+
seq_end INTEGER,
|
|
37
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
38
|
+
summary TEXT,
|
|
39
|
+
created_at TEXT NOT NULL
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_session ON episodes(session_id, id);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS spills (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
event_id INTEGER NOT NULL REFERENCES events(id),
|
|
46
|
+
ref TEXT NOT NULL,
|
|
47
|
+
blob TEXT,
|
|
48
|
+
sha256 TEXT NOT NULL,
|
|
49
|
+
created_at TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS lineage_edges (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
src_type TEXT NOT NULL,
|
|
56
|
+
src_id INTEGER,
|
|
57
|
+
dst_type TEXT NOT NULL,
|
|
58
|
+
dst_id INTEGER,
|
|
59
|
+
ref TEXT,
|
|
60
|
+
edge_type TEXT NOT NULL,
|
|
61
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
62
|
+
ts TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_lineage_src ON lineage_edges(session_id, src_type, src_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_lineage_dst ON lineage_edges(session_id, dst_type, dst_id);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
68
|
+
version INTEGER NOT NULL,
|
|
69
|
+
applied_at TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
`;
|
|
72
|
+
export const STRUCTURED_SCHEMA = `
|
|
73
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
session_id TEXT NOT NULL,
|
|
76
|
+
text TEXT NOT NULL,
|
|
77
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
78
|
+
source_event INTEGER,
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
project_key TEXT,
|
|
81
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
82
|
+
origin TEXT,
|
|
83
|
+
isolation INTEGER NOT NULL DEFAULT 0
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_goals_session ON goals(session_id, id);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
88
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
+
session_id TEXT NOT NULL,
|
|
90
|
+
text TEXT NOT NULL,
|
|
91
|
+
status TEXT NOT NULL DEFAULT 'proposed',
|
|
92
|
+
superseded_by INTEGER,
|
|
93
|
+
source_event INTEGER,
|
|
94
|
+
created_at TEXT NOT NULL,
|
|
95
|
+
updated_at TEXT NOT NULL,
|
|
96
|
+
project_key TEXT,
|
|
97
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
98
|
+
origin TEXT,
|
|
99
|
+
isolation INTEGER NOT NULL DEFAULT 0
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_session ON decisions(session_id, status);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
105
|
+
session_id TEXT NOT NULL,
|
|
106
|
+
text TEXT NOT NULL,
|
|
107
|
+
kind TEXT NOT NULL DEFAULT 'preference',
|
|
108
|
+
source_event INTEGER,
|
|
109
|
+
created_at TEXT NOT NULL,
|
|
110
|
+
project_key TEXT,
|
|
111
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
112
|
+
origin TEXT,
|
|
113
|
+
isolation INTEGER NOT NULL DEFAULT 0
|
|
114
|
+
);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_session ON feedback(session_id, id);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS session_isolation (
|
|
118
|
+
session_id TEXT PRIMARY KEY,
|
|
119
|
+
isolated INTEGER NOT NULL DEFAULT 0,
|
|
120
|
+
updated_at TEXT NOT NULL
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS lineage_edges (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
session_id TEXT NOT NULL,
|
|
126
|
+
src_type TEXT NOT NULL,
|
|
127
|
+
src_id INTEGER,
|
|
128
|
+
dst_type TEXT NOT NULL,
|
|
129
|
+
dst_id INTEGER,
|
|
130
|
+
ref TEXT,
|
|
131
|
+
edge_type TEXT NOT NULL,
|
|
132
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
133
|
+
ts TEXT NOT NULL
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_lineage_src ON lineage_edges(session_id, src_type, src_id);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_lineage_dst ON lineage_edges(session_id, dst_type, dst_id);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
139
|
+
version INTEGER NOT NULL,
|
|
140
|
+
applied_at TEXT NOT NULL
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
144
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
145
|
+
name TEXT NOT NULL UNIQUE,
|
|
146
|
+
kind TEXT NOT NULL
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS decision_entities (
|
|
150
|
+
decision_id INTEGER NOT NULL REFERENCES decisions(id),
|
|
151
|
+
entity_id INTEGER NOT NULL REFERENCES entities(id),
|
|
152
|
+
edge TEXT NOT NULL DEFAULT 'references',
|
|
153
|
+
ts TEXT NOT NULL,
|
|
154
|
+
PRIMARY KEY (decision_id, entity_id)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
158
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
159
|
+
event_id INTEGER,
|
|
160
|
+
name TEXT NOT NULL,
|
|
161
|
+
value REAL NOT NULL,
|
|
162
|
+
ts TEXT NOT NULL
|
|
163
|
+
);
|
|
164
|
+
`;
|
|
165
|
+
export class ThreadStore {
|
|
166
|
+
eventsDb;
|
|
167
|
+
structuredDb;
|
|
168
|
+
projectKey;
|
|
169
|
+
spillPolicy;
|
|
170
|
+
constructor(opts, spillPolicy = new SpillPolicy()) {
|
|
171
|
+
this.eventsDb = openDb(opts.eventsPath, EVENTS_SCHEMA, "events");
|
|
172
|
+
this.structuredDb = openDb(opts.structuredPath, STRUCTURED_SCHEMA, "structured");
|
|
173
|
+
this.projectKey = opts.projectKey;
|
|
174
|
+
this.spillPolicy = spillPolicy;
|
|
175
|
+
}
|
|
176
|
+
getRecentSessionId() {
|
|
177
|
+
const row = this.eventsDb
|
|
178
|
+
.prepare(`SELECT session_id FROM events ORDER BY id DESC LIMIT 1`)
|
|
179
|
+
.get();
|
|
180
|
+
return row?.session_id;
|
|
181
|
+
}
|
|
182
|
+
// 引用回拉:spill.blob → spills 表;无 spill 返回事件 body;不可回拉返回 body + 缺失标记
|
|
183
|
+
// 隔离保护:隔离事件的原文仅其所属会话可回拉(sessionId 缺省时隔离内容不可见)
|
|
184
|
+
expand(eventId, opts = {}) {
|
|
185
|
+
const meta = this.eventsDb
|
|
186
|
+
.prepare(`SELECT session_id, isolation FROM events WHERE id = ?`)
|
|
187
|
+
.get(eventId);
|
|
188
|
+
if (!meta) {
|
|
189
|
+
return `[缺失: event ${eventId} 不存在]`;
|
|
190
|
+
}
|
|
191
|
+
if (meta.isolation === 1 && opts.sessionId !== meta.session_id) {
|
|
192
|
+
return `[隔离内容不可见: event ${eventId} 属于隔离会话]`;
|
|
193
|
+
}
|
|
194
|
+
const spill = this.eventsDb
|
|
195
|
+
.prepare(`SELECT blob, ref FROM spills WHERE event_id = ? LIMIT 1`)
|
|
196
|
+
.get(eventId);
|
|
197
|
+
if (spill?.blob != null) {
|
|
198
|
+
return spill.blob;
|
|
199
|
+
}
|
|
200
|
+
const event = this.eventsDb.prepare(`SELECT body FROM events WHERE id = ?`).get(eventId);
|
|
201
|
+
if (event) {
|
|
202
|
+
return spill
|
|
203
|
+
? `${event.body}\n[原文不可回拉: ref=${spill.ref}]`
|
|
204
|
+
: event.body;
|
|
205
|
+
}
|
|
206
|
+
return `[缺失: event ${eventId} 不存在]`;
|
|
207
|
+
}
|
|
208
|
+
hasAssistantTurn(sessionId, uuid) {
|
|
209
|
+
return (this.eventsDb
|
|
210
|
+
.prepare(`SELECT 1 FROM events WHERE session_id = ? AND kind = 'assistant_message' AND json_extract(meta, '$.assistant_uuid') = ? LIMIT 1`)
|
|
211
|
+
.get(sessionId, uuid) !== undefined);
|
|
212
|
+
}
|
|
213
|
+
close() {
|
|
214
|
+
this.eventsDb.close();
|
|
215
|
+
this.structuredDb.close();
|
|
216
|
+
}
|
|
217
|
+
append(event, opts = {}) {
|
|
218
|
+
return this.eventsDb.transaction(() => {
|
|
219
|
+
if (opts.origin) {
|
|
220
|
+
const existing = this.eventsDb
|
|
221
|
+
.prepare(`SELECT id FROM events WHERE origin = ? LIMIT 1`)
|
|
222
|
+
.get(opts.origin);
|
|
223
|
+
if (existing) {
|
|
224
|
+
return this.eventsDb
|
|
225
|
+
.prepare(`SELECT * FROM events WHERE id = ?`)
|
|
226
|
+
.get(existing.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const { body, truncated } = truncateBody(event.body);
|
|
230
|
+
const metaJson = event.meta ? JSON.stringify(event.meta) : null;
|
|
231
|
+
const metaTruncated = metaJson ? metaJson.length > MAX_BODY_CHARS : false;
|
|
232
|
+
const storedMetaJson = metaTruncated && metaJson ? metaJson.slice(0, MAX_BODY_CHARS) : metaJson;
|
|
233
|
+
const spill = this.spillPolicy.evaluate(body, { ref: opts.spillRef });
|
|
234
|
+
const storedBody = spill.spill ? spill.kept : body;
|
|
235
|
+
const seq = this.nextSeq(event.session_id);
|
|
236
|
+
// 项目事实(tool 类事件)强制共享:隔离只作用于对话上下文,事实血缘不断链
|
|
237
|
+
const isolation = isToolKind(event.kind) ? 0 : opts.isolation ? 1 : 0;
|
|
238
|
+
this.eventsDb
|
|
239
|
+
.prepare(`INSERT INTO events (session_id, kind, ts, seq, body, meta, truncated, project_key, scope, origin, spilled, isolation)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
241
|
+
.run(event.session_id, event.kind, event.ts, seq, storedBody, storedMetaJson, truncated || metaTruncated ? 1 : 0, opts.scope === "global" ? null : opts.projectKey ?? this.projectKey ?? null, opts.scope ?? "project", opts.origin ?? null, spill.spill ? 1 : 0, isolation);
|
|
242
|
+
const eventId = this.eventsDb.prepare(`SELECT last_insert_rowid() AS id`).get();
|
|
243
|
+
if (spill.spill) {
|
|
244
|
+
const sha256 = sha256Hex(body);
|
|
245
|
+
this.eventsDb
|
|
246
|
+
.prepare(`INSERT INTO spills (event_id, ref, blob, sha256, created_at) VALUES (?, ?, ?, ?, ?)`)
|
|
247
|
+
.run(eventId.id, spill.ref ?? `spill:${eventId.id}`, body, sha256, event.ts);
|
|
248
|
+
}
|
|
249
|
+
if (isIndexable(event.kind)) {
|
|
250
|
+
this.eventsDb.prepare(`INSERT INTO events_fts(rowid, body) VALUES (?, ?)`).run(eventId.id, cjkSpace(storedBody));
|
|
251
|
+
}
|
|
252
|
+
const eventMeta = event.meta;
|
|
253
|
+
if (typeof eventMeta?.file_path === "string") {
|
|
254
|
+
this.addLineageEdge(event.session_id, "event", eventId.id, "file", null, "touches_file", {
|
|
255
|
+
ref: eventMeta.file_path,
|
|
256
|
+
ts: event.ts,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (typeof eventMeta?.tool_name === "string") {
|
|
260
|
+
this.addLineageEdge(event.session_id, "event", eventId.id, "tool", null, "uses_tool", {
|
|
261
|
+
ref: eventMeta.tool_name,
|
|
262
|
+
ts: event.ts,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
this.updateEpisode(event.session_id, seq, event.kind, event.ts);
|
|
266
|
+
return {
|
|
267
|
+
...event,
|
|
268
|
+
id: eventId.id,
|
|
269
|
+
seq,
|
|
270
|
+
body: storedBody,
|
|
271
|
+
meta: storedMetaJson ? safeParse(storedMetaJson) : undefined,
|
|
272
|
+
truncated: truncated || metaTruncated,
|
|
273
|
+
project_key: opts.projectKey ?? this.projectKey,
|
|
274
|
+
scope: opts.scope ?? "project",
|
|
275
|
+
origin: opts.origin,
|
|
276
|
+
spilled: spill.spill ? 1 : 0,
|
|
277
|
+
isolation,
|
|
278
|
+
};
|
|
279
|
+
})();
|
|
280
|
+
}
|
|
281
|
+
transact(fn) {
|
|
282
|
+
return this.structuredDb.transaction(fn)();
|
|
283
|
+
}
|
|
284
|
+
search(query, opts = {}) {
|
|
285
|
+
const ftsQuery = toFtsQuery(query);
|
|
286
|
+
if (!ftsQuery) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
let sql = `
|
|
290
|
+
SELECT e.id, e.session_id, e.kind, e.ts, e.seq, e.body, e.truncated, bm25(events_fts) AS score
|
|
291
|
+
FROM events_fts f JOIN events e ON e.id = f.rowid
|
|
292
|
+
WHERE events_fts MATCH ?`;
|
|
293
|
+
const params = [ftsQuery];
|
|
294
|
+
if (opts.sessionId) {
|
|
295
|
+
// 当前会话可见自己的全部内容(含隔离),其他会话只见未隔离行
|
|
296
|
+
sql += ` AND (e.session_id = ? OR e.isolation = 0)`;
|
|
297
|
+
params.push(opts.sessionId);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
sql += ` AND e.isolation = 0`;
|
|
301
|
+
}
|
|
302
|
+
sql += ` ORDER BY score LIMIT ?`;
|
|
303
|
+
params.push(opts.limit ?? 20);
|
|
304
|
+
return this.eventsDb.prepare(sql).all(...params);
|
|
305
|
+
}
|
|
306
|
+
getActiveEpisode(sessionId) {
|
|
307
|
+
return this.eventsDb
|
|
308
|
+
.prepare(`SELECT * FROM episodes WHERE session_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1`)
|
|
309
|
+
.get(sessionId);
|
|
310
|
+
}
|
|
311
|
+
getLatestEpisodeWithSummary(sessionId) {
|
|
312
|
+
return this.eventsDb
|
|
313
|
+
.prepare(`SELECT * FROM episodes WHERE session_id = ? AND summary IS NOT NULL ORDER BY id DESC LIMIT 1`)
|
|
314
|
+
.get(sessionId);
|
|
315
|
+
}
|
|
316
|
+
addGoal(sessionId, text, opts = {}) {
|
|
317
|
+
const existing = this.findByOrigin("goals", opts.origin);
|
|
318
|
+
if (existing) {
|
|
319
|
+
return existing;
|
|
320
|
+
}
|
|
321
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
322
|
+
const goal = this.structuredDb
|
|
323
|
+
.prepare(`INSERT INTO goals (session_id, text, status, source_event, created_at, project_key, scope, origin, isolation)
|
|
324
|
+
VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?) RETURNING *`)
|
|
325
|
+
.get(sessionId, text, opts.sourceEvent ?? null, ts, opts.scope === "global" ? null : opts.projectKey ?? this.projectKey ?? null, opts.scope ?? "project", opts.origin ?? null, opts.isolation ? 1 : 0);
|
|
326
|
+
if (opts.sourceEvent) {
|
|
327
|
+
this.addLineageEdge(sessionId, "goal", goal.id, "event", opts.sourceEvent, "derived_from", {
|
|
328
|
+
ts,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return goal;
|
|
332
|
+
}
|
|
333
|
+
getActiveGoals(sessionId) {
|
|
334
|
+
return this.structuredDb
|
|
335
|
+
.prepare(`SELECT * FROM goals WHERE session_id = ? AND status = 'active' ORDER BY id`)
|
|
336
|
+
.all(sessionId);
|
|
337
|
+
}
|
|
338
|
+
getFeedback(sessionId, limit = 5) {
|
|
339
|
+
return this.structuredDb
|
|
340
|
+
.prepare(`SELECT * FROM feedback WHERE session_id = ? ORDER BY id DESC LIMIT ?`)
|
|
341
|
+
.all(sessionId, limit);
|
|
342
|
+
}
|
|
343
|
+
getRecentEvents(sessionId, limit = 5) {
|
|
344
|
+
const rows = this.eventsDb
|
|
345
|
+
.prepare(`SELECT id, session_id, kind, ts, seq, body, meta, truncated
|
|
346
|
+
FROM events WHERE session_id = ? ORDER BY id DESC LIMIT ?`)
|
|
347
|
+
.all(sessionId, limit);
|
|
348
|
+
return rows.map((r) => ({
|
|
349
|
+
...r,
|
|
350
|
+
meta: r.meta ? safeParse(r.meta) : undefined,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
updateGoalStatus(sessionId, goalId, status) {
|
|
354
|
+
return this.structuredDb
|
|
355
|
+
.prepare(`UPDATE goals SET status = ? WHERE id = ? AND session_id = ? RETURNING *`)
|
|
356
|
+
.get(status, goalId, sessionId);
|
|
357
|
+
}
|
|
358
|
+
addFeedback(sessionId, text, kind, opts = {}) {
|
|
359
|
+
const existing = this.findByOrigin("feedback", opts.origin);
|
|
360
|
+
if (existing) {
|
|
361
|
+
return existing;
|
|
362
|
+
}
|
|
363
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
364
|
+
return this.structuredDb
|
|
365
|
+
.prepare(`INSERT INTO feedback (session_id, text, kind, source_event, created_at, project_key, scope, origin, isolation)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`)
|
|
367
|
+
.get(sessionId, text, kind, opts.sourceEvent ?? null, ts, opts.scope === "global" ? null : opts.projectKey ?? this.projectKey ?? null, opts.scope ?? "project", opts.origin ?? null, opts.isolation ? 1 : 0);
|
|
368
|
+
}
|
|
369
|
+
// 反馈治理(B⑥-② 恢复通道):按 id 删除反馈行——教训可删即恢复;事件流水保留(真相源不变)。
|
|
370
|
+
// 单用户语义:跨会话行亦可删(治理动作由用户/其代理发起)。
|
|
371
|
+
deleteFeedback(id) {
|
|
372
|
+
const res = this.structuredDb.prepare(`DELETE FROM feedback WHERE id = ?`).run(id);
|
|
373
|
+
return res.changes > 0;
|
|
374
|
+
}
|
|
375
|
+
proposeDecision(sessionId, text, opts = {}) {
|
|
376
|
+
const existing = this.findByOrigin("decisions", opts.origin);
|
|
377
|
+
if (existing) {
|
|
378
|
+
return existing;
|
|
379
|
+
}
|
|
380
|
+
const dup = this.structuredDb
|
|
381
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND text = ? AND status NOT IN ('superseded', 'revoked') LIMIT 1`)
|
|
382
|
+
.get(sessionId, text);
|
|
383
|
+
if (dup) {
|
|
384
|
+
return dup;
|
|
385
|
+
}
|
|
386
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
387
|
+
const decision = this.structuredDb
|
|
388
|
+
.prepare(`INSERT INTO decisions (session_id, text, status, source_event, created_at, updated_at, project_key, scope, origin, isolation)
|
|
389
|
+
VALUES (?, ?, 'proposed', ?, ?, ?, ?, ?, ?, ?) RETURNING *`)
|
|
390
|
+
.get(sessionId, text, opts.sourceEvent ?? null, ts, ts, opts.scope === "global" ? null : opts.projectKey ?? this.projectKey ?? null, opts.scope ?? "project", opts.origin ?? null, opts.isolation ? 1 : 0);
|
|
391
|
+
if (opts.sourceEvent) {
|
|
392
|
+
this.addLineageEdge(sessionId, "decision", decision.id, "event", opts.sourceEvent, "derived_from", {
|
|
393
|
+
ts,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return decision;
|
|
397
|
+
}
|
|
398
|
+
// 结构化表幂等:origin 已存在(含跨会话)则返回既有行,不重复写
|
|
399
|
+
findByOrigin(table, origin) {
|
|
400
|
+
if (!origin) {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
return this.structuredDb
|
|
404
|
+
.prepare(`SELECT * FROM ${table} WHERE origin = ? LIMIT 1`)
|
|
405
|
+
.get(origin);
|
|
406
|
+
}
|
|
407
|
+
confirmLatestProposed(sessionId, opts = {}) {
|
|
408
|
+
const target = this.structuredDb
|
|
409
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status = 'proposed' ORDER BY id DESC LIMIT 1`)
|
|
410
|
+
.get(sessionId);
|
|
411
|
+
if (!target) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
return this.transitionDecision(target, "active", opts.ts);
|
|
415
|
+
}
|
|
416
|
+
revokeLatestActive(sessionId, opts = {}) {
|
|
417
|
+
const target = this.structuredDb
|
|
418
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status IN ('proposed', 'active') ORDER BY id DESC LIMIT 1`)
|
|
419
|
+
.get(sessionId);
|
|
420
|
+
if (!target) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
return this.transitionDecision(target, "revoked", opts.ts);
|
|
424
|
+
}
|
|
425
|
+
supersedeLatestActive(sessionId, replacementText, opts = {}) {
|
|
426
|
+
const target = this.structuredDb
|
|
427
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status IN ('proposed', 'active') ORDER BY id DESC LIMIT 1`)
|
|
428
|
+
.get(sessionId);
|
|
429
|
+
const ts = opts.ts ?? new Date().toISOString();
|
|
430
|
+
if (!target) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
const replacement = this.structuredDb
|
|
434
|
+
.prepare(`INSERT INTO decisions (session_id, text, status, superseded_by, source_event, created_at, updated_at, isolation)
|
|
435
|
+
VALUES (?, ?, 'active', NULL, ?, ?, ?, ?) RETURNING *`)
|
|
436
|
+
.get(sessionId, replacementText, opts.sourceEvent ?? null, ts, ts, target.isolation ?? 0);
|
|
437
|
+
const superseded = this.transitionDecision(target, "superseded", ts, replacement.id);
|
|
438
|
+
this.addLineageEdge(sessionId, "decision", target.id, "decision", replacement.id, "supersedes", {
|
|
439
|
+
ts,
|
|
440
|
+
});
|
|
441
|
+
return { superseded, replacement };
|
|
442
|
+
}
|
|
443
|
+
getActiveDecisions(sessionId) {
|
|
444
|
+
return this.structuredDb
|
|
445
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status = 'active' ORDER BY id`)
|
|
446
|
+
.all(sessionId);
|
|
447
|
+
}
|
|
448
|
+
// 作用域合并(B②):当前会话全部 active + 同项目 project 级 active(其他会话建立)——非当前项目硬过滤
|
|
449
|
+
// 隔离过滤:其他会话的隔离行(isolation=1)不可见
|
|
450
|
+
getActiveDecisionsMerged(sessionId, projectKey) {
|
|
451
|
+
if (!projectKey) {
|
|
452
|
+
return this.getActiveDecisions(sessionId);
|
|
453
|
+
}
|
|
454
|
+
return this.structuredDb
|
|
455
|
+
.prepare(`SELECT * FROM decisions
|
|
456
|
+
WHERE status = 'active'
|
|
457
|
+
AND (session_id = ? OR (project_key = ? AND scope = 'project'))
|
|
458
|
+
AND (session_id = ? OR isolation = 0)
|
|
459
|
+
ORDER BY id`)
|
|
460
|
+
.all(sessionId, projectKey, sessionId);
|
|
461
|
+
}
|
|
462
|
+
getActiveGoalsMerged(sessionId, projectKey) {
|
|
463
|
+
if (!projectKey) {
|
|
464
|
+
return this.getActiveGoals(sessionId);
|
|
465
|
+
}
|
|
466
|
+
return this.structuredDb
|
|
467
|
+
.prepare(`SELECT * FROM goals
|
|
468
|
+
WHERE status = 'active'
|
|
469
|
+
AND (session_id = ? OR (project_key = ? AND scope = 'project'))
|
|
470
|
+
AND (session_id = ? OR isolation = 0)
|
|
471
|
+
ORDER BY id`)
|
|
472
|
+
.all(sessionId, projectKey, sessionId);
|
|
473
|
+
}
|
|
474
|
+
// 反馈合并:当前会话 + 同项目 project 级 + global 级(跨项目共享),去重按 text
|
|
475
|
+
// 隔离过滤:其他会话的隔离行不可见
|
|
476
|
+
getFeedbackMerged(sessionId, projectKey, limit = 5) {
|
|
477
|
+
const rows = this.structuredDb
|
|
478
|
+
.prepare(`SELECT * FROM feedback
|
|
479
|
+
WHERE (session_id = ? OR scope = 'global' OR (project_key = ? AND scope = 'project'))
|
|
480
|
+
AND (session_id = ? OR isolation = 0)
|
|
481
|
+
ORDER BY id DESC LIMIT ?`)
|
|
482
|
+
.all(sessionId, projectKey ?? "", sessionId, limit);
|
|
483
|
+
const seen = new Set();
|
|
484
|
+
const out = [];
|
|
485
|
+
for (const row of rows) {
|
|
486
|
+
if (seen.has(row.text)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
seen.add(row.text);
|
|
490
|
+
out.push(row);
|
|
491
|
+
}
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
// 会话临时隔离(B⑧):会话级可变开关——隔离期间对话上下文仅自己可见,项目事实(tool)仍共享
|
|
495
|
+
setSessionIsolation(sessionId, isolated) {
|
|
496
|
+
this.structuredDb
|
|
497
|
+
.prepare(`INSERT INTO session_isolation (session_id, isolated, updated_at) VALUES (?, ?, ?)
|
|
498
|
+
ON CONFLICT(session_id) DO UPDATE SET isolated = excluded.isolated, updated_at = excluded.updated_at`)
|
|
499
|
+
.run(sessionId, isolated ? 1 : 0, new Date().toISOString());
|
|
500
|
+
}
|
|
501
|
+
getSessionIsolation(sessionId) {
|
|
502
|
+
const row = this.structuredDb
|
|
503
|
+
.prepare(`SELECT isolated FROM session_isolation WHERE session_id = ?`)
|
|
504
|
+
.get(sessionId);
|
|
505
|
+
return row?.isolated === 1;
|
|
506
|
+
}
|
|
507
|
+
// 沉淀:清除指定结构化行的隔离标记(隔离会话期间产生的决策按需转共享)
|
|
508
|
+
unisolateRow(sessionId, table, id) {
|
|
509
|
+
const row = this.structuredDb
|
|
510
|
+
.prepare(`UPDATE ${table} SET isolation = 0 WHERE id = ? AND session_id = ? RETURNING id`)
|
|
511
|
+
.get(id, sessionId);
|
|
512
|
+
return row !== undefined;
|
|
513
|
+
}
|
|
514
|
+
getLatestProposed(sessionId) {
|
|
515
|
+
return this.structuredDb
|
|
516
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status = 'proposed' ORDER BY id DESC LIMIT 1`)
|
|
517
|
+
.get(sessionId);
|
|
518
|
+
}
|
|
519
|
+
getDecisions(sessionId, status) {
|
|
520
|
+
if (status) {
|
|
521
|
+
return this.structuredDb
|
|
522
|
+
.prepare(`SELECT * FROM decisions WHERE session_id = ? AND status = ? ORDER BY id`)
|
|
523
|
+
.all(sessionId, status);
|
|
524
|
+
}
|
|
525
|
+
return this.structuredDb.prepare(`SELECT * FROM decisions WHERE session_id = ? ORDER BY id`).all(sessionId);
|
|
526
|
+
}
|
|
527
|
+
// 血缘分库(B④):任一端 ∈ {goal,decision,feedback} → 结构化库;两端均 ∈ {event,file,tool} → 事件库
|
|
528
|
+
lineageDb(srcType, dstType) {
|
|
529
|
+
const srcEvent = srcType === "event" || srcType === "file" || srcType === "tool";
|
|
530
|
+
const dstEvent = dstType === "event" || dstType === "file" || dstType === "tool";
|
|
531
|
+
return srcEvent && dstEvent ? this.eventsDb : this.structuredDb;
|
|
532
|
+
}
|
|
533
|
+
addLineageEdge(sessionId, srcType, srcId, dstType, dstId, edgeType, opts = {}) {
|
|
534
|
+
const db = this.lineageDb(srcType, dstType);
|
|
535
|
+
db
|
|
536
|
+
.prepare(`INSERT INTO lineage_edges (session_id, src_type, src_id, dst_type, dst_id, ref, edge_type, confidence, ts)
|
|
537
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
538
|
+
.run(sessionId, srcType, srcId, dstType, dstId, opts.ref ?? null, edgeType, opts.confidence ?? 1.0, opts.ts ?? new Date().toISOString());
|
|
539
|
+
}
|
|
540
|
+
getRelatedEvents(sessionId, eventId) {
|
|
541
|
+
return this.getRelatedEdges(sessionId, "event", eventId);
|
|
542
|
+
}
|
|
543
|
+
getRelatedEdges(sessionId, type, id) {
|
|
544
|
+
const db = type === "event" || type === "file" || type === "tool" ? this.eventsDb : this.structuredDb;
|
|
545
|
+
return db
|
|
546
|
+
.prepare(`SELECT * FROM lineage_edges
|
|
547
|
+
WHERE session_id = ? AND (src_type = ? AND src_id = ? OR dst_type = ? AND dst_id = ?)
|
|
548
|
+
ORDER BY id`)
|
|
549
|
+
.all(sessionId, type, id, type, id);
|
|
550
|
+
}
|
|
551
|
+
getEventsForFile(sessionId, filePath) {
|
|
552
|
+
return this.eventsDb
|
|
553
|
+
.prepare(`SELECT * FROM lineage_edges
|
|
554
|
+
WHERE session_id = ? AND dst_type = 'file' AND ref = ?
|
|
555
|
+
ORDER BY id`)
|
|
556
|
+
.all(sessionId, filePath);
|
|
557
|
+
}
|
|
558
|
+
transitionDecision(decision, to, ts, supersededBy) {
|
|
559
|
+
assertTransition(decision.status, to);
|
|
560
|
+
const updatedAt = ts ?? new Date().toISOString();
|
|
561
|
+
return this.structuredDb
|
|
562
|
+
.prepare(`UPDATE decisions SET status = ?, superseded_by = ?, updated_at = ? WHERE id = ? RETURNING *`)
|
|
563
|
+
.get(to, supersededBy ?? decision.superseded_by, updatedAt, decision.id);
|
|
564
|
+
}
|
|
565
|
+
nextSeq(sessionId) {
|
|
566
|
+
const row = this.eventsDb
|
|
567
|
+
.prepare(`SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM events WHERE session_id = ?`)
|
|
568
|
+
.get(sessionId);
|
|
569
|
+
return row.seq;
|
|
570
|
+
}
|
|
571
|
+
updateEpisode(sessionId, seq, kind, ts) {
|
|
572
|
+
if (kind === "user_message") {
|
|
573
|
+
this.closeActiveEpisode(sessionId, seq - 1);
|
|
574
|
+
this.eventsDb
|
|
575
|
+
.prepare(`INSERT INTO episodes (session_id, seq_start, status, created_at) VALUES (?, ?, 'active', ?)`)
|
|
576
|
+
.run(sessionId, seq, ts);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
this.eventsDb
|
|
580
|
+
.prepare(`UPDATE episodes SET seq_end = ? WHERE session_id = ? AND status = 'active'`)
|
|
581
|
+
.run(seq, sessionId);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
closeActiveEpisode(sessionId, seqEnd) {
|
|
585
|
+
if (seqEnd <= 0) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const episode = this.getActiveEpisode(sessionId);
|
|
589
|
+
if (!episode) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const rows = this.eventsDb
|
|
593
|
+
.prepare(`SELECT body FROM events WHERE session_id = ? AND seq BETWEEN ? AND ? ORDER BY seq`)
|
|
594
|
+
.all(sessionId, episode.seq_start, seqEnd);
|
|
595
|
+
const summary = rows
|
|
596
|
+
.map((r) => r.body)
|
|
597
|
+
.join("\n")
|
|
598
|
+
.slice(0, EPISODE_SUMMARY_CHARS);
|
|
599
|
+
this.eventsDb
|
|
600
|
+
.prepare(`UPDATE episodes SET seq_end = ?, summary = COALESCE(summary, ?) WHERE id = ? AND session_id = ?`)
|
|
601
|
+
.run(seqEnd, summary, episode.id, sessionId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function openDb(path, schema, kind) {
|
|
605
|
+
const db = new Database(path);
|
|
606
|
+
db.pragma("journal_mode = WAL");
|
|
607
|
+
db.pragma("busy_timeout = 5000");
|
|
608
|
+
db.exec(schema);
|
|
609
|
+
ensureSchema(db, kind);
|
|
610
|
+
return db;
|
|
611
|
+
}
|
|
612
|
+
function isToolKind(kind) {
|
|
613
|
+
return kind === "tool_call" || kind === "tool_result";
|
|
614
|
+
}
|
|
615
|
+
function toFtsQuery(query) {
|
|
616
|
+
const tokens = cjkSpace(query).split(/\s+/).filter(Boolean);
|
|
617
|
+
if (tokens.length === 0) {
|
|
618
|
+
return "";
|
|
619
|
+
}
|
|
620
|
+
return tokens.map((t) => `"${t.replaceAll('"', '""')}"`).join(" AND ");
|
|
621
|
+
}
|
|
622
|
+
const EPISODE_SUMMARY_CHARS = 4000;
|
|
623
|
+
function safeParse(text) {
|
|
624
|
+
try {
|
|
625
|
+
return JSON.parse(text);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function sha256Hex(text) {
|
|
632
|
+
return createHash("sha256").update(text).digest("hex");
|
|
633
|
+
}
|
|
634
|
+
function cjkSpace(text) {
|
|
635
|
+
return text
|
|
636
|
+
.replace(/([\u3400-\u9fff\uf900-\ufaff])/g, " $1 ")
|
|
637
|
+
.replace(/\s+/g, " ")
|
|
638
|
+
.trim();
|
|
639
|
+
}
|
|
640
|
+
// 分层优先级裁决(B③):同事实(归一化 text)跨层级出现时保留最高优先级 会话内>项目>用户>全局
|
|
641
|
+
const SCOPE_PRIORITY = { session: 3, project: 2, global: 1 };
|
|
642
|
+
export function applyScopePriority(rows) {
|
|
643
|
+
const best = new Map();
|
|
644
|
+
for (const row of rows) {
|
|
645
|
+
const key = normalizeText(row.text);
|
|
646
|
+
if (!key) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const existing = best.get(key);
|
|
650
|
+
const prio = SCOPE_PRIORITY[row.scope ?? "project"] ?? 0;
|
|
651
|
+
const existingPrio = existing ? (SCOPE_PRIORITY[existing.scope ?? "project"] ?? 0) : -1;
|
|
652
|
+
if (!existing || prio > existingPrio) {
|
|
653
|
+
best.set(key, row);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return [...best.values()];
|
|
657
|
+
}
|
|
658
|
+
function normalizeText(text) {
|
|
659
|
+
return text
|
|
660
|
+
.replace(/\s+/g, "")
|
|
661
|
+
.replace(/[,。!!??、;;::,.·'"“”‘’()()[\]【】<>《》]/g, "")
|
|
662
|
+
.toLowerCase();
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=store.js.map
|