luckytri 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/en/CHANGELOG.md +44 -24
  4. package/docs/en/architecture.md +4 -0
  5. package/docs/en/cognition.md +34 -0
  6. package/docs/en/guide.md +8 -6
  7. package/docs/zh/CHANGELOG.md +44 -24
  8. package/docs/zh/architecture.md +4 -0
  9. package/docs/zh/cognition.md +34 -0
  10. package/docs/zh/guide.md +8 -6
  11. package/package.json +112 -112
  12. package/public/app/assets/ChatsPage-DH4ZPRXB.js +3 -0
  13. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-DRsHeX-L.js} +1 -1
  14. package/public/app/assets/Field-BfIUcaNa.js +1 -0
  15. package/public/app/assets/HeartPage-BxgSUJJ_.js +1 -0
  16. package/public/app/assets/LifePage-CAKr8h_q.js +1 -0
  17. package/public/app/assets/MemoryPage-CFq8L_TC.js +1 -0
  18. package/public/app/assets/NaturePage-BU1iDno3.js +3 -0
  19. package/public/app/assets/PeoplePage-C6i_nAfr.js +1 -0
  20. package/public/app/assets/PluginsPage-DRV8lzk3.js +2 -0
  21. package/public/app/assets/SystemPage-21AlQBiC.js +1 -0
  22. package/public/app/assets/SystemPage-CMcuUdLm.css +1 -0
  23. package/public/app/assets/TimePage-DUzjLnZV.js +3 -0
  24. package/public/app/assets/index-WAvrlAcO.js +4 -0
  25. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-V_blbx9R.js} +1 -1
  26. package/public/app/assets/media-C7j5HqxK.js +1 -0
  27. package/public/app/assets/{mind-DtTVrW6Z.js → mind-BBAstMqH.js} +1 -1
  28. package/public/app/index.html +3 -3
  29. package/public/guide.en.html +13 -5
  30. package/public/guide.html +12 -5
  31. package/public/tutorial.en.md +3 -1
  32. package/public/tutorial.md +3 -1
  33. package/server/core/backoff.js +123 -0
  34. package/server/core/context-builder.js +307 -288
  35. package/server/core/contribution-check.js +50 -0
  36. package/server/core/conversation-cues.js +377 -385
  37. package/server/core/conversation-grounding.js +547 -304
  38. package/server/core/dialogue-context.js +247 -244
  39. package/server/core/dialogue-contract.js +33 -9
  40. package/server/core/embedding.js +246 -0
  41. package/server/core/expression-strategy.js +90 -0
  42. package/server/core/initiative-context.js +88 -68
  43. package/server/core/model-json.js +223 -190
  44. package/server/core/model-manager.js +256 -56
  45. package/server/core/model-presets.js +1045 -900
  46. package/server/core/orchestrator.js +41 -3
  47. package/server/core/persona-manager.js +364 -359
  48. package/server/core/reader-check.js +166 -62
  49. package/server/core/reply-delivery.js +688 -537
  50. package/server/core/repository.js +36 -0
  51. package/server/core/response-generator.js +101 -92
  52. package/server/core/response-validator.js +18 -3
  53. package/server/core/retired-prompts.js +53 -51
  54. package/server/core/time-expressions.js +143 -0
  55. package/server/core/turn-processor.js +175 -5
  56. package/server/core/turn.js +299 -44
  57. package/server/core/understanding.js +85 -0
  58. package/server/core/working-memory.js +149 -0
  59. package/server/i18n/decisions.js +225 -209
  60. package/server/i18n/errors.js +14 -0
  61. package/server/i18n/models.js +120 -111
  62. package/server/knowledge/api.js +362 -342
  63. package/server/knowledge/embedding-profile.js +158 -114
  64. package/server/knowledge/manager.js +6 -1
  65. package/server/mind/api.js +68 -0
  66. package/server/mind/continuity.js +634 -562
  67. package/server/mind/day-planner.js +52 -3
  68. package/server/mind/episodes.js +242 -0
  69. package/server/mind/expression-grounding.js +24 -21
  70. package/server/mind/index.js +13 -2
  71. package/server/mind/initiative.js +356 -338
  72. package/server/mind/life/index.js +6 -0
  73. package/server/mind/meetings.js +1112 -1069
  74. package/server/mind/memory.js +1091 -888
  75. package/server/mind/recall/index.js +776 -0
  76. package/server/mind/recall/schema.js +132 -0
  77. package/server/mind/recall/sources.js +220 -0
  78. package/server/mind/recall/store.js +368 -0
  79. package/server/mind/salience.js +202 -201
  80. package/server/mind/schema.js +568 -567
  81. package/server/mind/self.js +537 -496
  82. package/server/mind/time/activity-recall.js +65 -0
  83. package/server/mind/time/index.js +6 -2
  84. package/server/mind/time/tasks.js +36 -10
  85. package/server/mind/time/works.js +528 -527
  86. package/server/mind/view.js +63 -17
  87. package/server/storage/archive.js +1 -1
  88. package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
  89. package/public/app/assets/Field-BjFv4Rfn.js +0 -1
  90. package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
  91. package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
  92. package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
  93. package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
  94. package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
  95. package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
  96. package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
  97. package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
  98. package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
  99. package/public/app/assets/index-CyRtYxP3.js +0 -4
  100. package/public/app/assets/media-Bwtx8blz.js +0 -1
