luckytri 1.0.4 → 1.0.6

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 (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,274 +1,274 @@
1
- import { randomUUID } from "node:crypto";
2
- import { evidenceRoots } from "./evidence.js";
3
- import {
4
- THOUGHT_FADED,
5
- anyTouches,
6
- cueList,
7
- thoughtSalience,
8
- } from "./salience.js";
9
- import { HOUR, clamp, evidence, parse, text } from "./util.js";
10
-
11
- export const THOUGHT_KINDS = {
12
- reflection: "后来想到",
13
- revision: "重新理解",
14
- unfinished: "仍放在心上",
15
- reconnection: "久别想起",
16
- expression: "自己的念头",
17
- };
18
-
19
- function row(value) {
20
- return value
21
- ? {
22
- ...value,
23
- sessions: parse(value.sessions, []),
24
- sources: parse(value.sources, []),
25
- outreach_draft: parse(value.outreach_draft, []),
26
- }
27
- : null;
28
- }
29
-
30
- // Her journal between conversations. A later understanding is a new entry
31
- // pointing at the old one; the old words stay as they were.
32
- export class Thoughts {
33
- constructor(mind) {
34
- this.mind = mind;
35
- this.db = mind.db;
36
- }
37
- add({
38
- kind,
39
- content,
40
- sessions = [],
41
- sources = [],
42
- parentId = null,
43
- importance = 0.5,
44
- revisitHours = 0,
45
- outreach = "",
46
- outreachSession = null,
47
- outreachAfterHours,
48
- outreachReason = "",
49
- runId = null,
50
- time = Date.now(),
51
- }) {
52
- const id = randomUUID();
53
- const hours = Number(revisitHours);
54
- this.db
55
- .prepare(
56
- "INSERT INTO mind_thoughts(id,created,kind,content,sessions,sources,parent_id,importance,revisit_at,status,hidden,outreach,outreach_session,outreach_status,run_id,outreach_at,outreach_reason) VALUES (?,?,?,?,?,?,?,?,?,'open',0,?,?,?,?,?,?)",
57
- )
58
- .run(
59
- id,
60
- time,
61
- THOUGHT_KINDS[kind] ? kind : "reflection",
62
- text(content, 600),
63
- JSON.stringify([...new Set(sessions)].slice(0, 12)),
64
- JSON.stringify(evidence(sources)),
65
- parentId || null,
66
- clamp(importance),
67
- Number.isFinite(hours) && hours > 0
68
- ? time + Math.min(8760, Math.max(1, hours)) * HOUR
69
- : null,
70
- text(outreach, 120),
71
- outreach ? outreachSession : null,
72
- outreach ? "planned" : "none",
73
- runId,
74
- outreach
75
- ? time +
76
- Math.min(
77
- 8760,
78
- Math.max(0, Number(outreachAfterHours ?? revisitHours) || 0),
79
- ) *
80
- HOUR
81
- : null,
82
- text(outreachReason, 300),
83
- );
84
- return id;
85
- }
86
- get(id) {
87
- return row(
88
- this.db.prepare("SELECT * FROM mind_thoughts WHERE id=?").get(id),
89
- );
90
- }
91
- list({
92
- before = Number.MAX_SAFE_INTEGER,
93
- session = "",
94
- q = "",
95
- limit = 30,
96
- hidden = true,
97
- } = {}) {
98
- return this.db
99
- .prepare(
100
- `SELECT * FROM mind_thoughts WHERE created<?${hidden ? "" : " AND hidden=0"}${session ? " AND sessions LIKE ?" : ""}${q ? " AND content LIKE ?" : ""} ORDER BY created DESC LIMIT ?`,
101
- )
102
- .all(
103
- before,
104
- ...(session ? [`%${JSON.stringify(session).slice(1, -1)}%`] : []),
105
- ...(q ? [`%${q}%`] : []),
106
- limit,
107
- )
108
- .map(row);
109
- }
110
- // Still on her mind at `now`, ranked by how present each one is; a thought
111
- // let go of later still counts as open when looking back. A later note
112
- // that only repeats sources already used does not start this clock over.
113
- weighed({ now = Date.now(), session = "" } = {}) {
114
- const lived = this.mind.days.lived(now);
115
- const notes = this.db
116
- .prepare(
117
- "SELECT * FROM mind_thoughts WHERE hidden=0 AND created<=? AND (status='open' OR resolved_at>?) ORDER BY created DESC LIMIT 400",
118
- )
119
- .all(now, now)
120
- .map(row)
121
- .filter((t) => !session || t.sessions.includes(session));
122
- const clock = sourceClock(notes, (sources) =>
123
- evidenceRoots(this.db, sources, now),
124
- );
125
- return notes
126
- .map((t) => {
127
- const earned = clock.get(t.id) ?? t.created;
128
- // A revisit brings back the note that actually lived the experience.
129
- // Rewriting it later and setting another time does not.
130
- const seen = earned < t.created ? { ...t, revisit_at: null } : t;
131
- return {
132
- ...t,
133
- salience: thoughtSalience({ ...seen, created: earned }, lived, now),
134
- };
135
- })
136
- .sort((a, b) => b.salience - a.salience || b.created - a.created);
137
- }
138
- open({ now = Date.now(), limit = 12, session = "" } = {}) {
139
- return this.weighed({ now, session })
140
- .filter((t) => t.salience >= THOUGHT_FADED)
141
- .slice(0, limit);
142
- }
143
- // The studio card shows the newest note still in view. Salience still
144
- // decides which thoughts a conversation carries.
145
- latest(now = Date.now()) {
146
- return (
147
- this.open({ now, limit: 40 }).sort((a, b) => b.created - a.created)[0] ||
148
- null
149
- );
150
- }
151
- // Faded thoughts from this place that the conversation brings back.
152
- reminded({ now = Date.now(), cue, cues, session = "", limit = 1 } = {}) {
153
- const sets = cueList(cue, cues);
154
- if (!sets.length) return [];
155
- return this.weighed({ now, session })
156
- .filter((t) => t.salience < THOUGHT_FADED && anyTouches(t.content, sets))
157
- .slice(0, limit);
158
- }
159
- resolve(id, resolution = "", time = Date.now()) {
160
- return (
161
- this.db
162
- .prepare(
163
- "UPDATE mind_thoughts SET status='resolved',resolved_at=?,resolution=? WHERE id=? AND status='open'",
164
- )
165
- .run(time, text(resolution, 120), id).changes > 0
166
- );
167
- }
168
- update(id, { status, hidden }) {
169
- const current = this.get(id);
170
- if (!current) throw Error("手记不存在");
171
- if (status !== undefined && !["open", "resolved"].includes(status))
172
- throw Error("状态无效");
173
- if (hidden !== undefined && typeof hidden !== "boolean")
174
- throw Error("开关无效");
175
- const next = status ?? current.status;
176
- const resolved =
177
- next === current.status
178
- ? [current.resolved_at ?? null, current.resolution ?? ""]
179
- : next === "resolved"
180
- ? [Date.now(), "在工作台里放下"]
181
- : [null, ""];
182
- this.db
183
- .prepare(
184
- "UPDATE mind_thoughts SET status=?,hidden=?,resolved_at=?,resolution=? WHERE id=?",
185
- )
186
- .run(
187
- next,
188
- hidden === undefined ? current.hidden : +hidden,
189
- ...resolved,
190
- id,
191
- );
192
- }
193
- remove(id) {
194
- if (
195
- this.db.prepare("SELECT 1 FROM mind_thoughts WHERE parent_id=?").get(id)
196
- )
197
- throw Error("这条手记有后续修正,请隐藏以保留轨迹");
198
- this.db.prepare("DELETE FROM mind_thoughts WHERE id=?").run(id);
199
- }
200
- setOutreach(id, status, reason = "") {
201
- this.db
202
- .prepare(
203
- "UPDATE mind_thoughts SET outreach_status=?,outreach_wait_reason=?,outreach_retry_at=NULL WHERE id=?",
204
- )
205
- .run(status, text(reason, 300), id);
206
- }
207
- planOutreach(id, { session, words, reason, time }) {
208
- const current = this.get(id);
209
- if (!current || current.outreach_status !== "none") return false;
210
- const draft = words
211
- .filter((s) => typeof s === "string" && s.trim())
212
- .slice(0, 3)
213
- .map((s) => text(s, 180));
214
- if (!session || !draft.length) return false;
215
- this.db
216
- .prepare(
217
- "UPDATE mind_thoughts SET sessions=?,outreach=?,outreach_session=?,outreach_status='planned',outreach_at=?,outreach_reason=?,outreach_draft=? WHERE id=? AND outreach_status='none'",
218
- )
219
- .run(
220
- // An independent thought is not owned by its first audience. Keep its
221
- // origin scope; outreach_session records where she chose to share it.
222
- JSON.stringify(
223
- current.kind === "expression"
224
- ? current.sessions
225
- : [...new Set([...current.sessions, session])],
226
- ),
227
- draft.join("\n"),
228
- session,
229
- time,
230
- text(reason, 300),
231
- JSON.stringify(draft),
232
- id,
233
- );
234
- return true;
235
- }
236
- deferOutreach(id, reason, retryAt) {
237
- this.db
238
- .prepare(
239
- "UPDATE mind_thoughts SET outreach_status='planned',outreach_wait_reason=?,outreach_retry_at=? WHERE id=?",
240
- )
241
- .run(text(reason, 300), retryAt, id);
242
- }
243
- dueOutreach(now = Date.now()) {
244
- return this.db
245
- .prepare(
246
- "SELECT * FROM mind_thoughts WHERE outreach_status='planned' AND hidden=0 AND status='open' AND outreach!='' AND COALESCE(outreach_at,revisit_at,created)<=? AND COALESCE(outreach_retry_at,0)<=? ORDER BY importance DESC, created LIMIT 5",
247
- )
248
- .all(now, now)
249
- .map(row);
250
- }
251
- }
252
-
253
- // The first time a source was lived. A later note that only repeats those
254
- // sources keeps that time, so rewriting it does not make it newly present.
255
- function sourceClock(notes, roots) {
256
- const earned = new Map();
257
- const clock = new Map();
258
- for (const note of [...notes].sort((a, b) => a.created - b.created)) {
259
- const resolved = roots(note.sources);
260
- const sources = resolved.length ? resolved : [`t:${note.id}`];
261
- const fresh = sources.some((source) => !earned.has(source));
262
- if (fresh) {
263
- clock.set(note.id, note.created);
264
- for (const source of sources)
265
- if (!earned.has(source)) earned.set(source, note.created);
266
- } else {
267
- clock.set(
268
- note.id,
269
- Math.min(...sources.map((source) => earned.get(source))),
270
- );
271
- }
272
- }
273
- return clock;
274
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidenceRoots } from "./evidence.js";
3
+ import {
4
+ THOUGHT_FADED,
5
+ anyTouches,
6
+ cueList,
7
+ thoughtSalience,
8
+ } from "./salience.js";
9
+ import { HOUR, clamp, evidence, parse, text } from "./util.js";
10
+
11
+ export const THOUGHT_KINDS = {
12
+ reflection: "后来想到",
13
+ revision: "重新理解",
14
+ unfinished: "仍放在心上",
15
+ reconnection: "久别想起",
16
+ expression: "自己的念头",
17
+ };
18
+
19
+ function row(value) {
20
+ return value
21
+ ? {
22
+ ...value,
23
+ sessions: parse(value.sessions, []),
24
+ sources: parse(value.sources, []),
25
+ outreach_draft: parse(value.outreach_draft, []),
26
+ }
27
+ : null;
28
+ }
29
+
30
+ // Her journal between conversations. A later understanding is a new entry
31
+ // pointing at the old one; the old words stay as they were.
32
+ export class Thoughts {
33
+ constructor(mind) {
34
+ this.mind = mind;
35
+ this.db = mind.db;
36
+ }
37
+ add({
38
+ kind,
39
+ content,
40
+ sessions = [],
41
+ sources = [],
42
+ parentId = null,
43
+ importance = 0.5,
44
+ revisitHours = 0,
45
+ outreach = "",
46
+ outreachSession = null,
47
+ outreachAfterHours,
48
+ outreachReason = "",
49
+ runId = null,
50
+ time = Date.now(),
51
+ }) {
52
+ const id = randomUUID();
53
+ const hours = Number(revisitHours);
54
+ this.db
55
+ .prepare(
56
+ "INSERT INTO mind_thoughts(id,created,kind,content,sessions,sources,parent_id,importance,revisit_at,status,hidden,outreach,outreach_session,outreach_status,run_id,outreach_at,outreach_reason) VALUES (?,?,?,?,?,?,?,?,?,'open',0,?,?,?,?,?,?)",
57
+ )
58
+ .run(
59
+ id,
60
+ time,
61
+ THOUGHT_KINDS[kind] ? kind : "reflection",
62
+ text(content, 600),
63
+ JSON.stringify([...new Set(sessions)].slice(0, 12)),
64
+ JSON.stringify(evidence(sources)),
65
+ parentId || null,
66
+ clamp(importance),
67
+ Number.isFinite(hours) && hours > 0
68
+ ? time + Math.min(8760, Math.max(1, hours)) * HOUR
69
+ : null,
70
+ text(outreach, 120),
71
+ outreach ? outreachSession : null,
72
+ outreach ? "planned" : "none",
73
+ runId,
74
+ outreach
75
+ ? time +
76
+ Math.min(
77
+ 8760,
78
+ Math.max(0, Number(outreachAfterHours ?? revisitHours) || 0),
79
+ ) *
80
+ HOUR
81
+ : null,
82
+ text(outreachReason, 300),
83
+ );
84
+ return id;
85
+ }
86
+ get(id) {
87
+ return row(
88
+ this.db.prepare("SELECT * FROM mind_thoughts WHERE id=?").get(id),
89
+ );
90
+ }
91
+ list({
92
+ before = Number.MAX_SAFE_INTEGER,
93
+ session = "",
94
+ q = "",
95
+ limit = 30,
96
+ hidden = true,
97
+ } = {}) {
98
+ return this.db
99
+ .prepare(
100
+ `SELECT * FROM mind_thoughts WHERE created<?${hidden ? "" : " AND hidden=0"}${session ? " AND sessions LIKE ?" : ""}${q ? " AND content LIKE ?" : ""} ORDER BY created DESC LIMIT ?`,
101
+ )
102
+ .all(
103
+ before,
104
+ ...(session ? [`%${JSON.stringify(session).slice(1, -1)}%`] : []),
105
+ ...(q ? [`%${q}%`] : []),
106
+ limit,
107
+ )
108
+ .map(row);
109
+ }
110
+ // Still on her mind at `now`, ranked by how present each one is; a thought
111
+ // let go of later still counts as open when looking back. A later note
112
+ // that only repeats sources already used does not start this clock over.
113
+ weighed({ now = Date.now(), session = "" } = {}) {
114
+ const lived = this.mind.days.lived(now);
115
+ const notes = this.db
116
+ .prepare(
117
+ "SELECT * FROM mind_thoughts WHERE hidden=0 AND created<=? AND (status='open' OR resolved_at>?) ORDER BY created DESC LIMIT 400",
118
+ )
119
+ .all(now, now)
120
+ .map(row)
121
+ .filter((t) => !session || t.sessions.includes(session));
122
+ const clock = sourceClock(notes, (sources) =>
123
+ evidenceRoots(this.db, sources, now),
124
+ );
125
+ return notes
126
+ .map((t) => {
127
+ const earned = clock.get(t.id) ?? t.created;
128
+ // A revisit brings back the note that actually lived the experience.
129
+ // Rewriting it later and setting another time does not.
130
+ const seen = earned < t.created ? { ...t, revisit_at: null } : t;
131
+ return {
132
+ ...t,
133
+ salience: thoughtSalience({ ...seen, created: earned }, lived, now),
134
+ };
135
+ })
136
+ .sort((a, b) => b.salience - a.salience || b.created - a.created);
137
+ }
138
+ open({ now = Date.now(), limit = 12, session = "" } = {}) {
139
+ return this.weighed({ now, session })
140
+ .filter((t) => t.salience >= THOUGHT_FADED)
141
+ .slice(0, limit);
142
+ }
143
+ // The studio card shows the newest note still in view. Salience still
144
+ // decides which thoughts a conversation carries.
145
+ latest(now = Date.now()) {
146
+ return (
147
+ this.open({ now, limit: 40 }).sort((a, b) => b.created - a.created)[0] ||
148
+ null
149
+ );
150
+ }
151
+ // Faded thoughts from this place that the conversation brings back.
152
+ reminded({ now = Date.now(), cue, cues, session = "", limit = 1 } = {}) {
153
+ const sets = cueList(cue, cues);
154
+ if (!sets.length) return [];
155
+ return this.weighed({ now, session })
156
+ .filter((t) => t.salience < THOUGHT_FADED && anyTouches(t.content, sets))
157
+ .slice(0, limit);
158
+ }
159
+ resolve(id, resolution = "", time = Date.now()) {
160
+ return (
161
+ this.db
162
+ .prepare(
163
+ "UPDATE mind_thoughts SET status='resolved',resolved_at=?,resolution=? WHERE id=? AND status='open'",
164
+ )
165
+ .run(time, text(resolution, 120), id).changes > 0
166
+ );
167
+ }
168
+ update(id, { status, hidden }) {
169
+ const current = this.get(id);
170
+ if (!current) throw Error("手记不存在");
171
+ if (status !== undefined && !["open", "resolved"].includes(status))
172
+ throw Error("状态无效");
173
+ if (hidden !== undefined && typeof hidden !== "boolean")
174
+ throw Error("开关无效");
175
+ const next = status ?? current.status;
176
+ const resolved =
177
+ next === current.status
178
+ ? [current.resolved_at ?? null, current.resolution ?? ""]
179
+ : next === "resolved"
180
+ ? [Date.now(), "在工作台里放下"]
181
+ : [null, ""];
182
+ this.db
183
+ .prepare(
184
+ "UPDATE mind_thoughts SET status=?,hidden=?,resolved_at=?,resolution=? WHERE id=?",
185
+ )
186
+ .run(
187
+ next,
188
+ hidden === undefined ? current.hidden : +hidden,
189
+ ...resolved,
190
+ id,
191
+ );
192
+ }
193
+ remove(id) {
194
+ if (
195
+ this.db.prepare("SELECT 1 FROM mind_thoughts WHERE parent_id=?").get(id)
196
+ )
197
+ throw Error("这条手记有后续修正,请隐藏以保留轨迹");
198
+ this.db.prepare("DELETE FROM mind_thoughts WHERE id=?").run(id);
199
+ }
200
+ setOutreach(id, status, reason = "") {
201
+ this.db
202
+ .prepare(
203
+ "UPDATE mind_thoughts SET outreach_status=?,outreach_wait_reason=?,outreach_retry_at=NULL WHERE id=?",
204
+ )
205
+ .run(status, text(reason, 300), id);
206
+ }
207
+ planOutreach(id, { session, words, reason, time }) {
208
+ const current = this.get(id);
209
+ if (!current || current.outreach_status !== "none") return false;
210
+ const draft = words
211
+ .filter((s) => typeof s === "string" && s.trim())
212
+ .slice(0, 3)
213
+ .map((s) => text(s, 180));
214
+ if (!session || !draft.length) return false;
215
+ this.db
216
+ .prepare(
217
+ "UPDATE mind_thoughts SET sessions=?,outreach=?,outreach_session=?,outreach_status='planned',outreach_at=?,outreach_reason=?,outreach_draft=? WHERE id=? AND outreach_status='none'",
218
+ )
219
+ .run(
220
+ // An independent thought is not owned by its first audience. Keep its
221
+ // origin scope; outreach_session records where she chose to share it.
222
+ JSON.stringify(
223
+ current.kind === "expression"
224
+ ? current.sessions
225
+ : [...new Set([...current.sessions, session])],
226
+ ),
227
+ draft.join("\n"),
228
+ session,
229
+ time,
230
+ text(reason, 300),
231
+ JSON.stringify(draft),
232
+ id,
233
+ );
234
+ return true;
235
+ }
236
+ deferOutreach(id, reason, retryAt) {
237
+ this.db
238
+ .prepare(
239
+ "UPDATE mind_thoughts SET outreach_status='planned',outreach_wait_reason=?,outreach_retry_at=? WHERE id=?",
240
+ )
241
+ .run(text(reason, 300), retryAt, id);
242
+ }
243
+ dueOutreach(now = Date.now()) {
244
+ return this.db
245
+ .prepare(
246
+ "SELECT * FROM mind_thoughts WHERE outreach_status='planned' AND hidden=0 AND status='open' AND outreach!='' AND COALESCE(outreach_at,revisit_at,created)<=? AND COALESCE(outreach_retry_at,0)<=? ORDER BY importance DESC, created LIMIT 5",
247
+ )
248
+ .all(now, now)
249
+ .map(row);
250
+ }
251
+ }
252
+
253
+ // The first time a source was lived. A later note that only repeats those
254
+ // sources keeps that time, so rewriting it does not make it newly present.
255
+ function sourceClock(notes, roots) {
256
+ const earned = new Map();
257
+ const clock = new Map();
258
+ for (const note of [...notes].sort((a, b) => a.created - b.created)) {
259
+ const resolved = roots(note.sources);
260
+ const sources = resolved.length ? resolved : [`t:${note.id}`];
261
+ const fresh = sources.some((source) => !earned.has(source));
262
+ if (fresh) {
263
+ clock.set(note.id, note.created);
264
+ for (const source of sources)
265
+ if (!earned.has(source)) earned.set(source, note.created);
266
+ } else {
267
+ clock.set(
268
+ note.id,
269
+ Math.min(...sources.map((source) => earned.get(source))),
270
+ );
271
+ }
272
+ }
273
+ return clock;
274
+ }