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.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. 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
+ }