luckytri 1.0.0 → 1.0.2
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/.github/SECURITY.md +4 -4
- package/README.en.md +252 -250
- package/README.md +252 -250
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +8 -0
- package/docs/en/connect.md +18 -18
- package/docs/en/guide.md +202 -216
- package/docs/en/npm-release.md +25 -25
- package/docs/zh/CHANGELOG.md +8 -0
- package/docs/zh/connect.md +17 -17
- package/docs/zh/guide.md +202 -216
- package/docs/zh/npm-release.md +33 -33
- package/package.json +110 -108
- package/plugins/sdk/index.d.ts +155 -155
- package/plugins/sdk/testing.js +58 -58
- package/plugins/weather/index.js +82 -82
- package/plugins/weather/luckytri-plugin.json +29 -29
- package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
- package/public/app/assets/Empty-DP6WMGHi.js +2 -0
- package/public/app/assets/Field-CtbdqaiB.js +1 -0
- package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
- package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
- package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
- package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
- package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
- package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
- package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
- package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
- package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
- package/public/app/assets/TimePage-7-awH5XL.js +3 -0
- package/public/app/assets/index-4rItIkD-.js +4 -0
- package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
- package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
- package/public/app/assets/media-SMfTT-yc.js +1 -0
- package/public/app/index.html +3 -3
- package/public/guide.en.html +72 -99
- package/public/guide.html +44 -86
- package/public/tutorial.en.md +25 -39
- package/public/tutorial.md +25 -39
- package/scripts/backup.js +311 -311
- package/scripts/browser.js +41 -41
- package/scripts/import-data.js +128 -0
- package/scripts/launch.js +101 -94
- package/scripts/plugin.js +150 -150
- package/scripts/reset-password.js +22 -0
- package/scripts/runtime.js +25 -25
- package/scripts/service.js +56 -47
- package/scripts/setup.js +50 -50
- package/scripts/start-server.js +41 -41
- package/scripts/stop.js +36 -36
- package/server/app.js +112 -96
- package/server/auth.js +217 -0
- package/server/channels/contract.js +57 -57
- package/server/channels/hub.js +165 -165
- package/server/channels/index.js +28 -25
- package/server/channels/onebot/index.js +335 -336
- package/server/channels/onebot/token.js +22 -3
- package/server/core/capabilities.js +208 -208
- package/server/core/events.js +64 -60
- package/server/core/image-preprocessor.js +208 -208
- package/server/core/model-manager.js +1213 -1213
- package/server/core/net-guard.js +49 -49
- package/server/core/orchestrator.js +569 -569
- package/server/core/perceivers.js +91 -91
- package/server/core/persona-manager.js +243 -243
- package/server/core/repository.js +293 -293
- package/server/core/response-validator.js +508 -508
- package/server/core/turn-processor.js +630 -630
- package/server/core/turn.js +236 -236
- package/server/core/vision-manager.js +633 -633
- package/server/i18n/errors.js +548 -497
- package/server/i18n/labels.js +649 -653
- package/server/index.js +151 -102
- package/server/knowledge/manager.js +386 -386
- package/server/knowledge/schema.js +12 -8
- package/server/local-access.js +32 -0
- package/server/mind/anticipations.js +507 -507
- package/server/mind/api.js +703 -703
- package/server/mind/budget.js +152 -152
- package/server/mind/context-selection.js +61 -61
- package/server/mind/day-planner.js +324 -324
- package/server/mind/evidence.js +148 -148
- package/server/mind/index.js +445 -445
- package/server/mind/life/diary.js +786 -786
- package/server/mind/life/index.js +1049 -1049
- package/server/mind/observations.js +84 -84
- package/server/mind/own-day.js +324 -324
- package/server/mind/own-voice.js +318 -318
- package/server/mind/senses.js +53 -53
- package/server/mind/time/activity-clock.js +196 -196
- package/server/mind/time/availability.js +69 -69
- package/server/mind/time/commitment-review.js +198 -198
- package/server/mind/time/executor.js +512 -512
- package/server/mind/time/experiences.js +128 -128
- package/server/mind/time/games.js +562 -562
- package/server/mind/time/index.js +739 -739
- package/server/mind/time/intent.js +166 -166
- package/server/mind/time/kinds.js +171 -171
- package/server/mind/time/plugin-activity.js +207 -207
- package/server/mind/time/tasks.js +587 -587
- package/server/mind/util.js +110 -110
- package/server/mind/view.js +273 -273
- package/server/paths.js +102 -102
- package/server/plugins/api.js +204 -204
- package/server/plugins/host.js +967 -967
- package/server/plugins/install.js +295 -295
- package/server/plugins/manifest.js +201 -201
- package/server/plugins/permissions.js +42 -42
- package/server/plugins/runner/guard.mjs +36 -36
- package/server/plugins/runner/worker.mjs +250 -250
- package/server/plugins/schema.js +91 -91
- package/server/plugins/zip.js +112 -112
- package/server/startup-lock.js +118 -118
- package/server/storage/archive.js +242 -241
- package/server/storage/backup-cleanup-worker.js +8 -8
- package/server/storage/backup-cleanup.js +415 -414
- package/server/storage/backup-lock.js +76 -76
- package/server/storage/backup-scheduler.js +131 -131
- package/server/storage/data-package.js +298 -0
- package/server/storage/data-transfer-api.js +50 -0
- package/server/storage/data-transfer-worker.js +18 -0
- package/server/storage/data-transfer.js +415 -0
- package/server/storage/store.js +202 -199
- package/server/studio/readiness.js +181 -190
- package/server/studio/settings.js +236 -236
- package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
- package/public/app/assets/Empty-CpP5kQHH.js +0 -2
- package/public/app/assets/Field-D4gY-INU.js +0 -1
- package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
- package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
- package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
- package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
- package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
- package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
- package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
- package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
- package/public/app/assets/TimePage-Cw44inM4.js +0 -3
- package/public/app/assets/index-BheRW-1t.js +0 -4
- package/public/app/assets/media-BzPDBm7e.js +0 -1
|
@@ -1,293 +1,293 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { DATABASE_VERSION } from "../storage/archive.js";
|
|
3
|
-
import { parseSessionKey } from "../channels/session-key.js";
|
|
4
|
-
import { migrateKnowledge } from "../knowledge/schema.js";
|
|
5
|
-
import { migrateMind } from "../mind/schema.js";
|
|
6
|
-
import { recordUsage } from "../mind/budget.js";
|
|
7
|
-
import { migratePlugins } from "../plugins/schema.js";
|
|
8
|
-
|
|
9
|
-
export function migrateCore(store) {
|
|
10
|
-
const db = store.db;
|
|
11
|
-
db.exec(`
|
|
12
|
-
CREATE TABLE IF NOT EXISTS core_config (id TEXT PRIMARY KEY, value TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1);
|
|
13
|
-
CREATE TABLE IF NOT EXISTS core_events (seq INTEGER PRIMARY KEY, event_id TEXT UNIQUE, session_id TEXT NOT NULL, platform_id TEXT, account_id TEXT, time INTEGER, role TEXT, payload TEXT NOT NULL);
|
|
14
|
-
CREATE INDEX IF NOT EXISTS core_events_session ON core_events(session_id,seq);
|
|
15
|
-
CREATE INDEX IF NOT EXISTS core_events_platform ON core_events(session_id,account_id,platform_id);
|
|
16
|
-
CREATE TABLE IF NOT EXISTS core_traces (id TEXT PRIMARY KEY, session_id TEXT, time INTEGER, mode TEXT, status TEXT, data TEXT);
|
|
17
|
-
CREATE INDEX IF NOT EXISTS core_traces_session_time ON core_traces(session_id,time DESC);
|
|
18
|
-
CREATE INDEX IF NOT EXISTS core_traces_status_session ON core_traces(status,session_id);
|
|
19
|
-
CREATE TABLE IF NOT EXISTS core_outbox (id TEXT PRIMARY KEY, trace_id TEXT, session_id TEXT, position INTEGER, text TEXT, status TEXT, platform_id TEXT, time INTEGER);
|
|
20
|
-
CREATE INDEX IF NOT EXISTS core_outbox_session_time ON core_outbox(session_id,time DESC);
|
|
21
|
-
CREATE TABLE IF NOT EXISTS core_memories (id TEXT PRIMARY KEY, session_id TEXT, subject TEXT, content TEXT, type TEXT, confidence REAL, importance REAL, status TEXT, locked INTEGER DEFAULT 0, sources TEXT, created INTEGER, updated INTEGER, last_access INTEGER, expires INTEGER, version INTEGER DEFAULT 1);
|
|
22
|
-
CREATE INDEX IF NOT EXISTS core_memories_scope ON core_memories(session_id,status);
|
|
23
|
-
CREATE TABLE IF NOT EXISTS core_memory_versions (id INTEGER PRIMARY KEY, memory_id TEXT, time INTEGER, value TEXT);
|
|
24
|
-
CREATE TABLE IF NOT EXISTS core_stages (id TEXT PRIMARY KEY, session_id TEXT, first_seq INTEGER, last_seq INTEGER, time INTEGER, data TEXT, UNIQUE(session_id,first_seq,last_seq));
|
|
25
|
-
CREATE TABLE IF NOT EXISTS core_cursors (session_id TEXT PRIMARY KEY, seq INTEGER DEFAULT 0);
|
|
26
|
-
CREATE TABLE IF NOT EXISTS core_jobs (seq INTEGER PRIMARY KEY, session_id TEXT, status TEXT, trace_id TEXT, time INTEGER);
|
|
27
|
-
CREATE TABLE IF NOT EXISTS core_references (id INTEGER PRIMARY KEY, session_id TEXT, platform_id TEXT, account_id TEXT, payload TEXT, UNIQUE(session_id,platform_id,account_id));
|
|
28
|
-
CREATE TABLE IF NOT EXISTS core_vision_cache (cache_key TEXT PRIMARY KEY, session_id TEXT, message_id INTEGER, description TEXT NOT NULL, created INTEGER NOT NULL);
|
|
29
|
-
CREATE INDEX IF NOT EXISTS core_traces_time ON core_traces(time);
|
|
30
|
-
CREATE TABLE IF NOT EXISTS core_context_summaries (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, level INTEGER NOT NULL, first_seq INTEGER NOT NULL, last_seq INTEGER NOT NULL, first_time INTEGER, last_time INTEGER, created INTEGER NOT NULL, data TEXT NOT NULL);
|
|
31
|
-
CREATE INDEX IF NOT EXISTS core_context_summaries_session ON core_context_summaries(session_id,first_seq);
|
|
32
|
-
`);
|
|
33
|
-
// A saved persona or prompt may intentionally use an older name. Leave it
|
|
34
|
-
// intact; fresh workspaces receive the LuckyTri defaults from the store.
|
|
35
|
-
if (!db.prepare("SELECT id FROM core_config WHERE id='migration-v1'").get()) {
|
|
36
|
-
db.exec("BEGIN IMMEDIATE");
|
|
37
|
-
try {
|
|
38
|
-
for (const r of db
|
|
39
|
-
.prepare("SELECT * FROM messages WHERE is_demo=0 ORDER BY id")
|
|
40
|
-
.all()) {
|
|
41
|
-
const m = {
|
|
42
|
-
sessionId: r.session_id,
|
|
43
|
-
kind: (() => {
|
|
44
|
-
try {
|
|
45
|
-
return parseSessionKey(r.session_id).kind;
|
|
46
|
-
} catch {
|
|
47
|
-
return String(r.session_id).startsWith("private")
|
|
48
|
-
? "private"
|
|
49
|
-
: "group";
|
|
50
|
-
}
|
|
51
|
-
})(),
|
|
52
|
-
userId: r.user_id,
|
|
53
|
-
name: r.name,
|
|
54
|
-
text: r.text,
|
|
55
|
-
role: r.role,
|
|
56
|
-
time: r.time,
|
|
57
|
-
mentions: [],
|
|
58
|
-
attachments: [],
|
|
59
|
-
replyId: null,
|
|
60
|
-
metadataUnknown: true,
|
|
61
|
-
};
|
|
62
|
-
db.prepare(
|
|
63
|
-
"INSERT OR IGNORE INTO core_events(event_id,session_id,platform_id,account_id,time,role,payload) VALUES (?,?,?,?,?,?,?)",
|
|
64
|
-
).run(
|
|
65
|
-
r.event_id || `legacy:${r.id}`,
|
|
66
|
-
r.session_id,
|
|
67
|
-
null,
|
|
68
|
-
null,
|
|
69
|
-
r.time,
|
|
70
|
-
r.role,
|
|
71
|
-
JSON.stringify(m),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
db.prepare("INSERT INTO core_config(id,value) VALUES (?,?)").run(
|
|
75
|
-
"migration-v1",
|
|
76
|
-
JSON.stringify({ time: Date.now() }),
|
|
77
|
-
);
|
|
78
|
-
db.exec("COMMIT");
|
|
79
|
-
} catch (e) {
|
|
80
|
-
db.exec("ROLLBACK");
|
|
81
|
-
throw e;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// A restarted process cannot know whether an in-flight send reached QQ.
|
|
85
|
-
db.prepare(
|
|
86
|
-
"UPDATE core_outbox SET status='uncertain' WHERE status='sending'",
|
|
87
|
-
).run();
|
|
88
|
-
db.prepare(
|
|
89
|
-
"UPDATE core_outbox SET status='cancelled' WHERE status='pending'",
|
|
90
|
-
).run();
|
|
91
|
-
db.prepare(
|
|
92
|
-
"UPDATE core_traces SET status='interrupted' WHERE status='running'",
|
|
93
|
-
).run();
|
|
94
|
-
migrateKnowledge(db);
|
|
95
|
-
migrateMind(db, store);
|
|
96
|
-
migratePlugins(db);
|
|
97
|
-
db.exec(`PRAGMA user_version=${DATABASE_VERSION}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const TOKEN_FIELDS = [
|
|
101
|
-
"input",
|
|
102
|
-
"cachedRead",
|
|
103
|
-
"cacheWrite",
|
|
104
|
-
"output",
|
|
105
|
-
"reasoning",
|
|
106
|
-
];
|
|
107
|
-
export const TRACE_RETENTION_DAYS = 14;
|
|
108
|
-
|
|
109
|
-
function eventRow(r) {
|
|
110
|
-
return {
|
|
111
|
-
...JSON.parse(r.payload),
|
|
112
|
-
seq: r.seq,
|
|
113
|
-
eventId: r.event_id,
|
|
114
|
-
platformId: r.platform_id,
|
|
115
|
-
accountId: r.account_id,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function simulatedFilter(simulated) {
|
|
120
|
-
return simulated === null
|
|
121
|
-
? ""
|
|
122
|
-
: " AND COALESCE(json_extract(payload,'$.simulated'),0)=?";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export class Repository {
|
|
126
|
-
constructor(store) {
|
|
127
|
-
this.store = store;
|
|
128
|
-
this.db = store.db;
|
|
129
|
-
migrateCore(store);
|
|
130
|
-
}
|
|
131
|
-
config(id, fallback = null) {
|
|
132
|
-
const r = this.db
|
|
133
|
-
.prepare("SELECT value FROM core_config WHERE id=?")
|
|
134
|
-
.get(id);
|
|
135
|
-
return r ? JSON.parse(r.value) : fallback;
|
|
136
|
-
}
|
|
137
|
-
saveConfig(id, value) {
|
|
138
|
-
this.db
|
|
139
|
-
.prepare(
|
|
140
|
-
"INSERT INTO core_config(id,value) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value,version=version+1",
|
|
141
|
-
)
|
|
142
|
-
.run(id, JSON.stringify(value));
|
|
143
|
-
this.store.revision++;
|
|
144
|
-
}
|
|
145
|
-
// `simulated` is deliberately explicit at context boundaries. The
|
|
146
|
-
// default keeps the repository useful for admin views and migrations, while
|
|
147
|
-
// callers building a live or demo prompt must choose one side of the
|
|
148
|
-
// boundary so a preview can never leak into a real conversation.
|
|
149
|
-
events(session, before = Number.MAX_SAFE_INTEGER, { simulated = null } = {}) {
|
|
150
|
-
const args =
|
|
151
|
-
simulated === null
|
|
152
|
-
? [session, before]
|
|
153
|
-
: [session, before, Number(!!simulated)];
|
|
154
|
-
return this.db
|
|
155
|
-
.prepare(
|
|
156
|
-
`SELECT * FROM core_events WHERE session_id=? AND seq<=?${simulatedFilter(simulated)} ORDER BY seq`,
|
|
157
|
-
)
|
|
158
|
-
.all(...args)
|
|
159
|
-
.map(eventRow);
|
|
160
|
-
}
|
|
161
|
-
eventsAfter(session, after = 0, { simulated = null, limit = null } = {}) {
|
|
162
|
-
const args =
|
|
163
|
-
simulated === null
|
|
164
|
-
? [session, after]
|
|
165
|
-
: [session, after, Number(!!simulated)];
|
|
166
|
-
const bounded = Number.isSafeInteger(limit) && limit > 0;
|
|
167
|
-
return this.db
|
|
168
|
-
.prepare(
|
|
169
|
-
`SELECT * FROM core_events WHERE session_id=? AND seq>?${simulatedFilter(simulated)} ORDER BY seq${bounded ? " LIMIT ?" : ""}`,
|
|
170
|
-
)
|
|
171
|
-
.all(...args, ...(bounded ? [limit] : []))
|
|
172
|
-
.map(eventRow);
|
|
173
|
-
}
|
|
174
|
-
// Unread attention needs the last matching users, not every event since its
|
|
175
|
-
// cursor. Apply role/time filters before decoding and before the row limit.
|
|
176
|
-
unreadEvents(session, after, since, limit = 40) {
|
|
177
|
-
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
178
|
-
return this.eventsAfter(session, after, { simulated: false })
|
|
179
|
-
.filter((m) => m.role === "user" && m.time >= since)
|
|
180
|
-
.slice(-limit);
|
|
181
|
-
return this.db
|
|
182
|
-
.prepare(
|
|
183
|
-
"SELECT * FROM core_events WHERE session_id=? AND seq>? AND role='user' AND time>=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT ?",
|
|
184
|
-
)
|
|
185
|
-
.all(session, after, since, limit)
|
|
186
|
-
.reverse()
|
|
187
|
-
.map(eventRow);
|
|
188
|
-
}
|
|
189
|
-
// The last few events of a session, oldest first, without reading the rest.
|
|
190
|
-
recentEvents(session, limit = 20, { simulated = null, role = null } = {}) {
|
|
191
|
-
const args = [session];
|
|
192
|
-
if (simulated !== null) args.push(Number(!!simulated));
|
|
193
|
-
if (role) args.push(role);
|
|
194
|
-
return this.db
|
|
195
|
-
.prepare(
|
|
196
|
-
`SELECT * FROM (SELECT * FROM core_events WHERE session_id=?${simulatedFilter(simulated)}${role ? " AND role=?" : ""} ORDER BY seq DESC LIMIT ?) ORDER BY seq`,
|
|
197
|
-
)
|
|
198
|
-
.all(...args, limit)
|
|
199
|
-
.map(eventRow);
|
|
200
|
-
}
|
|
201
|
-
latest(session) {
|
|
202
|
-
return (
|
|
203
|
-
this.db
|
|
204
|
-
.prepare("SELECT MAX(seq) n FROM core_events WHERE session_id=?")
|
|
205
|
-
.get(session).n || 0
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
references(session, { simulated = null } = {}) {
|
|
209
|
-
const filter =
|
|
210
|
-
simulated === null
|
|
211
|
-
? ""
|
|
212
|
-
: " AND COALESCE(json_extract(payload,'$.simulated'),0)=?";
|
|
213
|
-
const args =
|
|
214
|
-
simulated === null ? [session] : [session, Number(!!simulated)];
|
|
215
|
-
return this.db
|
|
216
|
-
.prepare(`SELECT * FROM core_references WHERE session_id=?${filter}`)
|
|
217
|
-
.all(...args)
|
|
218
|
-
.map((r) => ({
|
|
219
|
-
...JSON.parse(r.payload),
|
|
220
|
-
seq: -r.id,
|
|
221
|
-
platformId: r.platform_id,
|
|
222
|
-
accountId: r.account_id,
|
|
223
|
-
referenceOnly: true,
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
append(m) {
|
|
227
|
-
const r = this.db
|
|
228
|
-
.prepare(
|
|
229
|
-
"INSERT OR IGNORE INTO core_events(event_id,session_id,platform_id,account_id,time,role,payload) VALUES (?,?,?,?,?,?,?)",
|
|
230
|
-
)
|
|
231
|
-
.run(
|
|
232
|
-
m.eventId,
|
|
233
|
-
m.sessionId,
|
|
234
|
-
m.platformId || null,
|
|
235
|
-
m.accountId || null,
|
|
236
|
-
m.time || Date.now(),
|
|
237
|
-
m.role || "user",
|
|
238
|
-
JSON.stringify(m),
|
|
239
|
-
);
|
|
240
|
-
return r.changes ? Number(r.lastInsertRowid) : null;
|
|
241
|
-
}
|
|
242
|
-
trace(session, mode = "live") {
|
|
243
|
-
const id = randomUUID();
|
|
244
|
-
this.db
|
|
245
|
-
.prepare("INSERT INTO core_traces VALUES (?,?,?,?,?,?)")
|
|
246
|
-
.run(id, session, Date.now(), mode, "running", "{}");
|
|
247
|
-
return {
|
|
248
|
-
id,
|
|
249
|
-
sessionId: session,
|
|
250
|
-
mode,
|
|
251
|
-
started: Date.now(),
|
|
252
|
-
calls: [],
|
|
253
|
-
steps: [],
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
finish(trace, status) {
|
|
257
|
-
trace.status = status;
|
|
258
|
-
trace.elapsed = Date.now() - trace.started;
|
|
259
|
-
const counted = (trace.calls || []).filter((call) => call.tokens);
|
|
260
|
-
if (counted.length) {
|
|
261
|
-
trace.tokens = { calls: counted.length };
|
|
262
|
-
for (const field of TOKEN_FIELDS)
|
|
263
|
-
trace.tokens[field] = counted.reduce(
|
|
264
|
-
(sum, call) => sum + (Number(call.tokens[field]) || 0),
|
|
265
|
-
0,
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
// Previews spend real tokens too, so every mode is counted once.
|
|
269
|
-
recordUsage(this.db, trace);
|
|
270
|
-
this.db
|
|
271
|
-
.prepare("UPDATE core_traces SET status=?,data=? WHERE id=?")
|
|
272
|
-
.run(status, JSON.stringify(trace), trace.id);
|
|
273
|
-
}
|
|
274
|
-
// Small batches: old traces can be megabytes each and SQLite runs on the
|
|
275
|
-
// event loop, so a large backlog is cleared over several maintenance ticks.
|
|
276
|
-
pruneTraces(
|
|
277
|
-
now = Date.now(),
|
|
278
|
-
{ days = TRACE_RETENTION_DAYS, batchSize = 200, maxBatches = 5 } = {},
|
|
279
|
-
) {
|
|
280
|
-
const remove = this.db.prepare(
|
|
281
|
-
"DELETE FROM core_traces WHERE rowid IN (SELECT rowid FROM core_traces WHERE time<? AND status!='running' LIMIT ?)",
|
|
282
|
-
);
|
|
283
|
-
let removed = 0;
|
|
284
|
-
for (let batch = 0; batch < maxBatches; batch++) {
|
|
285
|
-
const changes = Number(
|
|
286
|
-
remove.run(now - days * 86400000, batchSize).changes,
|
|
287
|
-
);
|
|
288
|
-
removed += changes;
|
|
289
|
-
if (changes < batchSize) return { removed, more: false };
|
|
290
|
-
}
|
|
291
|
-
return { removed, more: true };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { DATABASE_VERSION } from "../storage/archive.js";
|
|
3
|
+
import { parseSessionKey } from "../channels/session-key.js";
|
|
4
|
+
import { migrateKnowledge } from "../knowledge/schema.js";
|
|
5
|
+
import { migrateMind } from "../mind/schema.js";
|
|
6
|
+
import { recordUsage } from "../mind/budget.js";
|
|
7
|
+
import { migratePlugins } from "../plugins/schema.js";
|
|
8
|
+
|
|
9
|
+
export function migrateCore(store) {
|
|
10
|
+
const db = store.db;
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS core_config (id TEXT PRIMARY KEY, value TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1);
|
|
13
|
+
CREATE TABLE IF NOT EXISTS core_events (seq INTEGER PRIMARY KEY, event_id TEXT UNIQUE, session_id TEXT NOT NULL, platform_id TEXT, account_id TEXT, time INTEGER, role TEXT, payload TEXT NOT NULL);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS core_events_session ON core_events(session_id,seq);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS core_events_platform ON core_events(session_id,account_id,platform_id);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS core_traces (id TEXT PRIMARY KEY, session_id TEXT, time INTEGER, mode TEXT, status TEXT, data TEXT);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS core_traces_session_time ON core_traces(session_id,time DESC);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS core_traces_status_session ON core_traces(status,session_id);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS core_outbox (id TEXT PRIMARY KEY, trace_id TEXT, session_id TEXT, position INTEGER, text TEXT, status TEXT, platform_id TEXT, time INTEGER);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS core_outbox_session_time ON core_outbox(session_id,time DESC);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS core_memories (id TEXT PRIMARY KEY, session_id TEXT, subject TEXT, content TEXT, type TEXT, confidence REAL, importance REAL, status TEXT, locked INTEGER DEFAULT 0, sources TEXT, created INTEGER, updated INTEGER, last_access INTEGER, expires INTEGER, version INTEGER DEFAULT 1);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS core_memories_scope ON core_memories(session_id,status);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS core_memory_versions (id INTEGER PRIMARY KEY, memory_id TEXT, time INTEGER, value TEXT);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS core_stages (id TEXT PRIMARY KEY, session_id TEXT, first_seq INTEGER, last_seq INTEGER, time INTEGER, data TEXT, UNIQUE(session_id,first_seq,last_seq));
|
|
25
|
+
CREATE TABLE IF NOT EXISTS core_cursors (session_id TEXT PRIMARY KEY, seq INTEGER DEFAULT 0);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS core_jobs (seq INTEGER PRIMARY KEY, session_id TEXT, status TEXT, trace_id TEXT, time INTEGER);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS core_references (id INTEGER PRIMARY KEY, session_id TEXT, platform_id TEXT, account_id TEXT, payload TEXT, UNIQUE(session_id,platform_id,account_id));
|
|
28
|
+
CREATE TABLE IF NOT EXISTS core_vision_cache (cache_key TEXT PRIMARY KEY, session_id TEXT, message_id INTEGER, description TEXT NOT NULL, created INTEGER NOT NULL);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS core_traces_time ON core_traces(time);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS core_context_summaries (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, level INTEGER NOT NULL, first_seq INTEGER NOT NULL, last_seq INTEGER NOT NULL, first_time INTEGER, last_time INTEGER, created INTEGER NOT NULL, data TEXT NOT NULL);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS core_context_summaries_session ON core_context_summaries(session_id,first_seq);
|
|
32
|
+
`);
|
|
33
|
+
// A saved persona or prompt may intentionally use an older name. Leave it
|
|
34
|
+
// intact; fresh workspaces receive the LuckyTri defaults from the store.
|
|
35
|
+
if (!db.prepare("SELECT id FROM core_config WHERE id='migration-v1'").get()) {
|
|
36
|
+
db.exec("BEGIN IMMEDIATE");
|
|
37
|
+
try {
|
|
38
|
+
for (const r of db
|
|
39
|
+
.prepare("SELECT * FROM messages WHERE is_demo=0 ORDER BY id")
|
|
40
|
+
.all()) {
|
|
41
|
+
const m = {
|
|
42
|
+
sessionId: r.session_id,
|
|
43
|
+
kind: (() => {
|
|
44
|
+
try {
|
|
45
|
+
return parseSessionKey(r.session_id).kind;
|
|
46
|
+
} catch {
|
|
47
|
+
return String(r.session_id).startsWith("private")
|
|
48
|
+
? "private"
|
|
49
|
+
: "group";
|
|
50
|
+
}
|
|
51
|
+
})(),
|
|
52
|
+
userId: r.user_id,
|
|
53
|
+
name: r.name,
|
|
54
|
+
text: r.text,
|
|
55
|
+
role: r.role,
|
|
56
|
+
time: r.time,
|
|
57
|
+
mentions: [],
|
|
58
|
+
attachments: [],
|
|
59
|
+
replyId: null,
|
|
60
|
+
metadataUnknown: true,
|
|
61
|
+
};
|
|
62
|
+
db.prepare(
|
|
63
|
+
"INSERT OR IGNORE INTO core_events(event_id,session_id,platform_id,account_id,time,role,payload) VALUES (?,?,?,?,?,?,?)",
|
|
64
|
+
).run(
|
|
65
|
+
r.event_id || `legacy:${r.id}`,
|
|
66
|
+
r.session_id,
|
|
67
|
+
null,
|
|
68
|
+
null,
|
|
69
|
+
r.time,
|
|
70
|
+
r.role,
|
|
71
|
+
JSON.stringify(m),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
db.prepare("INSERT INTO core_config(id,value) VALUES (?,?)").run(
|
|
75
|
+
"migration-v1",
|
|
76
|
+
JSON.stringify({ time: Date.now() }),
|
|
77
|
+
);
|
|
78
|
+
db.exec("COMMIT");
|
|
79
|
+
} catch (e) {
|
|
80
|
+
db.exec("ROLLBACK");
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// A restarted process cannot know whether an in-flight send reached QQ.
|
|
85
|
+
db.prepare(
|
|
86
|
+
"UPDATE core_outbox SET status='uncertain' WHERE status='sending'",
|
|
87
|
+
).run();
|
|
88
|
+
db.prepare(
|
|
89
|
+
"UPDATE core_outbox SET status='cancelled' WHERE status='pending'",
|
|
90
|
+
).run();
|
|
91
|
+
db.prepare(
|
|
92
|
+
"UPDATE core_traces SET status='interrupted' WHERE status='running'",
|
|
93
|
+
).run();
|
|
94
|
+
migrateKnowledge(db);
|
|
95
|
+
migrateMind(db, store);
|
|
96
|
+
migratePlugins(db);
|
|
97
|
+
db.exec(`PRAGMA user_version=${DATABASE_VERSION}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const TOKEN_FIELDS = [
|
|
101
|
+
"input",
|
|
102
|
+
"cachedRead",
|
|
103
|
+
"cacheWrite",
|
|
104
|
+
"output",
|
|
105
|
+
"reasoning",
|
|
106
|
+
];
|
|
107
|
+
export const TRACE_RETENTION_DAYS = 14;
|
|
108
|
+
|
|
109
|
+
function eventRow(r) {
|
|
110
|
+
return {
|
|
111
|
+
...JSON.parse(r.payload),
|
|
112
|
+
seq: r.seq,
|
|
113
|
+
eventId: r.event_id,
|
|
114
|
+
platformId: r.platform_id,
|
|
115
|
+
accountId: r.account_id,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function simulatedFilter(simulated) {
|
|
120
|
+
return simulated === null
|
|
121
|
+
? ""
|
|
122
|
+
: " AND COALESCE(json_extract(payload,'$.simulated'),0)=?";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class Repository {
|
|
126
|
+
constructor(store) {
|
|
127
|
+
this.store = store;
|
|
128
|
+
this.db = store.db;
|
|
129
|
+
migrateCore(store);
|
|
130
|
+
}
|
|
131
|
+
config(id, fallback = null) {
|
|
132
|
+
const r = this.db
|
|
133
|
+
.prepare("SELECT value FROM core_config WHERE id=?")
|
|
134
|
+
.get(id);
|
|
135
|
+
return r ? JSON.parse(r.value) : fallback;
|
|
136
|
+
}
|
|
137
|
+
saveConfig(id, value) {
|
|
138
|
+
this.db
|
|
139
|
+
.prepare(
|
|
140
|
+
"INSERT INTO core_config(id,value) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value,version=version+1",
|
|
141
|
+
)
|
|
142
|
+
.run(id, JSON.stringify(value));
|
|
143
|
+
this.store.revision++;
|
|
144
|
+
}
|
|
145
|
+
// `simulated` is deliberately explicit at context boundaries. The
|
|
146
|
+
// default keeps the repository useful for admin views and migrations, while
|
|
147
|
+
// callers building a live or demo prompt must choose one side of the
|
|
148
|
+
// boundary so a preview can never leak into a real conversation.
|
|
149
|
+
events(session, before = Number.MAX_SAFE_INTEGER, { simulated = null } = {}) {
|
|
150
|
+
const args =
|
|
151
|
+
simulated === null
|
|
152
|
+
? [session, before]
|
|
153
|
+
: [session, before, Number(!!simulated)];
|
|
154
|
+
return this.db
|
|
155
|
+
.prepare(
|
|
156
|
+
`SELECT * FROM core_events WHERE session_id=? AND seq<=?${simulatedFilter(simulated)} ORDER BY seq`,
|
|
157
|
+
)
|
|
158
|
+
.all(...args)
|
|
159
|
+
.map(eventRow);
|
|
160
|
+
}
|
|
161
|
+
eventsAfter(session, after = 0, { simulated = null, limit = null } = {}) {
|
|
162
|
+
const args =
|
|
163
|
+
simulated === null
|
|
164
|
+
? [session, after]
|
|
165
|
+
: [session, after, Number(!!simulated)];
|
|
166
|
+
const bounded = Number.isSafeInteger(limit) && limit > 0;
|
|
167
|
+
return this.db
|
|
168
|
+
.prepare(
|
|
169
|
+
`SELECT * FROM core_events WHERE session_id=? AND seq>?${simulatedFilter(simulated)} ORDER BY seq${bounded ? " LIMIT ?" : ""}`,
|
|
170
|
+
)
|
|
171
|
+
.all(...args, ...(bounded ? [limit] : []))
|
|
172
|
+
.map(eventRow);
|
|
173
|
+
}
|
|
174
|
+
// Unread attention needs the last matching users, not every event since its
|
|
175
|
+
// cursor. Apply role/time filters before decoding and before the row limit.
|
|
176
|
+
unreadEvents(session, after, since, limit = 40) {
|
|
177
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
178
|
+
return this.eventsAfter(session, after, { simulated: false })
|
|
179
|
+
.filter((m) => m.role === "user" && m.time >= since)
|
|
180
|
+
.slice(-limit);
|
|
181
|
+
return this.db
|
|
182
|
+
.prepare(
|
|
183
|
+
"SELECT * FROM core_events WHERE session_id=? AND seq>? AND role='user' AND time>=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT ?",
|
|
184
|
+
)
|
|
185
|
+
.all(session, after, since, limit)
|
|
186
|
+
.reverse()
|
|
187
|
+
.map(eventRow);
|
|
188
|
+
}
|
|
189
|
+
// The last few events of a session, oldest first, without reading the rest.
|
|
190
|
+
recentEvents(session, limit = 20, { simulated = null, role = null } = {}) {
|
|
191
|
+
const args = [session];
|
|
192
|
+
if (simulated !== null) args.push(Number(!!simulated));
|
|
193
|
+
if (role) args.push(role);
|
|
194
|
+
return this.db
|
|
195
|
+
.prepare(
|
|
196
|
+
`SELECT * FROM (SELECT * FROM core_events WHERE session_id=?${simulatedFilter(simulated)}${role ? " AND role=?" : ""} ORDER BY seq DESC LIMIT ?) ORDER BY seq`,
|
|
197
|
+
)
|
|
198
|
+
.all(...args, limit)
|
|
199
|
+
.map(eventRow);
|
|
200
|
+
}
|
|
201
|
+
latest(session) {
|
|
202
|
+
return (
|
|
203
|
+
this.db
|
|
204
|
+
.prepare("SELECT MAX(seq) n FROM core_events WHERE session_id=?")
|
|
205
|
+
.get(session).n || 0
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
references(session, { simulated = null } = {}) {
|
|
209
|
+
const filter =
|
|
210
|
+
simulated === null
|
|
211
|
+
? ""
|
|
212
|
+
: " AND COALESCE(json_extract(payload,'$.simulated'),0)=?";
|
|
213
|
+
const args =
|
|
214
|
+
simulated === null ? [session] : [session, Number(!!simulated)];
|
|
215
|
+
return this.db
|
|
216
|
+
.prepare(`SELECT * FROM core_references WHERE session_id=?${filter}`)
|
|
217
|
+
.all(...args)
|
|
218
|
+
.map((r) => ({
|
|
219
|
+
...JSON.parse(r.payload),
|
|
220
|
+
seq: -r.id,
|
|
221
|
+
platformId: r.platform_id,
|
|
222
|
+
accountId: r.account_id,
|
|
223
|
+
referenceOnly: true,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
append(m) {
|
|
227
|
+
const r = this.db
|
|
228
|
+
.prepare(
|
|
229
|
+
"INSERT OR IGNORE INTO core_events(event_id,session_id,platform_id,account_id,time,role,payload) VALUES (?,?,?,?,?,?,?)",
|
|
230
|
+
)
|
|
231
|
+
.run(
|
|
232
|
+
m.eventId,
|
|
233
|
+
m.sessionId,
|
|
234
|
+
m.platformId || null,
|
|
235
|
+
m.accountId || null,
|
|
236
|
+
m.time || Date.now(),
|
|
237
|
+
m.role || "user",
|
|
238
|
+
JSON.stringify(m),
|
|
239
|
+
);
|
|
240
|
+
return r.changes ? Number(r.lastInsertRowid) : null;
|
|
241
|
+
}
|
|
242
|
+
trace(session, mode = "live") {
|
|
243
|
+
const id = randomUUID();
|
|
244
|
+
this.db
|
|
245
|
+
.prepare("INSERT INTO core_traces VALUES (?,?,?,?,?,?)")
|
|
246
|
+
.run(id, session, Date.now(), mode, "running", "{}");
|
|
247
|
+
return {
|
|
248
|
+
id,
|
|
249
|
+
sessionId: session,
|
|
250
|
+
mode,
|
|
251
|
+
started: Date.now(),
|
|
252
|
+
calls: [],
|
|
253
|
+
steps: [],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
finish(trace, status) {
|
|
257
|
+
trace.status = status;
|
|
258
|
+
trace.elapsed = Date.now() - trace.started;
|
|
259
|
+
const counted = (trace.calls || []).filter((call) => call.tokens);
|
|
260
|
+
if (counted.length) {
|
|
261
|
+
trace.tokens = { calls: counted.length };
|
|
262
|
+
for (const field of TOKEN_FIELDS)
|
|
263
|
+
trace.tokens[field] = counted.reduce(
|
|
264
|
+
(sum, call) => sum + (Number(call.tokens[field]) || 0),
|
|
265
|
+
0,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
// Previews spend real tokens too, so every mode is counted once.
|
|
269
|
+
recordUsage(this.db, trace);
|
|
270
|
+
this.db
|
|
271
|
+
.prepare("UPDATE core_traces SET status=?,data=? WHERE id=?")
|
|
272
|
+
.run(status, JSON.stringify(trace), trace.id);
|
|
273
|
+
}
|
|
274
|
+
// Small batches: old traces can be megabytes each and SQLite runs on the
|
|
275
|
+
// event loop, so a large backlog is cleared over several maintenance ticks.
|
|
276
|
+
pruneTraces(
|
|
277
|
+
now = Date.now(),
|
|
278
|
+
{ days = TRACE_RETENTION_DAYS, batchSize = 200, maxBatches = 5 } = {},
|
|
279
|
+
) {
|
|
280
|
+
const remove = this.db.prepare(
|
|
281
|
+
"DELETE FROM core_traces WHERE rowid IN (SELECT rowid FROM core_traces WHERE time<? AND status!='running' LIMIT ?)",
|
|
282
|
+
);
|
|
283
|
+
let removed = 0;
|
|
284
|
+
for (let batch = 0; batch < maxBatches; batch++) {
|
|
285
|
+
const changes = Number(
|
|
286
|
+
remove.run(now - days * 86400000, batchSize).changes,
|
|
287
|
+
);
|
|
288
|
+
removed += changes;
|
|
289
|
+
if (changes < batchSize) return { removed, more: false };
|
|
290
|
+
}
|
|
291
|
+
return { removed, more: true };
|
|
292
|
+
}
|
|
293
|
+
}
|