@@ -0,0 +1,368 @@
1
+ import { dot, textHash } from "../../core/embedding.js";
2
+ import { interestTerms } from "../attention.js";
3
+ import { KINDS, MESSAGE_DAYS, SOURCES } from "./sources.js";
4
+
5
+ // Overlapping character pairs and Latin words, so "考完试" and "他考完试"
6
+ // share tokens; fixed two-character chunks would not.
7
+ export const recallTokens = (text) => [...interestTerms([text])].join(" ");
8
+ export function recallMatch(text, limit = 32) {
9
+ return [...interestTerms([text])]
10
+ .slice(0, limit)
11
+ .map((term) => `"${term.replace(/"/g, "")}"`)
12
+ .join(" OR ");
13
+ }
14
+ const toBlob = (vector) =>
15
+ Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
16
+ const fromBlob = (blob) => {
17
+ const buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
18
+ return new Float32Array(
19
+ buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength),
20
+ );
21
+ };
22
+
23
+ // The searchable shadow of her memories: one row per item, its words for
24
+ // keyword search, its people for structured lookups, and vectors kept per
25
+ // embedding profile. Vectors of the active profile are held in memory for
26
+ // exact cosine search; at tens of thousands of items that is milliseconds.
27
+ export class RecallStore {
28
+ constructor(db) {
29
+ this.db = db;
30
+ this.vectors = new Map();
31
+ }
32
+ get(id) {
33
+ return this.db.prepare("SELECT * FROM mind_index WHERE id=?").get(id);
34
+ }
35
+ many(ids) {
36
+ if (!ids.length) return [];
37
+ const rows = [];
38
+ for (let i = 0; i < ids.length; i += 400) {
39
+ const part = ids.slice(i, i + 400);
40
+ rows.push(
41
+ ...this.db
42
+ .prepare(
43
+ `SELECT * FROM mind_index WHERE id IN (${part.map(() => "?").join(",")})`,
44
+ )
45
+ .all(...part),
46
+ );
47
+ }
48
+ return rows;
49
+ }
50
+ // Idempotent: the same item written twice changes nothing; changed words
51
+ // drop the old vectors so they are embedded again.
52
+ upsert(item) {
53
+ const hash = textHash(item.text);
54
+ const old = this.db
55
+ .prepare(
56
+ "SELECT text_hash,status,discretion,session_id FROM mind_index WHERE id=?",
57
+ )
58
+ .get(item.id);
59
+ if (
60
+ old &&
61
+ old.text_hash === hash &&
62
+ old.status === item.status &&
63
+ old.discretion === item.discretion &&
64
+ old.session_id === item.session_id
65
+ ) {
66
+ this.db
67
+ .prepare("UPDATE mind_index SET weight=?,updated=? WHERE id=?")
68
+ .run(item.weight, item.updated, item.id);
69
+ return false;
70
+ }
71
+ this.db
72
+ .prepare(
73
+ "INSERT INTO mind_index(id,kind,ref,session_id,discretion,basis,status,happened,updated,weight,text,text_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET session_id=excluded.session_id,discretion=excluded.discretion,basis=excluded.basis,status=excluded.status,happened=excluded.happened,updated=excluded.updated,weight=excluded.weight,text=excluded.text,text_hash=excluded.text_hash",
74
+ )
75
+ .run(
76
+ item.id,
77
+ item.kind,
78
+ item.ref,
79
+ item.session_id,
80
+ item.discretion,
81
+ item.basis,
82
+ item.status,
83
+ item.happened,
84
+ item.updated,
85
+ item.weight,
86
+ item.text,
87
+ hash,
88
+ );
89
+ if (!old || old.text_hash !== hash) {
90
+ this.db
91
+ .prepare("DELETE FROM mind_index_fts WHERE item_id=?")
92
+ .run(item.id);
93
+ const tokens = recallTokens(item.text);
94
+ if (tokens)
95
+ this.db
96
+ .prepare("INSERT INTO mind_index_fts(item_id,tokens) VALUES (?,?)")
97
+ .run(item.id, tokens);
98
+ this.db.prepare("DELETE FROM mind_vectors WHERE item_id=?").run(item.id);
99
+ for (const cache of this.vectors.values()) cache.dirty = true;
100
+ }
101
+ this.db
102
+ .prepare("DELETE FROM mind_index_people WHERE item_id=?")
103
+ .run(item.id);
104
+ const person = this.db.prepare(
105
+ "INSERT OR IGNORE INTO mind_index_people(item_id,user_id) VALUES (?,?)",
106
+ );
107
+ for (const id of item.people) person.run(item.id, id);
108
+ return true;
109
+ }
110
+ remove(id) {
111
+ const existed = this.db
112
+ .prepare("DELETE FROM mind_index WHERE id=?")
113
+ .run(id).changes;
114
+ this.db.prepare("DELETE FROM mind_index_fts WHERE item_id=?").run(id);
115
+ this.db.prepare("DELETE FROM mind_index_people WHERE item_id=?").run(id);
116
+ this.db.prepare("DELETE FROM mind_vectors WHERE item_id=?").run(id);
117
+ if (existed) for (const cache of this.vectors.values()) cache.dirty = true;
118
+ return !!existed;
119
+ }
120
+ // Re-read one memory from where it lives. Gone or hidden means gone here.
121
+ sync(kind, ref) {
122
+ const source = SOURCES[kind];
123
+ if (!source) return false;
124
+ const row = this.db.prepare(source.one).get(ref);
125
+ if (!row) return this.remove(`${kind}:${ref}`);
126
+ return this.upsert(source.map(row));
127
+ }
128
+ // Compares every memory of the given kinds with the index. Cheap enough to
129
+ // run at start and now and then; it also catches writes made by paths that
130
+ // do not call `sync`.
131
+ reconcile(kinds = KINDS) {
132
+ const stats = { added: 0, removed: 0, seen: 0 };
133
+ this.db.exec("SAVEPOINT recall_reconcile");
134
+ try {
135
+ for (const kind of kinds) {
136
+ const source = SOURCES[kind];
137
+ const live = new Set();
138
+ for (const row of this.db.prepare(source.select).iterate()) {
139
+ const entry = source.map(row);
140
+ if (!entry.text) continue;
141
+ live.add(entry.id);
142
+ stats.seen++;
143
+ if (this.upsert(entry)) stats.added++;
144
+ }
145
+ for (const { id } of this.db
146
+ .prepare("SELECT id FROM mind_index WHERE kind=?")
147
+ .all(kind))
148
+ if (!live.has(id) && this.remove(id)) stats.removed++;
149
+ }
150
+ this.db.exec("RELEASE recall_reconcile");
151
+ } catch (error) {
152
+ this.db.exec("ROLLBACK TO recall_reconcile");
153
+ this.db.exec("RELEASE recall_reconcile");
154
+ throw error;
155
+ }
156
+ return stats;
157
+ }
158
+ // Memories that changed after `since`, and those withdrawn since then.
159
+ // Returns the latest change time seen, the next watermark.
160
+ catchUp(since, kinds = KINDS) {
161
+ let latest = since;
162
+ this.db.exec("SAVEPOINT recall_catch_up");
163
+ try {
164
+ for (const kind of kinds) {
165
+ const source = SOURCES[kind];
166
+ for (const row of this.db
167
+ .prepare(
168
+ `SELECT *,${source.changed} AS __changed FROM (${source.select}) WHERE ${source.changed}>?`,
169
+ )
170
+ .iterate(since)) {
171
+ const entry = source.map(row);
172
+ if (entry.text) this.upsert(entry);
173
+ latest = Math.max(latest, Number(row.__changed) || 0);
174
+ }
175
+ }
176
+ // Messages older than the window leave the index; the words stay in
177
+ // core_events as the record they always were.
178
+ for (const { id } of this.db
179
+ .prepare(
180
+ `SELECT id FROM mind_index WHERE kind='message' AND happened<=(SELECT MAX(time) FROM core_events)-?`,
181
+ )
182
+ .all(MESSAGE_DAYS * 86400000))
183
+ this.remove(id);
184
+ const KIND_OF = {
185
+ self: "self",
186
+ meeting: "meeting",
187
+ thought: "thought",
188
+ anticipation: "anticipation",
189
+ };
190
+ for (const row of this.db
191
+ .prepare(
192
+ "SELECT target_kind,target_id,created FROM mind_revocations WHERE created>?",
193
+ )
194
+ .iterate(since)) {
195
+ if (KIND_OF[row.target_kind])
196
+ this.sync(KIND_OF[row.target_kind], row.target_id);
197
+ latest = Math.max(latest, row.created);
198
+ }
199
+ this.db.exec("RELEASE recall_catch_up");
200
+ } catch (error) {
201
+ this.db.exec("ROLLBACK TO recall_catch_up");
202
+ this.db.exec("RELEASE recall_catch_up");
203
+ throw error;
204
+ }
205
+ return latest;
206
+ }
207
+ // The first full build, a page at a time, yielding between pages so a
208
+ // large old database does not hold up conversations while it is indexed.
209
+ async rebuild({ page = 400, kinds = KINDS } = {}) {
210
+ const stats = { added: 0, removed: 0, seen: 0 };
211
+ let latest = 0;
212
+ for (const kind of kinds) {
213
+ const source = SOURCES[kind];
214
+ const live = new Set();
215
+ for (let offset = 0; ; offset += page) {
216
+ const rows = this.db
217
+ .prepare(
218
+ `SELECT *,${source.changed} AS __changed FROM (${source.select}) LIMIT ? OFFSET ?`,
219
+ )
220
+ .all(page, offset);
221
+ if (!rows.length) break;
222
+ this.db.exec("BEGIN IMMEDIATE");
223
+ try {
224
+ for (const row of rows) {
225
+ const entry = source.map(row);
226
+ latest = Math.max(latest, Number(row.__changed) || 0);
227
+ if (!entry.text) continue;
228
+ live.add(entry.id);
229
+ stats.seen++;
230
+ if (this.upsert(entry)) stats.added++;
231
+ }
232
+ this.db.exec("COMMIT");
233
+ } catch (error) {
234
+ this.db.exec("ROLLBACK");
235
+ throw error;
236
+ }
237
+ await new Promise((resolve) => setImmediate(resolve));
238
+ }
239
+ for (const { id } of this.db
240
+ .prepare("SELECT id FROM mind_index WHERE kind=?")
241
+ .all(kind))
242
+ if (!live.has(id) && this.remove(id)) stats.removed++;
243
+ }
244
+ const revoked = this.db
245
+ .prepare("SELECT MAX(created) t FROM mind_revocations")
246
+ .get().t;
247
+ return { ...stats, latest: Math.max(latest, revoked || 0) };
248
+ }
249
+ // Items that still need a vector for this profile, oldest words first so a
250
+ // rebuild is resumable.
251
+ pending(profile, limit = 100, kinds = KINDS) {
252
+ return this.db
253
+ .prepare(
254
+ `SELECT i.id,i.text,i.text_hash FROM mind_index i
255
+ LEFT JOIN mind_vectors v ON v.item_id=i.id AND v.profile=?
256
+ WHERE (v.item_id IS NULL OR v.text_hash!=i.text_hash) AND i.status!='candidate'
257
+ AND i.kind IN (${kinds.map((k) => `'${k.replace(/[^a-z]/g, "")}'`).join(",")})
258
+ ORDER BY CASE i.kind WHEN 'fact' THEN 0 WHEN 'episode' THEN 1 WHEN 'anticipation' THEN 2
259
+ WHEN 'self' THEN 3 WHEN 'thought' THEN 4 WHEN 'diary' THEN 5 WHEN 'period' THEN 6 WHEN 'message' THEN 7 ELSE 8 END,
260
+ i.updated DESC LIMIT ?`,
261
+ )
262
+ .all(profile, limit);
263
+ }
264
+ coverage(profile, kinds = KINDS) {
265
+ return this.db
266
+ .prepare(
267
+ `SELECT COUNT(*) total, SUM(CASE WHEN v.text_hash=i.text_hash THEN 1 ELSE 0 END) embedded
268
+ FROM mind_index i LEFT JOIN mind_vectors v ON v.item_id=i.id AND v.profile=?
269
+ WHERE i.status!='candidate'
270
+ AND i.kind IN (${kinds.map((k) => `'${k.replace(/[^a-z]/g, "")}'`).join(",")})`,
271
+ )
272
+ .get(profile);
273
+ }
274
+ // A vector is stored only if the words it was made from are still current.
275
+ saveVector(id, profile, hash, vector, now) {
276
+ const current = this.db
277
+ .prepare("SELECT text_hash FROM mind_index WHERE id=?")
278
+ .get(id);
279
+ if (!current || current.text_hash !== hash) return false;
280
+ this.db
281
+ .prepare(
282
+ "INSERT INTO mind_vectors(item_id,profile,text_hash,dim,vector,created) VALUES (?,?,?,?,?,?) ON CONFLICT(item_id,profile) DO UPDATE SET text_hash=excluded.text_hash,dim=excluded.dim,vector=excluded.vector,created=excluded.created",
283
+ )
284
+ .run(id, profile, hash, vector.length, toBlob(vector), now);
285
+ const cache = this.vectors.get(profile);
286
+ if (cache && !cache.dirty) {
287
+ const at = cache.index.get(id);
288
+ if (at === undefined) {
289
+ cache.index.set(id, cache.ids.length);
290
+ cache.ids.push(id);
291
+ cache.rows.push(vector);
292
+ } else cache.rows[at] = vector;
293
+ }
294
+ return true;
295
+ }
296
+ #cache(profile) {
297
+ let cache = this.vectors.get(profile);
298
+ if (!cache || cache.dirty) {
299
+ cache = { ids: [], rows: [], index: new Map(), dirty: false };
300
+ for (const row of this.db
301
+ .prepare(
302
+ "SELECT v.item_id,v.vector FROM mind_vectors v JOIN mind_index i ON i.id=v.item_id AND i.text_hash=v.text_hash WHERE v.profile=?",
303
+ )
304
+ .iterate(profile)) {
305
+ cache.index.set(row.item_id, cache.ids.length);
306
+ cache.ids.push(row.item_id);
307
+ cache.rows.push(fromBlob(row.vector));
308
+ }
309
+ this.vectors.set(profile, cache);
310
+ }
311
+ return cache;
312
+ }
313
+ vector(id, profile) {
314
+ const cache = this.#cache(profile);
315
+ const at = cache.index.get(id);
316
+ return at === undefined ? null : cache.rows[at];
317
+ }
318
+ // Exact nearest items. `allow` filters before ranking, so what may not be
319
+ // recalled here never takes a place in the list.
320
+ nearest(
321
+ profile,
322
+ query,
323
+ { limit = 50, allow = () => true, minScore = 0 } = {},
324
+ ) {
325
+ const cache = this.#cache(profile);
326
+ const hits = [];
327
+ for (let i = 0; i < cache.ids.length; i++) {
328
+ const id = cache.ids[i];
329
+ if (!allow(id)) continue;
330
+ const score = dot(query, cache.rows[i]);
331
+ if (score >= minScore) hits.push({ id, score });
332
+ }
333
+ hits.sort((a, b) => b.score - a.score);
334
+ return hits.slice(0, limit);
335
+ }
336
+ keyword(text, limit = 100) {
337
+ const match = recallMatch(text);
338
+ if (!match) return [];
339
+ try {
340
+ return this.db
341
+ .prepare(
342
+ "SELECT item_id id FROM mind_index_fts WHERE tokens MATCH ? ORDER BY rank LIMIT ?",
343
+ )
344
+ .all(match, limit)
345
+ .map((row) => row.id);
346
+ } catch {
347
+ return [];
348
+ }
349
+ }
350
+ byPeople(people, kinds, limit = 200) {
351
+ if (!people.length) return [];
352
+ return this.db
353
+ .prepare(
354
+ `SELECT i.id FROM mind_index_people p JOIN mind_index i ON i.id=p.item_id
355
+ WHERE p.user_id IN (${people.map(() => "?").join(",")})
356
+ AND i.kind IN (${kinds.map(() => "?").join(",")})
357
+ ORDER BY i.updated DESC LIMIT ?`,
358
+ )
359
+ .all(...people, ...kinds, limit)
360
+ .map((row) => row.id);
361
+ }
362
+ people(id) {
363
+ return this.db
364
+ .prepare("SELECT user_id FROM mind_index_people WHERE item_id=?")
365
+ .all(id)
366
+ .map((row) => row.user_id);
367
+ }
368
+ }