luckytri 1.0.6 → 1.0.7

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 (155) hide show
  1. package/LICENSE +21 -21
  2. package/docs/en/CHANGELOG.md +25 -16
  3. package/docs/en/chat-style.md +28 -9
  4. package/docs/en/guide.md +5 -5
  5. package/docs/en/her-life.md +2 -2
  6. package/docs/en/webui.md +1 -1
  7. package/docs/zh/CHANGELOG.md +25 -16
  8. package/docs/zh/chat-style.md +30 -9
  9. package/docs/zh/guide.md +5 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +1 -1
  13. package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
  14. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  15. package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
  16. package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
  17. package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
  18. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  19. package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
  20. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  21. package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
  22. package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
  23. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  24. package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
  25. package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
  26. package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
  27. package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
  28. package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
  29. package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
  30. package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
  31. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  32. package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
  33. package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
  34. package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
  35. package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
  36. package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
  37. package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
  38. package/public/app/index.html +5 -6
  39. package/public/guide.en.html +3 -3
  40. package/public/guide.html +3 -3
  41. package/scripts/import-data.js +128 -128
  42. package/scripts/proxy-config.js +167 -167
  43. package/scripts/update-instance.js +201 -201
  44. package/server/channels/adapters.js +27 -27
  45. package/server/channels/onebot/adapter.js +288 -288
  46. package/server/channels/qqbot/adapter.js +253 -253
  47. package/server/channels/qqbot/api.js +199 -199
  48. package/server/channels/qqbot/credentials.js +11 -11
  49. package/server/channels/qqbot/gateway.js +274 -274
  50. package/server/channels/qqbot/index.js +389 -389
  51. package/server/channels/segments.js +40 -40
  52. package/server/channels/session-key.js +153 -153
  53. package/server/channels/state.js +42 -42
  54. package/server/core/api.js +564 -564
  55. package/server/core/context-builder.js +288 -288
  56. package/server/core/context-compactor.js +454 -454
  57. package/server/core/conversation-cues.js +365 -325
  58. package/server/core/conversation-grounding.js +178 -0
  59. package/server/core/conversation-manager.js +87 -87
  60. package/server/core/dialogue-context.js +52 -0
  61. package/server/core/feedback.js +8 -8
  62. package/server/core/group-style.js +72 -72
  63. package/server/core/initiative-context.js +66 -66
  64. package/server/core/input-budget.js +32 -32
  65. package/server/core/local-demo.js +98 -98
  66. package/server/core/message-manager.js +118 -118
  67. package/server/core/message-scheduler.js +78 -78
  68. package/server/core/model-json.js +190 -190
  69. package/server/core/model-presets.js +900 -900
  70. package/server/core/name-call.js +29 -29
  71. package/server/core/network.js +157 -157
  72. package/server/core/person-name.js +22 -22
  73. package/server/core/persona-manager.js +324 -269
  74. package/server/core/reply-delivery.js +472 -404
  75. package/server/core/reply-target-resolver.js +50 -50
  76. package/server/core/response-generator.js +58 -50
  77. package/server/core/response-validator.js +30 -27
  78. package/server/core/retired-prompts.js +51 -51
  79. package/server/core/sessions.js +109 -109
  80. package/server/core/speaker-names.js +92 -92
  81. package/server/core/topic-tracker.js +65 -65
  82. package/server/core/turn.js +42 -8
  83. package/server/http.js +18 -18
  84. package/server/i18n/decisions.js +209 -209
  85. package/server/i18n/index.js +144 -144
  86. package/server/i18n/labels.js +0 -5
  87. package/server/i18n/models.js +111 -111
  88. package/server/knowledge/api.js +342 -342
  89. package/server/knowledge/embedding-profile.js +114 -114
  90. package/server/knowledge/index.js +13 -13
  91. package/server/knowledge/retrieval.js +93 -93
  92. package/server/knowledge/schema.js +158 -158
  93. package/server/knowledge/vector-index.js +90 -90
  94. package/server/mind/affect.js +265 -265
  95. package/server/mind/attention.js +200 -200
  96. package/server/mind/bonds.js +565 -565
  97. package/server/mind/clock.js +83 -83
  98. package/server/mind/context-selection.js +16 -4
  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 +68 -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 -330
  108. package/server/mind/life/growth.js +261 -261
  109. package/server/mind/life/presence.js +305 -305
  110. package/server/mind/meetings.js +1069 -1069
  111. package/server/mind/memory-policy.js +48 -48
  112. package/server/mind/memory.js +888 -888
  113. package/server/mind/nature.js +358 -358
  114. package/server/mind/novelty.js +19 -19
  115. package/server/mind/periods.js +118 -118
  116. package/server/mind/reading.js +120 -120
  117. package/server/mind/relationship-context.js +71 -71
  118. package/server/mind/relationships.js +390 -390
  119. package/server/mind/salience.js +201 -201
  120. package/server/mind/schema.js +567 -567
  121. package/server/mind/self.js +496 -484
  122. package/server/mind/thoughts.js +274 -274
  123. package/server/mind/time/agenda.js +242 -242
  124. package/server/mind/time/api.js +200 -200
  125. package/server/mind/time/attention.js +34 -34
  126. package/server/mind/time/delivery-links.js +130 -130
  127. package/server/mind/time/games.js +7 -3
  128. package/server/mind/time/index.js +3 -0
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +35 -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 +438 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +43 -6
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-manager.js +140 -140
  141. package/server/storage/data-package.js +298 -298
  142. package/server/storage/data-transfer-api.js +50 -50
  143. package/server/storage/data-transfer-worker.js +18 -18
  144. package/server/storage/data-transfer.js +416 -416
  145. package/server/studio/display-names.js +139 -139
  146. package/server/studio/management.js +102 -102
  147. package/server/studio/updates.js +144 -144
  148. package/server/version.js +10 -10
  149. package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
  150. package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
  151. package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
  152. package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
  153. package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
  154. package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
  155. package/public/app/assets/TimePage-C-guwPk2.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
+ }