luckytri 1.0.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 (235) hide show
  1. package/.github/SECURITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +250 -0
  4. package/README.md +250 -0
  5. package/bin/luckytri.js +107 -0
  6. package/docs/brand/banner.png +0 -0
  7. package/docs/brand/palette.png +0 -0
  8. package/docs/brand/portrait.png +0 -0
  9. package/docs/en/CHANGELOG.md +113 -0
  10. package/docs/en/architecture.md +152 -0
  11. package/docs/en/chat-style.md +62 -0
  12. package/docs/en/connect.md +63 -0
  13. package/docs/en/guide.md +216 -0
  14. package/docs/en/her-life.md +189 -0
  15. package/docs/en/npm-release.md +25 -0
  16. package/docs/en/plugins.md +82 -0
  17. package/docs/en/recovery.md +28 -0
  18. package/docs/en/time.md +71 -0
  19. package/docs/en/webui.md +73 -0
  20. package/docs/zh/CHANGELOG.md +113 -0
  21. package/docs/zh/architecture.md +152 -0
  22. package/docs/zh/chat-style.md +62 -0
  23. package/docs/zh/connect.md +63 -0
  24. package/docs/zh/guide.md +216 -0
  25. package/docs/zh/her-life.md +189 -0
  26. package/docs/zh/npm-release.md +33 -0
  27. package/docs/zh/plugins.md +82 -0
  28. package/docs/zh/recovery.md +28 -0
  29. package/docs/zh/time.md +71 -0
  30. package/docs/zh/webui.md +73 -0
  31. package/package.json +108 -0
  32. package/plugins/sdk/index.d.ts +155 -0
  33. package/plugins/sdk/testing.js +58 -0
  34. package/plugins/weather/index.js +82 -0
  35. package/plugins/weather/luckytri-plugin.json +29 -0
  36. package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
  37. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  38. package/public/app/assets/Empty-CpP5kQHH.js +2 -0
  39. package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
  40. package/public/app/assets/Field-D4gY-INU.js +1 -0
  41. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  42. package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
  43. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  44. package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
  45. package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
  46. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  47. package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
  48. package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
  49. package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
  50. package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
  51. package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
  52. package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
  53. package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
  54. package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
  55. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  56. package/public/app/assets/TimePage-Cw44inM4.js +3 -0
  57. package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
  58. package/public/app/assets/index-BheRW-1t.js +4 -0
  59. package/public/app/assets/index-YDz1ree1.css +1 -0
  60. package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
  61. package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
  62. package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
  63. package/public/app/assets/media-BzPDBm7e.js +1 -0
  64. package/public/app/index.html +27 -0
  65. package/public/guide.en.html +1053 -0
  66. package/public/guide.html +893 -0
  67. package/public/plugin-kit/v1/bridge.js +30 -0
  68. package/public/plugin-kit/v1/kit.css +14 -0
  69. package/public/tutorial.en.md +214 -0
  70. package/public/tutorial.md +214 -0
  71. package/scripts/backup.js +311 -0
  72. package/scripts/browser.js +41 -0
  73. package/scripts/launch.js +94 -0
  74. package/scripts/plugin.js +150 -0
  75. package/scripts/proxy-config.js +167 -0
  76. package/scripts/recover.js +26 -0
  77. package/scripts/runtime.js +25 -0
  78. package/scripts/service.js +47 -0
  79. package/scripts/setup.js +50 -0
  80. package/scripts/start-server.js +41 -0
  81. package/scripts/stop.js +36 -0
  82. package/server/app.js +96 -0
  83. package/server/channels/adapters.js +27 -0
  84. package/server/channels/contract.js +57 -0
  85. package/server/channels/hub.js +165 -0
  86. package/server/channels/index.js +25 -0
  87. package/server/channels/onebot/adapter.js +288 -0
  88. package/server/channels/onebot/index.js +336 -0
  89. package/server/channels/onebot/token.js +3 -0
  90. package/server/channels/qqbot/adapter.js +253 -0
  91. package/server/channels/qqbot/api.js +199 -0
  92. package/server/channels/qqbot/credentials.js +11 -0
  93. package/server/channels/qqbot/gateway.js +274 -0
  94. package/server/channels/qqbot/index.js +389 -0
  95. package/server/channels/segments.js +40 -0
  96. package/server/channels/session-key.js +153 -0
  97. package/server/channels/state.js +42 -0
  98. package/server/core/api.js +564 -0
  99. package/server/core/capabilities.js +208 -0
  100. package/server/core/context-builder.js +288 -0
  101. package/server/core/context-compactor.js +454 -0
  102. package/server/core/conversation-cues.js +325 -0
  103. package/server/core/conversation-manager.js +87 -0
  104. package/server/core/events.js +60 -0
  105. package/server/core/feedback.js +8 -0
  106. package/server/core/group-style.js +72 -0
  107. package/server/core/image-preprocessor.js +208 -0
  108. package/server/core/initiative-context.js +66 -0
  109. package/server/core/input-budget.js +32 -0
  110. package/server/core/local-demo.js +98 -0
  111. package/server/core/message-manager.js +118 -0
  112. package/server/core/message-scheduler.js +78 -0
  113. package/server/core/model-json.js +190 -0
  114. package/server/core/model-manager.js +1213 -0
  115. package/server/core/model-presets.js +900 -0
  116. package/server/core/name-call.js +29 -0
  117. package/server/core/net-guard.js +49 -0
  118. package/server/core/network.js +157 -0
  119. package/server/core/orchestrator.js +569 -0
  120. package/server/core/perceivers.js +91 -0
  121. package/server/core/person-name.js +22 -0
  122. package/server/core/persona-manager.js +243 -0
  123. package/server/core/reply-delivery.js +393 -0
  124. package/server/core/reply-target-resolver.js +50 -0
  125. package/server/core/repository.js +293 -0
  126. package/server/core/response-generator.js +50 -0
  127. package/server/core/response-validator.js +508 -0
  128. package/server/core/retired-prompts.js +51 -0
  129. package/server/core/sessions.js +109 -0
  130. package/server/core/speaker-names.js +92 -0
  131. package/server/core/topic-tracker.js +65 -0
  132. package/server/core/turn-processor.js +630 -0
  133. package/server/core/turn.js +236 -0
  134. package/server/core/vision-manager.js +633 -0
  135. package/server/http.js +18 -0
  136. package/server/i18n/decisions.js +206 -0
  137. package/server/i18n/errors.js +497 -0
  138. package/server/i18n/index.js +144 -0
  139. package/server/i18n/labels.js +653 -0
  140. package/server/i18n/models.js +111 -0
  141. package/server/index.js +102 -0
  142. package/server/knowledge/api.js +342 -0
  143. package/server/knowledge/embedding-profile.js +114 -0
  144. package/server/knowledge/index.js +13 -0
  145. package/server/knowledge/manager.js +386 -0
  146. package/server/knowledge/retrieval.js +93 -0
  147. package/server/knowledge/schema.js +154 -0
  148. package/server/knowledge/vector-index.js +90 -0
  149. package/server/mind/affect.js +265 -0
  150. package/server/mind/anticipations.js +507 -0
  151. package/server/mind/api.js +703 -0
  152. package/server/mind/attention.js +200 -0
  153. package/server/mind/bonds.js +558 -0
  154. package/server/mind/budget.js +152 -0
  155. package/server/mind/clock.js +83 -0
  156. package/server/mind/context-selection.js +61 -0
  157. package/server/mind/continuity.js +562 -0
  158. package/server/mind/conversation-origin.js +175 -0
  159. package/server/mind/day-planner.js +324 -0
  160. package/server/mind/days.js +210 -0
  161. package/server/mind/evidence-read.js +45 -0
  162. package/server/mind/evidence.js +148 -0
  163. package/server/mind/faces.js +138 -0
  164. package/server/mind/guard.js +60 -0
  165. package/server/mind/index.js +445 -0
  166. package/server/mind/initiative.js +333 -0
  167. package/server/mind/life/activities.js +21 -0
  168. package/server/mind/life/context.js +309 -0
  169. package/server/mind/life/diary.js +786 -0
  170. package/server/mind/life/growth.js +252 -0
  171. package/server/mind/life/index.js +1049 -0
  172. package/server/mind/life/presence.js +305 -0
  173. package/server/mind/meetings.js +1069 -0
  174. package/server/mind/memory-policy.js +48 -0
  175. package/server/mind/memory.js +888 -0
  176. package/server/mind/nature.js +262 -0
  177. package/server/mind/novelty.js +19 -0
  178. package/server/mind/observations.js +84 -0
  179. package/server/mind/own-day.js +324 -0
  180. package/server/mind/own-voice.js +318 -0
  181. package/server/mind/periods.js +118 -0
  182. package/server/mind/reading.js +120 -0
  183. package/server/mind/salience.js +201 -0
  184. package/server/mind/schema.js +562 -0
  185. package/server/mind/self.js +484 -0
  186. package/server/mind/senses.js +53 -0
  187. package/server/mind/thoughts.js +274 -0
  188. package/server/mind/time/activity-clock.js +196 -0
  189. package/server/mind/time/agenda.js +242 -0
  190. package/server/mind/time/api.js +200 -0
  191. package/server/mind/time/attention.js +34 -0
  192. package/server/mind/time/availability.js +69 -0
  193. package/server/mind/time/commitment-review.js +198 -0
  194. package/server/mind/time/delivery-links.js +130 -0
  195. package/server/mind/time/executor.js +512 -0
  196. package/server/mind/time/experiences.js +128 -0
  197. package/server/mind/time/games.js +562 -0
  198. package/server/mind/time/index.js +739 -0
  199. package/server/mind/time/intent.js +166 -0
  200. package/server/mind/time/kinds.js +171 -0
  201. package/server/mind/time/model-material.js +77 -0
  202. package/server/mind/time/plugin-activity.js +207 -0
  203. package/server/mind/time/presentation.js +51 -0
  204. package/server/mind/time/schedule.js +83 -0
  205. package/server/mind/time/schema.js +84 -0
  206. package/server/mind/time/search.js +383 -0
  207. package/server/mind/time/sharing.js +472 -0
  208. package/server/mind/time/task-links.js +311 -0
  209. package/server/mind/time/tasks.js +587 -0
  210. package/server/mind/time/works.js +432 -0
  211. package/server/mind/traits.js +191 -0
  212. package/server/mind/util.js +110 -0
  213. package/server/mind/view.js +273 -0
  214. package/server/paths.js +102 -0
  215. package/server/plugins/api.js +204 -0
  216. package/server/plugins/host.js +967 -0
  217. package/server/plugins/install.js +295 -0
  218. package/server/plugins/manifest.js +201 -0
  219. package/server/plugins/permissions.js +42 -0
  220. package/server/plugins/runner/guard.mjs +36 -0
  221. package/server/plugins/runner/worker.mjs +250 -0
  222. package/server/plugins/schema.js +91 -0
  223. package/server/plugins/zip.js +112 -0
  224. package/server/startup-lock.js +118 -0
  225. package/server/storage/archive.js +241 -0
  226. package/server/storage/backup-cleanup-worker.js +8 -0
  227. package/server/storage/backup-cleanup.js +414 -0
  228. package/server/storage/backup-lock.js +76 -0
  229. package/server/storage/backup-scheduler.js +131 -0
  230. package/server/storage/store.js +199 -0
  231. package/server/studio/display-names.js +138 -0
  232. package/server/studio/management.js +102 -0
  233. package/server/studio/readiness.js +190 -0
  234. package/server/studio/settings.js +236 -0
  235. package/server/version.js +10 -0
