luckytri 1.0.9 → 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 (96) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/en/CHANGELOG.md +9 -0
  4. package/docs/en/architecture.md +4 -0
  5. package/docs/en/cognition.md +34 -0
  6. package/docs/en/guide.md +5 -5
  7. package/docs/zh/CHANGELOG.md +9 -0
  8. package/docs/zh/architecture.md +4 -0
  9. package/docs/zh/cognition.md +34 -0
  10. package/docs/zh/guide.md +5 -5
  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 +3 -3
  30. package/public/guide.html +3 -3
  31. package/server/core/backoff.js +123 -0
  32. package/server/core/context-builder.js +307 -307
  33. package/server/core/contribution-check.js +50 -50
  34. package/server/core/conversation-cues.js +377 -377
  35. package/server/core/conversation-grounding.js +547 -547
  36. package/server/core/dialogue-context.js +247 -247
  37. package/server/core/dialogue-contract.js +1 -1
  38. package/server/core/embedding.js +246 -0
  39. package/server/core/expression-strategy.js +90 -0
  40. package/server/core/initiative-context.js +88 -88
  41. package/server/core/model-json.js +223 -223
  42. package/server/core/model-manager.js +254 -54
  43. package/server/core/model-presets.js +1045 -1045
  44. package/server/core/orchestrator.js +41 -3
  45. package/server/core/persona-manager.js +364 -364
  46. package/server/core/reader-check.js +166 -166
  47. package/server/core/reply-delivery.js +688 -678
  48. package/server/core/repository.js +3 -0
  49. package/server/core/response-generator.js +101 -101
  50. package/server/core/retired-prompts.js +53 -51
  51. package/server/core/time-expressions.js +143 -0
  52. package/server/core/turn-processor.js +154 -6
  53. package/server/core/turn.js +41 -0
  54. package/server/core/understanding.js +85 -0
  55. package/server/core/working-memory.js +149 -0
  56. package/server/i18n/decisions.js +225 -225
  57. package/server/i18n/errors.js +12 -0
  58. package/server/i18n/models.js +120 -120
  59. package/server/knowledge/api.js +362 -342
  60. package/server/knowledge/embedding-profile.js +158 -114
  61. package/server/knowledge/manager.js +6 -1
  62. package/server/mind/api.js +68 -0
  63. package/server/mind/continuity.js +634 -562
  64. package/server/mind/day-planner.js +52 -3
  65. package/server/mind/episodes.js +242 -0
  66. package/server/mind/expression-grounding.js +24 -24
  67. package/server/mind/index.js +13 -2
  68. package/server/mind/initiative.js +356 -338
  69. package/server/mind/life/index.js +6 -0
  70. package/server/mind/meetings.js +1112 -1069
  71. package/server/mind/memory.js +1091 -888
  72. package/server/mind/recall/index.js +776 -0
  73. package/server/mind/recall/schema.js +132 -0
  74. package/server/mind/recall/sources.js +220 -0
  75. package/server/mind/recall/store.js +368 -0
  76. package/server/mind/salience.js +202 -201
  77. package/server/mind/schema.js +568 -567
  78. package/server/mind/self.js +537 -496
  79. package/server/mind/time/activity-recall.js +65 -65
  80. package/server/mind/time/tasks.js +28 -0
  81. package/server/mind/time/works.js +528 -528
  82. package/server/mind/view.js +54 -15
  83. package/server/storage/archive.js +1 -1
  84. package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
  85. package/public/app/assets/Field-BjFv4Rfn.js +0 -1
  86. package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
  87. package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
  88. package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
  89. package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
  90. package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
  91. package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
  92. package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
  93. package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
  94. package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
  95. package/public/app/assets/index-CyRtYxP3.js +0 -4
  96. package/public/app/assets/media-Bwtx8blz.js +0 -1
@@ -0,0 +1,132 @@
1
+ import { recallTokens } from "./store.js";
2
+
3
+ // The recall index is derived: every row can be rebuilt from the tables that
4
+ // hold her actual memories, so it may be dropped and refilled at any time.
5
+ // Episodes are memories in their own right and follow the append-only rule:
6
+ // a later state of the same affair is a new row in the same thread.
7
+ export function migrateRecall(db) {
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS mind_index(
10
+ id TEXT PRIMARY KEY,
11
+ kind TEXT NOT NULL,
12
+ ref TEXT NOT NULL,
13
+ session_id TEXT,
14
+ discretion TEXT NOT NULL DEFAULT 'open',
15
+ basis TEXT NOT NULL DEFAULT 'stated',
16
+ status TEXT NOT NULL DEFAULT 'active',
17
+ happened INTEGER NOT NULL,
18
+ updated INTEGER NOT NULL,
19
+ weight REAL NOT NULL DEFAULT 0.5,
20
+ text TEXT NOT NULL,
21
+ text_hash TEXT NOT NULL
22
+ );
23
+ CREATE INDEX IF NOT EXISTS mind_index_kind ON mind_index(kind,status,happened);
24
+ CREATE INDEX IF NOT EXISTS mind_index_ref ON mind_index(kind,ref);
25
+ CREATE TABLE IF NOT EXISTS mind_index_people(
26
+ item_id TEXT NOT NULL,
27
+ user_id TEXT NOT NULL,
28
+ PRIMARY KEY(item_id,user_id)
29
+ );
30
+ CREATE INDEX IF NOT EXISTS mind_index_people_user ON mind_index_people(user_id,item_id);
31
+ CREATE TABLE IF NOT EXISTS mind_vectors(
32
+ item_id TEXT NOT NULL,
33
+ profile TEXT NOT NULL,
34
+ text_hash TEXT NOT NULL,
35
+ dim INTEGER NOT NULL,
36
+ vector BLOB NOT NULL,
37
+ created INTEGER NOT NULL,
38
+ PRIMARY KEY(item_id,profile)
39
+ );
40
+ CREATE INDEX IF NOT EXISTS mind_vectors_profile ON mind_vectors(profile);
41
+ CREATE TABLE IF NOT EXISTS mind_episodes(
42
+ id TEXT PRIMARY KEY,
43
+ thread_id TEXT NOT NULL,
44
+ created INTEGER NOT NULL,
45
+ session_id TEXT NOT NULL,
46
+ title TEXT NOT NULL,
47
+ content TEXT NOT NULL,
48
+ outcome TEXT NOT NULL DEFAULT '',
49
+ status TEXT NOT NULL DEFAULT 'open',
50
+ participants TEXT NOT NULL DEFAULT '[]',
51
+ happened_start INTEGER,
52
+ happened_end INTEGER,
53
+ follow_up_at INTEGER,
54
+ basis TEXT NOT NULL DEFAULT 'witnessed',
55
+ importance REAL NOT NULL DEFAULT 0.5,
56
+ discretion TEXT NOT NULL DEFAULT 'open',
57
+ sources TEXT NOT NULL DEFAULT '[]',
58
+ run_id TEXT
59
+ );
60
+ CREATE INDEX IF NOT EXISTS mind_episodes_thread ON mind_episodes(thread_id,created);
61
+ CREATE INDEX IF NOT EXISTS mind_episodes_time ON mind_episodes(created);
62
+ -- How two memories relate: an episode continues or resolves another, a
63
+ -- fact may conflict with an older one, two events may be alike. Ends
64
+ -- are index ids such as "episode:ID" or "fact:ID". "similar" is only a
65
+ -- resemblance found by meaning; it never asserts that either is true.
66
+ CREATE TABLE IF NOT EXISTS mind_links(
67
+ from_id TEXT NOT NULL,
68
+ to_id TEXT NOT NULL,
69
+ relation TEXT NOT NULL,
70
+ created INTEGER NOT NULL,
71
+ sources TEXT NOT NULL DEFAULT '[]',
72
+ PRIMARY KEY(from_id,to_id,relation)
73
+ );
74
+ CREATE INDEX IF NOT EXISTS mind_links_to ON mind_links(to_id,relation);
75
+ -- How the turn read a message it answered: in earnest or as a joke, a
76
+ -- fact or a guess. Consolidation uses it so a joke is not filed as fact.
77
+ CREATE TABLE IF NOT EXISTS core_message_cues(
78
+ seq INTEGER PRIMARY KEY,
79
+ tone TEXT,
80
+ stance TEXT,
81
+ trace_id TEXT,
82
+ created INTEGER NOT NULL
83
+ );
84
+ `);
85
+ db.exec(
86
+ "CREATE VIRTUAL TABLE IF NOT EXISTS mind_index_fts USING fts5(item_id UNINDEXED, tokens)",
87
+ );
88
+ // Keyword tokens moved to overlapping character pairs; rebuild once.
89
+ if (!db.prepare("SELECT 1 FROM core_config WHERE id='recall-fts-v2'").get()) {
90
+ db.exec("DELETE FROM mind_index_fts");
91
+ const insert = db.prepare(
92
+ "INSERT INTO mind_index_fts(item_id,tokens) VALUES (?,?)",
93
+ );
94
+ for (const row of db.prepare("SELECT id,text FROM mind_index").iterate()) {
95
+ const tokens = recallTokens(row.text);
96
+ if (tokens) insert.run(row.id, tokens);
97
+ }
98
+ db.prepare("INSERT INTO core_config(id,value) VALUES (?,?)").run(
99
+ "recall-fts-v2",
100
+ JSON.stringify({ time: Date.now() }),
101
+ );
102
+ }
103
+ const columns = new Set(
104
+ db
105
+ .prepare("PRAGMA table_info(core_memories)")
106
+ .all()
107
+ .map((c) => c.name),
108
+ );
109
+ // How a fact was learned, how often it was said again, and when last.
110
+ if (!columns.has("basis"))
111
+ db.exec(
112
+ "ALTER TABLE core_memories ADD COLUMN basis TEXT NOT NULL DEFAULT 'stated'",
113
+ );
114
+ if (!columns.has("evidence_count"))
115
+ db.exec(
116
+ "ALTER TABLE core_memories ADD COLUMN evidence_count INTEGER NOT NULL DEFAULT 1",
117
+ );
118
+ if (!columns.has("last_confirmed"))
119
+ db.exec("ALTER TABLE core_memories ADD COLUMN last_confirmed INTEGER");
120
+ if (
121
+ !db.prepare("SELECT 1 FROM core_config WHERE id='memory-basis-v1'").get()
122
+ ) {
123
+ db.exec(`UPDATE core_memories SET basis=CASE
124
+ WHEN type='inferred' OR sources LIKE '%"certainty":"inferred"%' THEN 'inferred'
125
+ WHEN type='hearsay' OR sources LIKE '%"certainty":"hearsay"%' THEN 'hearsay'
126
+ ELSE 'stated' END`);
127
+ db.prepare("INSERT INTO core_config(id,value) VALUES (?,?)").run(
128
+ "memory-basis-v1",
129
+ JSON.stringify({ time: Date.now() }),
130
+ );
131
+ }
132
+ }
@@ -0,0 +1,220 @@
1
+ import { parse, text } from "../util.js";
2
+
3
+ // How each kind of memory becomes one searchable item. The text is what the
4
+ // item is about, written the way it would be recalled; people are the users it
5
+ // concerns; discretion and session say where it may surface.
6
+ const MAX_TEXT = 600;
7
+ const revoked = (kind) =>
8
+ `SELECT target_id FROM mind_revocations WHERE target_kind='${kind}'`;
9
+
10
+ function item(kind, ref, fields) {
11
+ return {
12
+ id: `${kind}:${ref}`,
13
+ kind,
14
+ ref: String(ref),
15
+ session_id: fields.session_id ?? null,
16
+ discretion: fields.discretion || "open",
17
+ basis: fields.basis || "stated",
18
+ status: fields.status || "active",
19
+ happened: Number(fields.happened) || 0,
20
+ updated: Number(fields.updated ?? fields.happened) || 0,
21
+ weight: Number.isFinite(Number(fields.weight))
22
+ ? Number(fields.weight)
23
+ : 0.5,
24
+ text: text(fields.text, MAX_TEXT),
25
+ people: [...new Set((fields.people || []).map(String).filter(Boolean))],
26
+ };
27
+ }
28
+
29
+ const FACT_STATUS = {
30
+ confirmed: "active",
31
+ superseded: "superseded",
32
+ candidate: "candidate",
33
+ archived: "archived",
34
+ };
35
+
36
+ export const SOURCES = {
37
+ fact: {
38
+ changed: "updated",
39
+ select:
40
+ "SELECT id,session_id,subject,content,importance,status,discretion,basis,created,updated FROM core_memories WHERE status!='deleted'",
41
+ one: "SELECT id,session_id,subject,content,importance,status,discretion,basis,created,updated FROM core_memories WHERE id=? AND status!='deleted'",
42
+ map: (row) =>
43
+ item("fact", row.id, {
44
+ session_id: row.session_id,
45
+ discretion: row.discretion,
46
+ basis: row.basis,
47
+ status: FACT_STATUS[row.status] || "candidate",
48
+ happened: row.created,
49
+ updated: row.updated,
50
+ weight: row.importance,
51
+ text: row.content,
52
+ people: [row.subject],
53
+ }),
54
+ },
55
+ meeting: {
56
+ changed: "created",
57
+ select: `SELECT m.*,(SELECT group_concat(user_id) FROM mind_meeting_people p WHERE p.meeting_id=m.id) who
58
+ FROM mind_meetings m WHERE (m.appraisal!='' OR m.exchange!='null') AND m.id NOT IN (${revoked("meeting")})`,
59
+ one: `SELECT m.*,(SELECT group_concat(user_id) FROM mind_meeting_people p WHERE p.meeting_id=m.id) who
60
+ FROM mind_meetings m WHERE m.id=? AND (m.appraisal!='' OR m.exchange!='null') AND m.id NOT IN (${revoked("meeting")})`,
61
+ map: (row) => {
62
+ const exchange = parse(row.exchange, null);
63
+ const they = (exchange?.they || [])
64
+ .map((line) => text(line.text, 120))
65
+ .join(" / ");
66
+ const said = (exchange?.iSaid || []).map((l) => text(l, 120)).join(" / ");
67
+ return item("meeting", row.id, {
68
+ session_id: row.session_id,
69
+ discretion: row.discretion,
70
+ basis: "witnessed",
71
+ status: "active",
72
+ happened: row.created,
73
+ weight: row.choice === "silent" ? 0.3 : 0.45,
74
+ text: [
75
+ row.topic,
76
+ row.appraisal,
77
+ they && `对方说:${they}`,
78
+ said && `我说:${said}`,
79
+ ]
80
+ .filter(Boolean)
81
+ .join("。"),
82
+ people: String(row.who || "").split(","),
83
+ });
84
+ },
85
+ },
86
+ self: {
87
+ changed: "created",
88
+ select: `SELECT s.* FROM mind_self s JOIN (SELECT thread,MAX(created) last FROM mind_self GROUP BY thread) l
89
+ ON l.thread=s.thread AND l.last=s.created WHERE s.thread NOT IN (${revoked("self")})`,
90
+ one: `SELECT * FROM mind_self WHERE thread=? AND thread NOT IN (${revoked("self")}) ORDER BY created DESC LIMIT 1`,
91
+ map: (row) =>
92
+ item("self", row.thread, {
93
+ session_id: row.session_id,
94
+ basis: "self",
95
+ status: row.status === "closed" ? "closed" : "active",
96
+ happened: row.created,
97
+ weight: row.strength,
98
+ text: [row.content, row.subject].filter(Boolean).join(" "),
99
+ people: row.subject ? [String(row.subject)] : [],
100
+ }),
101
+ },
102
+ thought: {
103
+ changed: "MAX(created,COALESCE(resolved_at,0))",
104
+ select: "SELECT * FROM mind_thoughts WHERE hidden=0",
105
+ one: "SELECT * FROM mind_thoughts WHERE id=? AND hidden=0",
106
+ map: (row) =>
107
+ item("thought", row.id, {
108
+ session_id: parse(row.sessions, [])[0] || null,
109
+ basis: "self",
110
+ status: row.status === "open" ? "active" : "resolved",
111
+ happened: row.created,
112
+ updated: row.resolved_at || row.created,
113
+ weight: row.importance,
114
+ text: row.content,
115
+ }),
116
+ },
117
+ diary: {
118
+ changed: "created",
119
+ select: `SELECT d.* FROM mind_diary d JOIN (SELECT day,MAX(created) last FROM mind_diary GROUP BY day) l
120
+ ON l.day=d.day AND l.last=d.created`,
121
+ one: "SELECT * FROM mind_diary WHERE day=? ORDER BY created DESC LIMIT 1",
122
+ key: "day",
123
+ map: (row) =>
124
+ item("diary", row.day, {
125
+ basis: "self",
126
+ happened: row.created,
127
+ weight: 0.4,
128
+ text: `${row.day} 的日记:${row.content}`,
129
+ }),
130
+ },
131
+ period: {
132
+ changed: "created",
133
+ select: "SELECT * FROM mind_periods",
134
+ one: "SELECT * FROM mind_periods WHERE id=?",
135
+ map: (row) =>
136
+ item("period", row.id, {
137
+ basis: "self",
138
+ happened: row.period_end || row.created,
139
+ weight: 0.4,
140
+ text:
141
+ [row.title, row.content].filter(Boolean).join(":") || row.content,
142
+ }),
143
+ },
144
+ anticipation: {
145
+ changed: "MAX(created,COALESCE(closed_at,0))",
146
+ select: `SELECT * FROM mind_anticipations WHERE id NOT IN (${revoked("anticipation")})`,
147
+ one: `SELECT * FROM mind_anticipations WHERE id=? AND id NOT IN (${revoked("anticipation")})`,
148
+ map: (row) =>
149
+ item("anticipation", row.id, {
150
+ session_id: row.session_id,
151
+ discretion: row.discretion,
152
+ basis: row.kind === "promise" ? "self" : "stated",
153
+ status: row.status === "pending" ? "active" : row.status,
154
+ happened: row.created,
155
+ updated: row.closed_at || row.created,
156
+ weight: 0.6,
157
+ text: row.content,
158
+ people: [row.subject],
159
+ }),
160
+ },
161
+ episode: {
162
+ changed: "created",
163
+ select: "SELECT * FROM mind_episodes",
164
+ one: "SELECT * FROM mind_episodes WHERE id=?",
165
+ map: (row) =>
166
+ item("episode", row.id, {
167
+ session_id: row.session_id,
168
+ discretion: row.discretion,
169
+ basis: row.basis,
170
+ status: row.status,
171
+ happened: row.happened_start || row.created,
172
+ updated: row.created,
173
+ weight: row.importance,
174
+ text: [row.title, row.content, row.outcome && `结果:${row.outcome}`]
175
+ .filter(Boolean)
176
+ .join("。"),
177
+ people: parse(row.participants, []),
178
+ }),
179
+ },
180
+ };
181
+
182
+ // Raw messages are evidence, not memories: they are searchable for a month
183
+ // so an older line of the same conversation can be found by meaning, then
184
+ // leave the index. The window is measured from the newest event, not the
185
+ // wall clock, so a replay or a test sees the same index.
186
+ export const MESSAGE_DAYS = 30;
187
+ const recent = `time>(SELECT MAX(time) FROM core_events)-${MESSAGE_DAYS * 86400000}`;
188
+ SOURCES.message = {
189
+ changed: "time",
190
+ select: `SELECT seq,session_id,time,role,payload FROM core_events
191
+ WHERE role IN ('user','assistant') AND ${recent}
192
+ AND COALESCE(json_extract(payload,'$.simulated'),0)=0
193
+ AND json_extract(payload,'$.artifact') IS NULL
194
+ AND seq NOT IN (SELECT seq FROM mind_unlived)`,
195
+ one: `SELECT seq,session_id,time,role,payload FROM core_events
196
+ WHERE seq=? AND role IN ('user','assistant') AND ${recent}
197
+ AND COALESCE(json_extract(payload,'$.simulated'),0)=0
198
+ AND json_extract(payload,'$.artifact') IS NULL
199
+ AND seq NOT IN (SELECT seq FROM mind_unlived)`,
200
+ map: (row) => {
201
+ const message = parse(row.payload, {});
202
+ const words = String(message.text || "");
203
+ return item("message", row.seq, {
204
+ session_id: row.session_id,
205
+ discretion: /private/.test(row.session_id) ? "private" : "open",
206
+ basis: "witnessed",
207
+ happened: row.time,
208
+ weight: 0.3,
209
+ text:
210
+ /^\[(?:图片|表情|媒体)\]+$/.test(words) || words.length < 2
211
+ ? ""
212
+ : words,
213
+ people: row.role === "user" ? [message.userId] : [],
214
+ });
215
+ },
216
+ };
217
+
218
+ export const KINDS = Object.keys(SOURCES);
219
+ // Kinds that are memories proper; raw messages are searched only on purpose.
220
+ export const MEMORY_KINDS = KINDS.filter((kind) => kind !== "message");