@@ -0,0 +1,1049 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidenceRoots } from "../evidence.js";
3
+ import { localClock } from "../../core/conversation-cues.js";
4
+ import { replyPrompt, prompts } from "../../core/persona-manager.js";
5
+ import { estimateTokens } from "../../core/model-manager.js";
6
+ import { elapsedLabel } from "../clock.js";
7
+ import { isPrivateSession } from "../memory.js";
8
+ import { lifeDayKey, rhythmPhase } from "../nature.js";
9
+ import { THOUGHT_KINDS } from "../thoughts.js";
10
+ import { Initiative } from "../initiative.js";
11
+ import { OwnVoice } from "../own-voice.js";
12
+ import {
13
+ DAY,
14
+ HOUR,
15
+ clamp,
16
+ evidence,
17
+ hasCredential,
18
+ parse,
19
+ similar,
20
+ text,
21
+ } from "../util.js";
22
+ export const LIFE_DEFAULTS = {
23
+ solitude: true,
24
+ proactive: true,
25
+ diary: true,
26
+ reading: true,
27
+ night: true,
28
+ idleMinutes: 20,
29
+ intervalMinutes: 90,
30
+ minMessages: 6,
31
+ chapterDays: 7,
32
+ proactiveIntervalHours: 0,
33
+ initiativeIntervalMinutes: 30,
34
+ modelId: "",
35
+ timeZone: "Asia/Shanghai",
36
+ };
37
+ const MINUTE = 60000;
38
+ const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
39
+ const SOLITUDE_INPUT_CAP = 24000;
40
+ import { LifeContext } from "./context.js";
41
+ import { LifeGrowth } from "./growth.js";
42
+ import { LifeDiary } from "./diary.js";
43
+ import { LifePresence } from "./presence.js";
44
+ import { LifeActivities } from "./activities.js";
45
+ import { OwnDay } from "../own-day.js";
46
+ import { DayPlanner } from "../day-planner.js";
47
+ import { withActivities } from "../time/kinds.js";
48
+
49
+ export class Life {
50
+ constructor(
51
+ chat,
52
+ {
53
+ now = chat.now || Date.now,
54
+ online = () => false,
55
+ // Whether the channel lets her start a conversation in a room right now.
56
+ canReach = () => true,
57
+ } = {},
58
+ ) {
59
+ this.chat = chat;
60
+ this.mind = chat.mind;
61
+ this.repo = chat.repo;
62
+ this.db = chat.repo.db;
63
+ this.now = now;
64
+ this.mind.time.now = now;
65
+ this.mind.time.search.bind(this);
66
+ this.online = online;
67
+ this.canReach = canReach;
68
+ this.busy = false;
69
+ this.closed = false;
70
+ this.initiative = new Initiative(this);
71
+ this.ownVoice = new OwnVoice(this);
72
+ this.context = new LifeContext(this);
73
+ this.growth = new LifeGrowth(this);
74
+ this.journal = new LifeDiary(this);
75
+ this.presence = new LifePresence(this);
76
+ this.activities = new LifeActivities(this);
77
+ this.ownDay = new OwnDay(this);
78
+ this.planner = new DayPlanner(this);
79
+ }
80
+ settings() {
81
+ return { ...LIFE_DEFAULTS, ...this.repo.config("life", {}) };
82
+ }
83
+ save(value) {
84
+ const next = { ...this.settings(), ...value };
85
+ for (const key of ["solitude", "proactive", "diary", "reading", "night"])
86
+ if (typeof next[key] !== "boolean") throw Error("开关无效");
87
+ localClock(this.now(), next.timeZone);
88
+ for (const [key, min, max] of [
89
+ ["idleMinutes", 0, 1440],
90
+ ["intervalMinutes", 10, 10080],
91
+ ["minMessages", 0, 10000],
92
+ ["chapterDays", 1, 365],
93
+ ["initiativeIntervalMinutes", 1, 10080],
94
+ ])
95
+ if (!Number.isInteger(next[key]) || next[key] < min || next[key] > max)
96
+ throw Error(`${key} 超出范围`);
97
+ if (
98
+ !Number.isFinite(next.proactiveIntervalHours) ||
99
+ next.proactiveIntervalHours < 0 ||
100
+ next.proactiveIntervalHours > 8760
101
+ )
102
+ throw Error("proactiveIntervalHours 超出范围");
103
+ next.modelId = "";
104
+ this.repo.saveConfig(
105
+ "life",
106
+ Object.fromEntries(Object.keys(LIFE_DEFAULTS).map((k) => [k, next[k]])),
107
+ );
108
+ return this.settings();
109
+ }
110
+ start() {
111
+ if (this.timer) return;
112
+ this.timer = setInterval(() => this.tick().catch(() => {}), MINUTE);
113
+ this.timer.unref();
114
+ }
115
+ close() {
116
+ this.closed = true;
117
+ clearInterval(this.timer);
118
+ }
119
+ phase(now = this.now()) {
120
+ return rhythmPhase(
121
+ this.mind.nature.current(now),
122
+ now,
123
+ this.mind.timeZone(),
124
+ );
125
+ }
126
+ // A day of her life starts when she wakes, not at midnight.
127
+ lifeDay(now = this.now()) {
128
+ return lifeDayKey(this.mind.nature.current(now), now, this.mind.timeZone());
129
+ }
130
+ living({ memory = true } = {}) {
131
+ return this.db
132
+ .prepare(
133
+ "SELECT id,name,kind FROM sessions WHERE enabled=1 AND archived=0",
134
+ )
135
+ .all()
136
+ .filter(
137
+ (s) =>
138
+ this.chat.enabled(s.id, { simulated: false }) &&
139
+ (!memory || this.chat.policy(s.id).memory !== false),
140
+ );
141
+ }
142
+ profile() {
143
+ return this.chat.models.profile();
144
+ }
145
+ run(kind, reason, watermark = null) {
146
+ const id = randomUUID();
147
+ this.db
148
+ .prepare(
149
+ "INSERT INTO mind_runs(id,kind,started,status,reason,watermark) VALUES (?,?,?,'running',?,?)",
150
+ )
151
+ .run(id, kind, this.now(), reason, watermark);
152
+ this.repo.store.revision++;
153
+ return id;
154
+ }
155
+ end(id, status, reason, trace, summary = null) {
156
+ const tokens = (trace?.calls || []).reduce(
157
+ (n, c) =>
158
+ n +
159
+ (c.tokens
160
+ ? Number(c.tokens.input || 0) + Number(c.tokens.output || 0)
161
+ : Number(c.usage?.total_tokens) || 0),
162
+ 0,
163
+ );
164
+ this.db
165
+ .prepare(
166
+ "UPDATE mind_runs SET finished=?,status=?,reason=?,tokens=?,model=?,summary=? WHERE id=?",
167
+ )
168
+ .run(
169
+ this.now(),
170
+ status,
171
+ String(reason).slice(0, 300),
172
+ tokens,
173
+ trace?.calls?.[0]?.model?.model || null,
174
+ summary ? JSON.stringify(summary) : null,
175
+ id,
176
+ );
177
+ this.db
178
+ .prepare(
179
+ "DELETE FROM mind_runs WHERE started<? AND id NOT IN (SELECT id FROM mind_runs ORDER BY started DESC LIMIT 400)",
180
+ )
181
+ .run(this.now() - 30 * DAY);
182
+ this.repo.store.revision++;
183
+ }
184
+ runs(limit = 60) {
185
+ return this.db
186
+ .prepare(
187
+ "SELECT * FROM mind_runs WHERE NOT (kind='activity' AND status IN ('reading','engaged')) ORDER BY started DESC LIMIT ?",
188
+ )
189
+ .all(limit)
190
+ .map((r) => ({ ...r, summary: parse(r.summary, null) }));
191
+ }
192
+ async tick() {
193
+ if (this.closed || this.busy)
194
+ return { status: "skipped", reason: "已有后台任务或服务已停止" };
195
+ const now = this.now();
196
+ this.mind.time.tasks.sync(now);
197
+ this.mind.time.tick(now);
198
+ this.mind.days.rollup(now);
199
+ const phase = this.phase(now).key;
200
+ if (phase !== "asleep") {
201
+ const woke = await this.wake(now);
202
+ if (woke) return woke;
203
+ }
204
+ const due = this.diaryDue(now);
205
+ if (this.mind.time.primary() && this.mind.time.attention.activityBusy)
206
+ return { status: "doing", reason: "正在推进自己的活动步骤" };
207
+ if (this.mind.time.primary() && (due || this.nightDue(now)))
208
+ this.mind.time.tasks.control(
209
+ this.mind.time.primary().id,
210
+ {
211
+ action: "pause",
212
+ readyAt: now + this.mind.time.settings().breakMinutes * 60000,
213
+ reason: "先写日记或整理今天,保留续接位置",
214
+ },
215
+ now,
216
+ );
217
+ if (due) return this.review(due);
218
+ const night = this.nightDue(now);
219
+ if (night)
220
+ return night.kind === "memory"
221
+ ? this.rememberAtNight(night.session, night.day, now)
222
+ : this.reviewPeriod(now);
223
+ const arrangement = await this.planner.run(now);
224
+ if (arrangement) return arrangement;
225
+ const care = await this.ownDay.run(now);
226
+ if (care) return care;
227
+ const activity = await this.activities.run(now);
228
+ if (activity) return activity;
229
+ if (this.mind.time.primary())
230
+ return {
231
+ status: "doing",
232
+ reason: "自己的活动在继续,普通交流可以伴随进行",
233
+ };
234
+ const outreach = await this.reachOut(now);
235
+ if (outreach) return outreach;
236
+ const reason = this.eligible(now);
237
+ if (!reason) {
238
+ const reflection = await this.reflect();
239
+ const growth = await this.evolve();
240
+ // A wish for now is not silently turned into a wish for the next tick.
241
+ const contact = await this.reachOut(this.now());
242
+ if (contact) return { ...contact, reflection, growth };
243
+ const presence = await this.considerPresence(this.now());
244
+ return presence && presence.status !== "presence-silent"
245
+ ? { ...presence, reflection, growth }
246
+ : {
247
+ ...reflection,
248
+ ...(growth ? { growth } : {}),
249
+ ...(presence ? { expression: presence } : {}),
250
+ };
251
+ }
252
+ const presence = await this.considerPresence(now);
253
+ if (presence) return presence;
254
+ return { status: "skipped", reason };
255
+ }
256
+ // Direct messages that came in while she slept.
257
+ async wake(now = this.now()) {
258
+ let last = null;
259
+ for (const session of this.mind.deferred()) {
260
+ const unread = this.mind.unread(session, { now });
261
+ if (!unread.length) {
262
+ this.mind.look(session, this.repo.latest(session), now);
263
+ continue;
264
+ }
265
+ const trace = await this.chat.initiate(session, { type: "wake" }, unread);
266
+ if (trace) last = { status: trace.status, reason: trace.reason, session };
267
+ // Seen once is seen: a failed look is not retried every minute.
268
+ this.mind.look(session, unread.at(-1).seq, now);
269
+ }
270
+ return last;
271
+ }
272
+ eligible(now = this.now()) {
273
+ if (this.mind.time.primary()) return "正在做自己的事";
274
+ const s = this.settings();
275
+ if (!s.solitude) return "独处未开启";
276
+ if (this.phase(now).key === "asleep") return "睡着了";
277
+ if (!this.mind.budget.allows("inner", now))
278
+ return "今天用于独处的预算已经用完";
279
+ try {
280
+ this.profile();
281
+ } catch {
282
+ return "尚未配置模型";
283
+ }
284
+ const sessions = this.living();
285
+ if (!sessions.length) return "没有正在参与、开启记忆的会话";
286
+ const ids = sessions.map((x) => x.id);
287
+ const marks = ids.map(() => "?").join(",");
288
+ const recent = this.db
289
+ .prepare(
290
+ `SELECT MAX(time) t FROM core_events WHERE session_id IN (${marks}) AND seq NOT IN (SELECT seq FROM mind_unlived) AND ${LIVE}`,
291
+ )
292
+ .get(...ids).t;
293
+ if (!recent) return "还没有真实的经历";
294
+ if (s.idleMinutes > 0 && now - recent < s.idleMinutes * MINUTE)
295
+ return "还在聊天,等一个安静的空隙";
296
+ if (
297
+ this.db
298
+ .prepare(
299
+ `SELECT 1 FROM core_jobs WHERE session_id IN (${marks}) AND status IN ('pending','running') LIMIT 1`,
300
+ )
301
+ .get(...ids)
302
+ )
303
+ return "对话处理中";
304
+ const last = this.db
305
+ .prepare(
306
+ "SELECT * FROM mind_runs WHERE kind='solitude' AND status NOT IN ('interrupted') ORDER BY started DESC LIMIT 1",
307
+ )
308
+ .get();
309
+ if (last && now - last.started < s.intervalMinutes * MINUTE)
310
+ return "距离上次独处太近";
311
+ const fresh = this.db
312
+ .prepare(
313
+ `SELECT COUNT(*) n FROM core_events WHERE session_id IN (${marks}) AND role='user' AND seq>? AND seq NOT IN (SELECT seq FROM mind_unlived) AND ${LIVE}`,
314
+ )
315
+ .get(...ids, last?.watermark || 0).n;
316
+ const revisit = this.db
317
+ .prepare(
318
+ "SELECT 1 FROM mind_thoughts WHERE status='open' AND hidden=0 AND outreach_status!='planned' AND revisit_at<=? AND revisit_at>? LIMIT 1",
319
+ )
320
+ .get(now, last?.started || 0);
321
+ const feedback = this.db
322
+ .prepare("SELECT 1 FROM reply_feedback WHERE time>? LIMIT 1")
323
+ .get(last?.started || 0);
324
+ // Something unread on the shelf is reason enough for a quiet hour now
325
+ // and then, even when nothing new has happened.
326
+ const shelf =
327
+ s.reading &&
328
+ (!last || now - last.started >= 6 * HOUR) &&
329
+ this.mind.reading.unreadCount() > 0;
330
+ // Something she was waiting for has come near, or just went by.
331
+ const ahead = this.mind.anticipations.newlyDue(last?.started || 0, now);
332
+ const timeToSelf =
333
+ now - recent >= Math.max(s.idleMinutes, s.intervalMinutes) * MINUTE &&
334
+ this.initiative.contacts(now).length > 0;
335
+ if (
336
+ (fresh === 0 || fresh < s.minMessages) &&
337
+ !revisit &&
338
+ !feedback &&
339
+ !shelf &&
340
+ !ahead &&
341
+ !timeToSelf
342
+ )
343
+ return fresh ? "新经历还不多" : "没有新的经历";
344
+ // Nothing but the time passing is the reason, and the last runs on
345
+ // that reason found nothing: wait longer each time until something new
346
+ // comes in, instead of asking again on the same clock.
347
+ const onlyTime =
348
+ (fresh === 0 || fresh < s.minMessages) &&
349
+ !revisit &&
350
+ !feedback &&
351
+ !shelf &&
352
+ !ahead;
353
+ if (onlyTime && last) {
354
+ let empties = 0;
355
+ for (const run of this.db
356
+ .prepare(
357
+ "SELECT status FROM mind_runs WHERE kind='solitude' AND status NOT IN ('interrupted') ORDER BY started DESC LIMIT 6",
358
+ )
359
+ .all()) {
360
+ if (run.status !== "empty") break;
361
+ empties++;
362
+ }
363
+ if (
364
+ empties >= 2 &&
365
+ now - last.started <
366
+ Math.min(6 * HOUR, s.intervalMinutes * MINUTE * 2 ** (empties - 1))
367
+ )
368
+ return "连着几次独处都没有新的理解,等新的经历";
369
+ }
370
+ return null;
371
+ }
372
+
373
+ experiences(...args) {
374
+ return this.context.experiences(...args);
375
+ }
376
+
377
+ feedbackSince(...args) {
378
+ return this.context.feedbackSince(...args);
379
+ }
380
+
381
+ selfView(...args) {
382
+ return this.context.selfView(...args);
383
+ }
384
+
385
+ livingForView(...args) {
386
+ return this.context.livingForView(...args);
387
+ }
388
+
389
+ faceView(...args) {
390
+ return this.context.faceView(...args);
391
+ }
392
+
393
+ identityMeetings(...args) {
394
+ return this.context.identityMeetings(...args);
395
+ }
396
+ // Threads slipping out of view: she may let them go or, with something
397
+ // new behind it, hold on.
398
+
399
+ fadingView(...args) {
400
+ return this.context.fadingView(...args);
401
+ }
402
+ // People she has grown close to. Their last message is what she can
403
+ // point at when she thinks of them.
404
+
405
+ bondCards(...args) {
406
+ return this.context.bondCards(...args);
407
+ }
408
+
409
+ missingView(...args) {
410
+ return this.context.missingView(...args);
411
+ }
412
+ // Close people she still sees, and has not talked with for a while.
413
+
414
+ quietView(...args) {
415
+ return this.context.quietView(...args);
416
+ }
417
+ // Sessions a citation actually rests on. A thought that also lists a
418
+ // private room only counts as private when it has no public room.
419
+
420
+ sourcePlaces(...args) {
421
+ return this.context.sourcePlaces(...args);
422
+ }
423
+ // Where a thought came from decides where a plan built on it may surface:
424
+ // anything rooted in a private chat stays there.
425
+
426
+ placeOf(...args) {
427
+ return this.context.placeOf(...args);
428
+ }
429
+ // Her own plans, and endings for things she was looking ahead to. Only
430
+ // what she was shown can be closed, and only with something behind it.
431
+ anticipate(result, { valid, shown, origin, now }) {
432
+ const applied = { plans: 0, closed: 0 };
433
+ for (const plan of (Array.isArray(result?.plans) ? result.plans : []).slice(
434
+ 0,
435
+ 3,
436
+ )) {
437
+ const sources = evidence(plan?.sources).filter((s) => valid.has(s));
438
+ if (!sources.length) continue;
439
+ const added = this.mind.anticipations.add({
440
+ kind: "plan",
441
+ activity: plan?.activity,
442
+ content: plan?.content,
443
+ due: plan?.due,
444
+ sources,
445
+ origin,
446
+ time: now,
447
+ ...this.placeOf(sources),
448
+ });
449
+ if (added.id) applied.plans++;
450
+ else if (!plan?.due) {
451
+ const task = this.mind.time.tasks.add(
452
+ {
453
+ kind: "plan",
454
+ activity: plan?.activity,
455
+ title: plan?.content,
456
+ why: text(plan?.why, 240) || "这次自己选择留下的打算",
457
+ sources,
458
+ ...this.placeOf(sources),
459
+ },
460
+ now,
461
+ );
462
+ if (task.id) applied.plans++;
463
+ }
464
+ }
465
+ for (const item of (Array.isArray(result?.closeAnticipations)
466
+ ? result.closeAnticipations
467
+ : []
468
+ ).slice(0, 6)) {
469
+ const id = String(item?.id ?? "").replace(/^a:/, "");
470
+ if (!shown.has(id)) continue;
471
+ const sources = evidence(item?.sources).filter((s) => valid.has(s));
472
+ if (item?.status !== "let_go" && !sources.length) continue;
473
+ if (
474
+ this.mind.anticipations.close(id, {
475
+ status: item?.status,
476
+ note: item?.why,
477
+ sources,
478
+ time: now,
479
+ })
480
+ )
481
+ applied.closed++;
482
+ }
483
+ return applied;
484
+ }
485
+ // Thoughts she decided to put down. Only ones she was shown can be let go.
486
+ letGo(items, shown, now) {
487
+ let count = 0;
488
+ for (const item of (Array.isArray(items) ? items : []).slice(0, 6)) {
489
+ const id = String(item?.id ?? item ?? "").replace(/^t:/, "");
490
+ if (!shown.has(id)) continue;
491
+ if (this.mind.thoughts.resolve(id, text(item?.why, 120) || "放下了", now))
492
+ count++;
493
+ }
494
+ return count;
495
+ }
496
+
497
+ peopleIn(...args) {
498
+ return this.context.peopleIn(...args);
499
+ }
500
+ // Applies what she concluded about herself, her faces and people. Every
501
+ // change must cite something she actually lived through.
502
+
503
+ grow(...args) {
504
+ return this.growth.grow(...args);
505
+ }
506
+ // A narrower, infrequent look at her own direction. General reflection had
507
+ // too many jobs and usually returned skip even after busy days, leaving
508
+ // numeric tendencies and group faces permanently at their seed values.
509
+
510
+ async evolve(...args) {
511
+ return this.growth.evolve(...args);
512
+ }
513
+ async reflect() {
514
+ if (this.mind.time.attention.activityBusy)
515
+ return { status: "skipped", reason: "自己的活动步骤尚未结束" };
516
+ if (this.mind.time.primary())
517
+ return { status: "skipped", reason: "正在做自己的事,保留专注时段" };
518
+ this.busy = true;
519
+ const now = this.now();
520
+ const s = this.settings();
521
+ const last = this.db
522
+ .prepare(
523
+ "SELECT * FROM mind_runs WHERE kind='solitude' ORDER BY started DESC LIMIT 1",
524
+ )
525
+ .get();
526
+ const watermark =
527
+ this.db.prepare(`SELECT MAX(seq) n FROM core_events WHERE ${LIVE}`).get()
528
+ .n || 0;
529
+ const since = last?.watermark
530
+ ? Math.min(last.watermark, watermark)
531
+ : this.db
532
+ .prepare(
533
+ `SELECT COALESCE(MIN(seq),1)-1 n FROM core_events WHERE time>? AND ${LIVE}`,
534
+ )
535
+ .get(now - 36 * HOUR).n;
536
+ const id = this.run("solitude", "安静下来,重新看看最近的事", watermark);
537
+ const trace = this.repo.trace("__mind__", "solitude");
538
+ let status = "empty";
539
+ let reason = "没有新的理解";
540
+ let summary = null;
541
+ let freshMessages = 0;
542
+ try {
543
+ const nature = this.mind.nature.current(now);
544
+ const version = nature.version;
545
+ const experiences = this.experiences(since, now);
546
+ freshMessages = experiences.reduce((n, e) => n + e.messages.length, 0);
547
+ const thoughts = this.mind.thoughts.open({ now, limit: 10 });
548
+ const chunk = s.reading ? this.mind.reading.next(now) : null;
549
+ const chapter = this.chapterView(now);
550
+ const livingFor = this.livingForView(now);
551
+ const input = {
552
+ ...(chunk ? { reading: this.mind.reading.passage(chunk) } : {}),
553
+ clock: localClock(now, this.mind.timeZone()),
554
+ currentLife: this.mind.time.view({ now }),
555
+ ...((this.mind.body?.({ now }) || []).length
556
+ ? { body: this.mind.body({ now }) }
557
+ : {}),
558
+ actions: this.mind.time.lived({
559
+ since: last?.started || now - DAY,
560
+ before: now,
561
+ }),
562
+ ...(chapter ? { chapter } : {}),
563
+ mood: (({ mood, cause, energyLabel, phaseLabel }) => ({
564
+ mood,
565
+ cause,
566
+ energy: energyLabel,
567
+ phase: phaseLabel,
568
+ }))(this.mind.affect.state(now, { nature })),
569
+ self: this.selfView(now),
570
+ livedTraits: this.mind.traits.current(nature, now),
571
+ livedPersona: this.mind.traits.persona(nature, now)?.content || "",
572
+ initiative: { contacts: this.initiative.contacts(now) },
573
+ ...(livingFor ? { livingFor } : {}),
574
+ fading: this.fadingView(now),
575
+ faces: this.faceView(experiences, now),
576
+ people: this.peopleIn(experiences, now),
577
+ missing: this.missingView(now),
578
+ fromWish: this.mind.meetings.wishAway({ now }),
579
+ quiet: this.quietView(now),
580
+ ahead: this.mind.anticipations.due({ now, limit: 5 }),
581
+ thoughts: thoughts.map((t) => ({
582
+ id: t.id,
583
+ kind: THOUGHT_KINDS[t.kind],
584
+ when: elapsedLabel(t.created, now, this.mind.timeZone()),
585
+ content: text(t.content, 200),
586
+ ...(t.revisit_at && t.revisit_at <= now ? { due: true } : {}),
587
+ })),
588
+ feedback: this.feedbackSince(last?.started || now - DAY),
589
+ experiences,
590
+ meetings: this.mind.meetings.held({
591
+ since: last?.started || now - 36 * HOUR,
592
+ before: now,
593
+ limit: 6,
594
+ }),
595
+ world: this.mind.observations.recent({
596
+ since: last?.started || now - 36 * HOUR,
597
+ before: now,
598
+ limit: 6,
599
+ }),
600
+ };
601
+ if (!input.missing.length) delete input.missing;
602
+ if (input.fromWish?.length && input.missing)
603
+ input.fromWish = input.fromWish.filter(
604
+ (p) => !input.missing.some((m) => m.userId === p.userId),
605
+ );
606
+ if (!input.fromWish.length) delete input.fromWish;
607
+ if (!input.quiet?.length) delete input.quiet;
608
+ if (!input.fading.length) delete input.fading;
609
+ if (!input.ahead.length) delete input.ahead;
610
+ if (!input.meetings.length) delete input.meetings;
611
+ if (!input.world.length) delete input.world;
612
+ while (
613
+ estimateTokens(input) > SOLITUDE_INPUT_CAP &&
614
+ input.experiences.some((e) => e.messages.length > 4)
615
+ )
616
+ for (const e of input.experiences)
617
+ if (e.messages.length > 4) e.messages.shift();
618
+ while (
619
+ estimateTokens(input) > SOLITUDE_INPUT_CAP &&
620
+ input.meetings?.length > 2
621
+ )
622
+ input.meetings.shift();
623
+ const result = await this.chat.models.call(
624
+ this.profile(),
625
+ "reflection",
626
+ replyPrompt(nature, prompts(this.repo), "reflection") +
627
+ "\n" +
628
+ withActivities(
629
+ 'plans 可以是留给自己做的事;确实想在资料书架阅读、写短文、独处思考或接触游戏资料时,可加 activity:"read"|"write"|"think"|"game",并写清content中的游戏名、why动机。game通过剧情、场景与人物互动推进游玩,直接记录自己的进度和感受。不为增加任务而列计划。',
630
+ ),
631
+ input,
632
+ trace,
633
+ );
634
+ const involved = experiences.map((e) => e.session);
635
+ // New messages while she was thinking do not undo what she understood
636
+ // about what came before; they only make a planned word there stale.
637
+ const stirred = new Set(
638
+ involved.filter((session) =>
639
+ this.repo
640
+ .eventsAfter(session, watermark, { simulated: false })
641
+ .some((m) => m.role === "user"),
642
+ ),
643
+ );
644
+ if (this.closed || this.mind.nature.version() !== version) {
645
+ if (chunk && !this.closed)
646
+ this.mind.reading.record(chunk, result?.readingNote || "", id, now);
647
+ status = "cancelled";
648
+ reason = this.closed
649
+ ? "服务停止了,这次想法不作数"
650
+ : "天性改了,这次想法不作数";
651
+ } else if (
652
+ result?.skip === true &&
653
+ !result.mood &&
654
+ !result.outreach?.text
655
+ ) {
656
+ if (chunk)
657
+ this.mind.reading.record(chunk, result?.readingNote || "", id, now);
658
+ status = chunk ? "written" : "empty";
659
+ reason = chunk ? `读了《${chunk.title}》,没多想` : reason;
660
+ if (chunk) summary = { read: chunk.title };
661
+ } else {
662
+ const missing = input.missing || [];
663
+ const ahead = input.ahead || [];
664
+ const valid = new Set([
665
+ ...input.actions.map((a) => a.ref),
666
+ ...experiences.flatMap((e) => e.messages.map((m) => `m:${m.seq}`)),
667
+ ...thoughts.map((t) => `t:${t.id}`),
668
+ ...input.feedback.map((f) => f.ref),
669
+ ...(chunk ? [`r:${chunk.id}`] : []),
670
+ ...(input.livingFor ? [`s:${input.livingFor.thread}`] : []),
671
+ ...input.self.map((s) => `s:${s.thread}`),
672
+ ...input.initiative.contacts.flatMap((c) =>
673
+ [...c.recent, ...c.thoughts].map((m) => m.ref),
674
+ ),
675
+ ...(input.meetings || []).map((m) => m.ref),
676
+ ...(input.fromWish || []).map((p) => p.ref),
677
+ ...(input.quiet || []).map((p) => p.ref).filter(Boolean),
678
+ ...missing.map((p) => p.ref).filter(Boolean),
679
+ ...ahead.map((a) => a.ref),
680
+ ...(input.world || []).map((item) => item.ref),
681
+ ]);
682
+ if (chunk)
683
+ this.mind.reading.record(chunk, result?.readingNote || "", id, now);
684
+ const noted = this.writeThought(result?.thought, {
685
+ valid,
686
+ thoughts,
687
+ involved,
688
+ reachable: new Set([
689
+ ...involved,
690
+ ...missing.flatMap((p) => p.sessions),
691
+ ...(input.fromWish || []).map((p) => p.session),
692
+ ...(input.quiet || []).flatMap((p) => p.sessions),
693
+ ...input.initiative.contacts.map((c) => c.session),
694
+ ...ahead.map((a) => a.session).filter(Boolean),
695
+ ]),
696
+ id,
697
+ now,
698
+ // New messages are considered at delivery, not a reason to erase a wish.
699
+ outreach: s.proactive ? result?.outreach : null,
700
+ });
701
+ const applied = result?.skip
702
+ ? { self: 0, faces: 0, bonds: 0, traits: 0, persona: 0 }
703
+ : this.grow(result, valid, "solitude", now);
704
+ applied.letGo = this.letGo(
705
+ result?.letGo,
706
+ new Set(thoughts.map((t) => t.id)),
707
+ now,
708
+ );
709
+ Object.assign(
710
+ applied,
711
+ this.anticipate(result, {
712
+ valid,
713
+ shown: new Set(ahead.map((a) => a.ref.slice(2))),
714
+ origin: "solitude",
715
+ now,
716
+ }),
717
+ );
718
+ if (result?.mood?.feeling && !stirred.size)
719
+ this.mind.affect.feel({
720
+ feeling: result.mood.feeling,
721
+ intensity: clamp(result.mood.intensity ?? 0.25, 0, 0.6),
722
+ valence: result.mood.valence,
723
+ cause: noted ? text(result.thought?.content, 80) : "独处",
724
+ origin: "solitude",
725
+ time: now,
726
+ });
727
+ summary = {
728
+ thought: noted,
729
+ ...(noted && this.mind.thoughts.get(noted)?.outreach
730
+ ? {
731
+ outreach: {
732
+ session: this.mind.thoughts.get(noted).outreach_session,
733
+ reason: this.mind.thoughts.get(noted).outreach_reason,
734
+ at: this.mind.thoughts.get(noted).outreach_at,
735
+ },
736
+ }
737
+ : {}),
738
+ ...applied,
739
+ ...(chunk ? { read: chunk.title } : {}),
740
+ ...(stirred.size ? { interrupted: [...stirred] } : {}),
741
+ };
742
+ status =
743
+ noted ||
744
+ applied.self ||
745
+ applied.faces ||
746
+ applied.traits ||
747
+ applied.persona ||
748
+ applied.bonds ||
749
+ applied.letGo ||
750
+ applied.plans ||
751
+ applied.closed ||
752
+ chunk
753
+ ? "written"
754
+ : result?.mood?.feeling && !stirred.size
755
+ ? "state"
756
+ : "empty";
757
+ reason =
758
+ (stirred.size ? "想着想着又有了新对话;" : "") +
759
+ (status === "written"
760
+ ? chunk
761
+ ? `读了《${chunk.title}》,${noted || applied.self ? "留下了新的理解" : "没多想"}`
762
+ : "留下了新的理解"
763
+ : status === "state"
764
+ ? "没有新想法,但心情有了变化"
765
+ : "没有新的理解");
766
+ }
767
+ } catch (error) {
768
+ status = "error";
769
+ reason = /timeout|abort/i.test(error.name)
770
+ ? "独处时模型超时"
771
+ : error instanceof SyntaxError
772
+ ? "独处输出格式无效,未保存"
773
+ : String(error.message).slice(0, 300);
774
+ trace.error = error.message;
775
+ } finally {
776
+ trace.reason = reason;
777
+ this.chat.finishQuietly(trace, status === "error" ? "error" : "complete");
778
+ this.end(id, status, reason, trace, { ...summary, freshMessages });
779
+ this.busy = false;
780
+ }
781
+ return { status, reason, runId: id };
782
+ }
783
+ writeThought(
784
+ thought,
785
+ {
786
+ valid,
787
+ thoughts,
788
+ involved,
789
+ reachable = new Set(involved),
790
+ id,
791
+ now,
792
+ outreach,
793
+ },
794
+ ) {
795
+ let reach =
796
+ outreach &&
797
+ typeof outreach.text === "string" &&
798
+ outreach.text.trim() &&
799
+ reachable.has(outreach.session) &&
800
+ !hasCredential(outreach.text)
801
+ ? outreach
802
+ : null;
803
+ // Wanting to say something does not require inventing a new revelation.
804
+ if ((!thought || typeof thought !== "object") && reach)
805
+ thought = {
806
+ kind: "reconnection",
807
+ content: reach.reason || `想说:${reach.text}`,
808
+ sources: reach.sources || [],
809
+ };
810
+ if (!thought || typeof thought !== "object") return null;
811
+ const content = text(thought.content, 600);
812
+ if (!content || hasCredential(content)) return null;
813
+ const cited = evidence([
814
+ ...(Array.isArray(thought.sources) ? thought.sources : []),
815
+ ...(Array.isArray(reach?.sources) ? reach.sources : []),
816
+ ]);
817
+ const sources = cited.filter((x) => valid.has(x));
818
+ if (!sources.length && (cited.length || !reach)) return null;
819
+ const parent = thought.parentId
820
+ ? thoughts.find((t) => t.id === thought.parentId) ||
821
+ this.mind.thoughts.get(thought.parentId)
822
+ : null;
823
+ if (thought.parentId && !parent) return null;
824
+ if (thought.kind === "revision" && !parent) return null;
825
+ // A revision that only cites what the old note already rested on is the
826
+ // same understanding again. It must not set the old one down and start
827
+ // the clock over.
828
+ if (thought.kind === "revision" && parent) {
829
+ const spent = new Set();
830
+ let current = parent;
831
+ const seen = new Set();
832
+ while (current && !seen.has(current.id)) {
833
+ seen.add(current.id);
834
+ spent.add(`t:${current.id}`);
835
+ for (const source of evidenceRoots(this.db, current.sources || [], now))
836
+ spent.add(source);
837
+ current = current.parent_id
838
+ ? this.mind.thoughts.get(current.parent_id)
839
+ : null;
840
+ }
841
+ if (
842
+ !evidenceRoots(this.db, sources, now).some(
843
+ (source) => !spent.has(source),
844
+ )
845
+ )
846
+ return null;
847
+ }
848
+ const recent = this.mind.thoughts.list({ limit: 20 });
849
+ const repeated = recent.some((t) => similar(t.content, content));
850
+ if (repeated && !reach) return null;
851
+ if (
852
+ reach &&
853
+ recent.some(
854
+ (t) =>
855
+ t.outreach_session === reach.session &&
856
+ ["planned", "sending", "uncertain"].includes(t.outreach_status) &&
857
+ similar(t.outreach, reach.text),
858
+ )
859
+ )
860
+ return null;
861
+ // Anything grounded in a private room stays there, including a planned
862
+ // sentence. A private message counts the same as a private meeting.
863
+ if (reach) {
864
+ if (
865
+ !this.initiative.audience(
866
+ {
867
+ sources: [...sources, ...(parent ? [`t:${parent.id}`] : [])],
868
+ created: now,
869
+ },
870
+ reach.session,
871
+ now,
872
+ ).allowed
873
+ )
874
+ reach = null;
875
+ }
876
+ if (!sources.length && !reach) return null;
877
+ if (repeated && !reach) return null;
878
+ const place = this.placeOf(sources);
879
+ const quiet = new Set(this.mind.meetings.privateRoots(sources));
880
+ const fits = (id) =>
881
+ quiet.size
882
+ ? quiet.has(id)
883
+ : place.discretion === "private"
884
+ ? isPrivateSession(id)
885
+ : !isPrivateSession(id);
886
+ const rooted = this.sourcePlaces(sources).filter(fits);
887
+ const sessions = [
888
+ ...new Set([
889
+ ...(rooted.length ? rooted : involved.filter(fits)),
890
+ ...(reach ? [reach.session] : []),
891
+ ]),
892
+ ];
893
+ const added = this.mind.thoughts.add({
894
+ kind: thought.kind,
895
+ content:
896
+ repeated && reach
897
+ ? text(`想说:${reach.text}。${reach.reason || ""}`, 600)
898
+ : content,
899
+ sessions,
900
+ sources,
901
+ parentId: parent?.id || null,
902
+ importance: clamp(thought.importance ?? 0.5),
903
+ revisitHours: Math.min(720, Number(thought.revisitHours) || 0),
904
+ outreach: reach ? text(reach.text, 120) : "",
905
+ outreachSession: reach?.session || null,
906
+ outreachAfterHours: reach
907
+ ? Math.max(0, Number(reach.afterHours) || 0)
908
+ : undefined,
909
+ outreachReason: reach?.reason || "",
910
+ runId: id,
911
+ time: now,
912
+ });
913
+ // The old words stay as they were; only the old understanding is set down.
914
+ if (thought.kind === "revision" && parent)
915
+ this.mind.thoughts.resolve(parent.id, "有了新的理解", now);
916
+ return added;
917
+ }
918
+
919
+ diaryDue(...args) {
920
+ return this.journal.diaryDue(...args);
921
+ }
922
+
923
+ dayStart(...args) {
924
+ return this.journal.dayStart(...args);
925
+ }
926
+ // A review can be rewritten into the story she carries everywhere, so a day
927
+ // whose words stay private does not travel there as text.
928
+
929
+ diaryLine(...args) {
930
+ return this.journal.diaryLine(...args);
931
+ }
932
+ // The account she carries into later writing. Sentences that repeat a
933
+ // private fact are left out; the rest can still be retold.
934
+
935
+ openWords(...args) {
936
+ return this.journal.openWords(...args);
937
+ }
938
+
939
+ openThreads(...args) {
940
+ return this.journal.openThreads(...args);
941
+ }
942
+ // Where she is in her own story, kept short for the diary and solitude.
943
+ // Writing that still names a duty as the life she is living is kept
944
+ // as history; those sentences are not handed back as what she is living.
945
+
946
+ chapterView(...args) {
947
+ return this.journal.chapterView(...args);
948
+ }
949
+ // The wish she is living is not yet in this chapter.
950
+
951
+ // Drop sentences that still call a duty the life she is living.
952
+
953
+ async review(...args) {
954
+ if (this.mind.time.attention.activityBusy)
955
+ return { status: "skipped", reason: "自己的活动步骤尚未结束" };
956
+ const primary = this.mind.time.primary();
957
+ if (primary)
958
+ this.mind.time.tasks.control(
959
+ primary.id,
960
+ {
961
+ action: "pause",
962
+ readyAt: this.now() + this.mind.time.settings().breakMinutes * 60000,
963
+ reason: "安排调整:先写日记,保留续接位置",
964
+ },
965
+ this.now(),
966
+ );
967
+ return this.journal.review(...args);
968
+ }
969
+ // Latest version of each chapter: she can re-understand her own past.
970
+
971
+ chapters(...args) {
972
+ return this.journal.chapters(...args);
973
+ }
974
+
975
+ chapterVersions(...args) {
976
+ return this.journal.chapterVersions(...args);
977
+ }
978
+ // Night: while she sleeps (in the small hours without a rhythm), one
979
+ // quiet task at a time, after the diary.
980
+
981
+ isNight(...args) {
982
+ return this.journal.isNight(...args);
983
+ }
984
+
985
+ nightDue(...args) {
986
+ return this.journal.nightDue(...args);
987
+ }
988
+ // Quiet chats do not wait for forty messages: what was said today is
989
+ // sorted into memory tonight.
990
+
991
+ async rememberAtNight(...args) {
992
+ return this.journal.rememberAtNight(...args);
993
+ }
994
+ // Every so often (and first once there are two diaries) she looks back
995
+ // over the stretch since the last review.
996
+
997
+ reviewDue(...args) {
998
+ return this.journal.reviewDue(...args);
999
+ }
1000
+
1001
+ async reviewPeriod(...args) {
1002
+ if (this.mind.time.attention.activityBusy)
1003
+ return { status: "skipped", reason: "自己的活动步骤尚未结束" };
1004
+ return this.journal.reviewPeriod(...args);
1005
+ }
1006
+ // continue rewrites the chapter she is in; close ends it and opens the next.
1007
+
1008
+ turnChapter(...args) {
1009
+ return this.journal.turnChapter(...args);
1010
+ }
1011
+
1012
+ diaries(...args) {
1013
+ return this.journal.diaries(...args);
1014
+ }
1015
+ // Whether to say something first is her own turn; these are only the
1016
+ // limits a considerate person keeps.
1017
+ // A room she was just in has gone quiet. She looks once and may say one
1018
+ // thing of her own; silence is still a complete answer.
1019
+
1020
+ async considerPresence(...args) {
1021
+ return this.presence.considerPresence(...args);
1022
+ }
1023
+
1024
+ presenceCooling(...args) {
1025
+ return this.presence.presenceCooling(...args);
1026
+ }
1027
+
1028
+ markPresence(...args) {
1029
+ return this.presence.markPresence(...args);
1030
+ }
1031
+
1032
+ wasHere(...args) {
1033
+ return this.presence.wasHere(...args);
1034
+ }
1035
+
1036
+ async reachOut(...args) {
1037
+ return this.presence.reachOut(...args);
1038
+ }
1039
+ // People this planned sentence was about, who have shown up since she
1040
+ // wrote it. The sentence can still be said; the absence is no longer a fact.
1041
+
1042
+ returnedSince(...args) {
1043
+ return this.presence.returnedSince(...args);
1044
+ }
1045
+
1046
+ outreachBlocked(...args) {
1047
+ return this.presence.outreachBlocked(...args);
1048
+ }
1049
+ }