luckytri 1.0.0 → 1.0.2

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 (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,786 +1,786 @@
1
- import { randomUUID } from "node:crypto";
2
- import { localClock } from "../../core/conversation-cues.js";
3
- import { replyPrompt, prompts } from "../../core/persona-manager.js";
4
- import { estimateTokens, withFallback } from "../../core/model-manager.js";
5
- import { diffSnapshots } from "../index.js";
6
- import { lifeDayStart } from "../nature.js";
7
- import { SELF_KINDS } from "../self.js";
8
- import { aboutLife, ownLife } from "../salience.js";
9
- import { DAY, HOUR, hasCredential, parse, text } from "../util.js";
10
- const MINUTE = 60000;
11
- const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
12
- const SOLITUDE_INPUT_CAP = 24000;
13
- const NIGHT_PENDING = 6;
14
-
15
- // Owns this part of the lifecycle; the facade keeps the shared runtime state.
16
- export class LifeDiary {
17
- constructor(owner) {
18
- this.owner = owner;
19
- }
20
- diaryDue(now = this.owner.now()) {
21
- const s = this.owner.settings();
22
- if (!s.diary) return null;
23
- try {
24
- this.owner.profile();
25
- } catch {
26
- return null;
27
- }
28
- if (!this.owner.mind.budget.allows("inner", now)) return null;
29
- const phase = this.owner.phase(now).key;
30
- const nature = this.owner.mind.nature.current(now);
31
- const bedtime = nature.rhythm?.enabled
32
- ? ["sleepy", "asleep"].includes(phase)
33
- : localClock(now, this.owner.mind.timeZone()).hour >= 23;
34
- const day = this.owner.lifeDay(now);
35
- // A diary that failed to come out waits a while, and a day is given up
36
- // after a few tries rather than retried every minute.
37
- const has = (d) => {
38
- if (
39
- this.owner.db
40
- .prepare("SELECT 1 FROM mind_diary WHERE day=? LIMIT 1")
41
- .get(d)
42
- )
43
- return true;
44
- const tries = this.owner.db
45
- .prepare(
46
- "SELECT COUNT(*) n, MAX(started) last FROM mind_runs WHERE kind='daily' AND json_extract(summary,'$.day')=?",
47
- )
48
- .get(d);
49
- return tries.n >= 3 || (tries.last && now - tries.last < 3 * HOUR);
50
- };
51
- const start = this.owner.dayStart(now);
52
- if (
53
- bedtime &&
54
- !has(day) &&
55
- this.owner.mind.days.participated(start, now, { inclusive: true })
56
- )
57
- return { day, start, end: now };
58
- const yesterday = this.owner.lifeDay(start - MINUTE);
59
- const before = this.owner.dayStart(start - MINUTE);
60
- if (
61
- !bedtime &&
62
- !has(yesterday) &&
63
- this.owner.mind.days.participated(before, start)
64
- )
65
- return { day: yesterday, start: before, end: start };
66
- return null;
67
- }
68
- dayStart(now) {
69
- return lifeDayStart(
70
- this.owner.mind.nature.current(now),
71
- now,
72
- this.owner.mind.timeZone(),
73
- );
74
- }
75
- diaryLine(entry, now = this.owner.now()) {
76
- const closed =
77
- this.owner.mind.meetings.privateBeyond(entry.day, "", now) ||
78
- !this.owner.mind.meetings.sayable(entry.content, "");
79
- const compare =
80
- !closed &&
81
- entry.compare &&
82
- this.owner.mind.meetings.sayable(entry.compare, "")
83
- ? text(entry.compare, 120)
84
- : "";
85
- return {
86
- ref: `d:${entry.day}`,
87
- day: entry.day,
88
- ...(entry.mood ? { mood: entry.mood } : {}),
89
- content: closed
90
- ? "这一天有留在私下的事,原文不带到回顾里。"
91
- : text(entry.content, 400),
92
- ...(compare ? { compare } : {}),
93
- ...(closed ? { private: true } : {}),
94
- };
95
- }
96
- openWords(value, limit) {
97
- let out = String(value || "");
98
- for (const saying of this.owner.mind.meetings.privateSayings(""))
99
- if (saying.content) out = out.split(saying.content).join("");
100
- out = out
101
- .replace(/[。!?]{2,}/g, (mark) => mark[0])
102
- .replace(/\n{3,}/g, "\n\n")
103
- .trim();
104
- if (!out || !this.owner.mind.meetings.sayable(out, "")) return "";
105
- return text(out, limit);
106
- }
107
- openThreads(rows, now = this.owner.now()) {
108
- const known = this.owner.mind.self.latest(now);
109
- return (rows || []).filter((item) => {
110
- const row = known.find((thread) => thread.thread === item.thread);
111
- return !!row && this.owner.mind.meetings.stays(row, "");
112
- });
113
- }
114
- chapterView(now, size = 120) {
115
- const chapter = this.owner.mind.periods.current(now);
116
- if (!chapter) return null;
117
- const gist = this.#presentLifeWriting(chapter.content, now, size);
118
- return {
119
- number: chapter.chapter,
120
- title: chapter.title,
121
- ...(gist ? { gist } : {}),
122
- };
123
- }
124
- #lifeMoved(now) {
125
- const living = this.owner.mind.self.living({ now, before: now });
126
- const chapter = this.owner.mind.periods.current(now);
127
- return !!(
128
- living &&
129
- ownLife(living.content) &&
130
- chapter &&
131
- !aboutLife(living.content, chapter.content)
132
- );
133
- }
134
- #presentLifeWriting(content, now, size) {
135
- let body = String(content || "");
136
- const living = this.owner.mind.self.living({ now, before: now });
137
- if (
138
- living &&
139
- ownLife(living.content) &&
140
- body &&
141
- !aboutLife(living.content, body)
142
- ) {
143
- const duties = this.owner.mind.self
144
- .latest(now)
145
- .filter(
146
- (row) =>
147
- row.kind === "intention" &&
148
- row.status !== "closed" &&
149
- !ownLife(row.content),
150
- );
151
- body = body
152
- .split(/(?<=[。\n])/)
153
- .filter((sentence) => {
154
- if (
155
- /为自己而活的那件事/.test(sentence) &&
156
- !aboutLife(living.content, sentence)
157
- )
158
- return false;
159
- return !duties.some((row) => aboutLife(row.content, sentence));
160
- })
161
- .join("");
162
- }
163
- return this.owner.openWords(body, size);
164
- }
165
- async review({ day, start, end }) {
166
- this.owner.busy = true;
167
- const now = this.owner.now();
168
- const id = this.owner.run("daily", `写 ${day} 的日记`);
169
- const trace = this.owner.repo.trace("__mind__", "daily");
170
- let status = "empty";
171
- let reason = "今天没有写下什么";
172
- let summary = { day };
173
- try {
174
- const nature = this.owner.mind.nature.current(now);
175
- const since =
176
- this.owner.db
177
- .prepare(
178
- `SELECT COALESCE(MIN(seq),1)-1 n FROM core_events WHERE time>? AND ${LIVE}`,
179
- )
180
- .get(start).n || 0;
181
- const experiences = this.owner.experiences(since, end, {
182
- limit: 6,
183
- rows: 14,
184
- });
185
- const thoughts = this.owner.db
186
- .prepare(
187
- "SELECT * FROM mind_thoughts WHERE created>? AND created<=? AND hidden=0 ORDER BY created LIMIT 10",
188
- )
189
- .all(start, end);
190
- const choices = this.owner.db
191
- .prepare(
192
- "SELECT choice,COUNT(*) n FROM mind_choices WHERE created>? AND created<=? GROUP BY choice",
193
- )
194
- .all(start, end);
195
- const moods = this.owner.mind.affect
196
- .history({ before: end, limit: 40 })
197
- .filter((a) => a.created > start)
198
- .slice(0, 12)
199
- .map((a) => ({
200
- at: localClock(a.created, this.owner.mind.timeZone()).local.slice(11),
201
- feeling: a.feeling,
202
- cause: text(a.cause, 50),
203
- }));
204
- const previousDay = this.owner.db
205
- .prepare(
206
- "SELECT day FROM mind_snapshots WHERE day<? ORDER BY day DESC LIMIT 1",
207
- )
208
- .get(day)?.day;
209
- const yesterday = previousDay
210
- ? this.owner.mind.snapshotOf(previousDay)
211
- : null;
212
- const yesterdayLife = yesterday
213
- ? this.owner.mind.livedThen(yesterday)
214
- : null;
215
- const lastDiary = this.owner.db
216
- .prepare(
217
- "SELECT day,content,compare FROM mind_diary WHERE day<? ORDER BY day DESC, created DESC LIMIT 1",
218
- )
219
- .get(day);
220
- const earlierDiary =
221
- lastDiary &&
222
- !this.owner.mind.meetings.privateBeyond(lastDiary.day, "", end)
223
- ? text(lastDiary.content, 240)
224
- : "";
225
- // Only the short account of her life and the chapter she is in, so
226
- // the diary does not grow with her age.
227
- const story = this.owner.mind.periods.story(now);
228
- const storyWords = this.#presentLifeWriting(story?.content, now, 600);
229
- const chapter = this.owner.chapterView(now);
230
- const anniversaries = this.owner.mind.days.anniversaries(end);
231
- const expected = this.owner.mind.anticipations.today({ start, end });
232
- const open = this.owner.mind.anticipations.due({
233
- now: end,
234
- limit: 5,
235
- shareable: true,
236
- });
237
- const ahead = Object.fromEntries(
238
- Object.entries({ ...expected, open }).filter(([, v]) => v.length),
239
- );
240
- const input = {
241
- date: day,
242
- dayOfLife: this.owner.mind.days.dayOfLife(end),
243
- today: {
244
- actions: this.owner.mind.time.lived({ since: start, before: end }),
245
- moods,
246
- thoughts: thoughts.map((t) => ({
247
- id: t.id,
248
- content: text(t.content, 200),
249
- })),
250
- choices: Object.fromEntries(choices.map((c) => [c.choice, c.n])),
251
- feedback: this.owner.feedbackSince(start),
252
- ...(Object.keys(ahead).length ? { ahead } : {}),
253
- experiences,
254
- meetings: this.owner.mind.meetings.held({
255
- since: start,
256
- before: end,
257
- limit: 6,
258
- }),
259
- world: this.owner.mind.observations.recent({
260
- since: start,
261
- before: end,
262
- limit: 6,
263
- }),
264
- },
265
- yesterday: yesterday
266
- ? {
267
- day: yesterday.day,
268
- mood: yesterday.affect?.mood,
269
- ...(yesterdayLife
270
- ? { livingFor: text(yesterdayLife.content, 80) }
271
- : {}),
272
- self: (yesterday.self || [])
273
- .filter((t) => {
274
- const row = this.owner.mind.self
275
- .latest(yesterday.created || end)
276
- .find((item) => item.thread === t.thread);
277
- return row && this.owner.mind.meetings.stays(row, "");
278
- })
279
- .slice(0, 12)
280
- .map(
281
- (t) =>
282
- `${SELF_KINDS[t.kind] || t.kind}:${t.content}(${t.strength})`,
283
- ),
284
- ...(earlierDiary ? { diary: earlierDiary } : {}),
285
- }
286
- : null,
287
- self: this.owner.selfView(now, { open: true }),
288
- faces: this.owner.faceView(experiences, now),
289
- livedTraits: this.owner.mind.traits.current(nature, now),
290
- livedPersona:
291
- this.owner.mind.traits.persona(nature, now)?.content || "",
292
- ...(this.owner.livingForView(end, { since: start, open: true })
293
- ? {
294
- livingFor: this.owner.livingForView(end, {
295
- since: start,
296
- open: true,
297
- }),
298
- }
299
- : {}),
300
- people: this.owner.peopleIn(experiences, now, { open: true }),
301
- ...(storyWords ? { story: storyWords } : {}),
302
- ...(chapter
303
- ? {
304
- chapter: {
305
- ...chapter,
306
- gist: this.owner.openWords(chapter.gist, 120) || undefined,
307
- },
308
- }
309
- : {}),
310
- ...(anniversaries.length ? { anniversaries } : {}),
311
- };
312
- if (!input.today.meetings.length) delete input.today.meetings;
313
- if (!input.today.world.length) delete input.today.world;
314
- while (
315
- estimateTokens(input) > SOLITUDE_INPUT_CAP &&
316
- input.today.experiences.some((e) => e.messages.length > 3)
317
- )
318
- for (const e of input.today.experiences)
319
- if (e.messages.length > 3) e.messages.shift();
320
- while (
321
- estimateTokens(input) > SOLITUDE_INPUT_CAP &&
322
- input.today.meetings?.length > 2
323
- )
324
- input.today.meetings.shift();
325
- const result = await this.owner.chat.models.call(
326
- this.owner.profile(),
327
- "daily",
328
- replyPrompt(nature, prompts(this.owner.repo), "daily"),
329
- input,
330
- trace,
331
- );
332
- const diary = text(result?.diary, 1200);
333
- if (!diary || hasCredential(diary)) throw SyntaxError("日记格式无效");
334
- const valid = new Set([
335
- ...input.today.actions.map((a) => a.ref),
336
- ...experiences.flatMap((e) => e.messages.map((m) => `m:${m.seq}`)),
337
- ...thoughts.map((t) => `t:${t.id}`),
338
- ...input.today.feedback.map((f) => f.ref),
339
- ...open.map((a) => a.ref),
340
- ...(input.livingFor ? [`s:${input.livingFor.thread}`] : []),
341
- ...(input.today.meetings || []).map((m) => m.ref),
342
- ...(input.today.world || []).map((item) => item.ref),
343
- ]);
344
- this.owner.db
345
- .prepare(
346
- "INSERT INTO mind_diary(id,day,created,content,mood,compare,sources,run_id) VALUES (?,?,?,?,?,?,?,?)",
347
- )
348
- .run(
349
- randomUUID(),
350
- day,
351
- now,
352
- diary,
353
- text(result.mood, 16),
354
- text(result.compare, 300),
355
- JSON.stringify([...valid].slice(0, 24)),
356
- id,
357
- );
358
- const applied = this.owner.grow(result, valid, "daily", now);
359
- applied.letGo = this.owner.letGo(
360
- result?.letGo,
361
- new Set(thoughts.map((t) => t.id)),
362
- now,
363
- );
364
- Object.assign(
365
- applied,
366
- this.owner.anticipate(result, {
367
- valid,
368
- shown: new Set(open.map((a) => a.ref.slice(2))),
369
- origin: "daily",
370
- now,
371
- }),
372
- );
373
- if (result.mood)
374
- this.owner.mind.affect.feel({
375
- feeling: result.mood,
376
- intensity: 0.2,
377
- valence: 0,
378
- cause: "写完了今天的日记",
379
- origin: "daily",
380
- time: now,
381
- });
382
- this.owner.mind.snapshot(now, day);
383
- summary = { day, ...applied };
384
- status = "written";
385
- reason = `写下了 ${day} 的日记`;
386
- } catch (error) {
387
- status = "error";
388
- reason =
389
- error instanceof SyntaxError
390
- ? "日记格式无效,未保存"
391
- : String(error.message).slice(0, 300);
392
- trace.error = error.message;
393
- } finally {
394
- trace.reason = reason;
395
- this.owner.chat.finishQuietly(
396
- trace,
397
- status === "error" ? "error" : "complete",
398
- );
399
- this.owner.end(id, status, reason, trace, summary);
400
- this.owner.busy = false;
401
- }
402
- return { status, reason, runId: id };
403
- }
404
- chapters() {
405
- return this.owner.mind.periods.chapters();
406
- }
407
- chapterVersions(chapter) {
408
- return this.owner.mind.periods.chapterVersions(chapter);
409
- }
410
- isNight(now = this.owner.now()) {
411
- if (this.owner.mind.nature.current(now).rhythm?.enabled)
412
- return this.owner.phase(now).key === "asleep";
413
- const hour = localClock(now, this.owner.mind.timeZone()).hour;
414
- return hour >= 3 && hour < 6;
415
- }
416
- nightDue(now = this.owner.now()) {
417
- if (!this.owner.settings().night || !this.owner.isNight(now)) return null;
418
- try {
419
- this.owner.profile();
420
- } catch {
421
- return null;
422
- }
423
- const day = this.owner.lifeDay(now);
424
- if (
425
- this.owner.mind.budget.allows("upkeep", now) &&
426
- this.owner.repo.store.settings().memoryEnabled !== false
427
- ) {
428
- const done = this.owner.db.prepare(
429
- "SELECT 1 FROM mind_runs WHERE kind='night' AND json_extract(summary,'$.day')=? AND json_extract(summary,'$.session')=? LIMIT 1",
430
- );
431
- for (const { id } of this.owner.living())
432
- if (
433
- !this.owner.mind.memory.busy.has(id) &&
434
- this.owner.mind.memory.pending(id) >= NIGHT_PENDING &&
435
- !done.get(day, id)
436
- )
437
- return { kind: "memory", session: id, day };
438
- }
439
- if (this.owner.reviewDue(now)) return { kind: "review" };
440
- return null;
441
- }
442
- async rememberAtNight(session, day, now = this.owner.now()) {
443
- this.owner.busy = true;
444
- const name =
445
- this.owner.living().find((s) => s.id === session)?.name || session;
446
- const id = this.owner.run("night", `夜里整理「${name}」里的事`);
447
- const trace = this.owner.repo.trace(session, "memory");
448
- let status = "empty";
449
- let reason = "没有需要整理的";
450
- try {
451
- const profile = this.owner.profile();
452
- const models = withFallback(
453
- this.owner.chat.models,
454
- this.owner.chat.fallbackFor(null, profile, trace),
455
- );
456
- const before = this.owner.mind.memory.pending(session);
457
- await this.owner.mind.memory.consolidate(
458
- session,
459
- profile,
460
- prompts(this.owner.repo).memory,
461
- trace,
462
- { force: true, models, now },
463
- );
464
- const left = this.owner.mind.memory.pending(session);
465
- if (left < before) {
466
- status = "written";
467
- reason = `夜里整理了「${name}」的 ${before - left} 条消息`;
468
- }
469
- } catch (error) {
470
- status = "error";
471
- reason = String(error.message).slice(0, 300);
472
- trace.error = error.message;
473
- } finally {
474
- trace.reason = reason;
475
- this.owner.chat.finishQuietly(
476
- trace,
477
- status === "error" ? "error" : "complete",
478
- );
479
- this.owner.end(id, status, reason, trace, { day, session });
480
- this.owner.busy = false;
481
- }
482
- return { status, reason, runId: id };
483
- }
484
- reviewDue(now = this.owner.now()) {
485
- const s = this.owner.settings();
486
- if (!s.diary || !this.owner.mind.budget.allows("inner", now)) return false;
487
- const last = this.owner.mind.periods.lastReview(now);
488
- const written = this.owner.db
489
- .prepare(
490
- "SELECT COUNT(DISTINCT day) n FROM mind_diary WHERE created>? AND created<=?",
491
- )
492
- .get(last?.created ?? 0, now).n;
493
- if (written < 2) return false;
494
- const wait = this.#lifeMoved(now) ? DAY : s.chapterDays * DAY - 6 * HOUR;
495
- if (last && now - last.created < wait) return false;
496
- const tries = this.owner.db
497
- .prepare(
498
- "SELECT COUNT(*) n, MAX(started) last FROM mind_runs WHERE kind='weekly' AND status='error' AND started>=?",
499
- )
500
- .get(this.owner.dayStart(now));
501
- return !(tries.n >= 3 || (tries.last && now - tries.last < 3 * HOUR));
502
- }
503
- async reviewPeriod(now = this.owner.now()) {
504
- this.owner.busy = true;
505
- const id = this.owner.run("weekly", "回顾这一段日子");
506
- const trace = this.owner.repo.trace("__mind__", "weekly");
507
- let status = "empty";
508
- let reason = "这次没有写下回顾";
509
- let summary = null;
510
- try {
511
- const nature = this.owner.mind.nature.current(now);
512
- const periods = this.owner.mind.periods;
513
- const last = periods.lastReview(now);
514
- const start =
515
- last?.period_end ??
516
- this.owner.mind.days.born() ??
517
- now - this.owner.settings().chapterDays * DAY;
518
- const diaries = [
519
- ...new Map(
520
- this.owner.db
521
- .prepare(
522
- "SELECT * FROM mind_diary WHERE created>? AND created<=? ORDER BY day, created",
523
- )
524
- .all(last?.created ?? 0, now)
525
- .map((d) => [d.day, d]),
526
- ).values(),
527
- ].slice(-10);
528
- const first = diaries[0]?.day;
529
- const earlier = first
530
- ? this.owner.db
531
- .prepare(
532
- "SELECT day FROM mind_snapshots WHERE day<? ORDER BY day DESC LIMIT 1",
533
- )
534
- .get(first)?.day
535
- : null;
536
- const latest = this.owner.db
537
- .prepare(
538
- "SELECT day FROM mind_snapshots WHERE created<=? ORDER BY day DESC LIMIT 1",
539
- )
540
- .get(now)?.day;
541
- const earlierSnap = earlier ? this.owner.mind.snapshotOf(earlier) : null;
542
- const latestSnap = latest ? this.owner.mind.snapshotOf(latest) : null;
543
- const asLived = (snap) =>
544
- snap ? { ...snap, livingFor: this.owner.mind.livedThen(snap) } : null;
545
- const change =
546
- earlierSnap && latestSnap
547
- ? diffSnapshots(asLived(earlierSnap), asLived(latestSnap))
548
- : null;
549
- const chapter = periods.current(now);
550
- const previous = chapter
551
- ? periods.chapters(now).find((c) => c.chapter === chapter.chapter - 1)
552
- : null;
553
- const story = periods.story(now);
554
- const { kept, missed } = this.owner.mind.anticipations.today({
555
- start,
556
- end: now,
557
- shareable: true,
558
- });
559
- const anniversaries = this.owner.mind.days.anniversaries(now);
560
- const line = (t) =>
561
- `${SELF_KINDS[t.kind] || t.kind}:${text(t.content, 60)}`;
562
- const lastReview = last
563
- ? this.#presentLifeWriting(last.content, now, 300)
564
- : "";
565
- const storyWords = story
566
- ? this.#presentLifeWriting(story.content, now, 600)
567
- : "";
568
- const previousWords = previous
569
- ? this.#presentLifeWriting(previous.content, now, 160)
570
- : "";
571
- const input = {
572
- dayOfLife: this.owner.mind.days.dayOfLife(now),
573
- period: { from: first, to: diaries.at(-1)?.day },
574
- diaries: diaries.map((d) => this.owner.diaryLine(d, now)),
575
- ...(last
576
- ? {
577
- lastReview: {
578
- ...(lastReview ? { content: lastReview } : {}),
579
- ...(last.compare ? { compare: text(last.compare, 120) } : {}),
580
- },
581
- }
582
- : {}),
583
- ...(change
584
- ? {
585
- changes: {
586
- appeared: this.owner
587
- .openThreads(change.appeared, now)
588
- .slice(0, 6)
589
- .map(line),
590
- faded: this.owner
591
- .openThreads(change.faded, now)
592
- .slice(0, 6)
593
- .map(line),
594
- changed: this.owner
595
- .openThreads(change.changed, now)
596
- .slice(0, 6)
597
- .map(
598
- (t) =>
599
- `${text(t.before.content, 40)} → ${text(t.content, 40)}`,
600
- ),
601
- people: change.people
602
- .sort(
603
- (a, b) => Math.abs(b.shift ?? 1) - Math.abs(a.shift ?? 1),
604
- )
605
- .slice(0, 5)
606
- .map(
607
- (p) =>
608
- `${p.name}:${p.shift === null ? "新认识的" : p.shift > 0 ? "更近了" : "远了一些"}`,
609
- ),
610
- ...(change.livingFor
611
- ? {
612
- livingFor: `${text(change.livingFor.from, 40) || "还没有"} → ${text(change.livingFor.to, 40) || "还没有"}`,
613
- }
614
- : {}),
615
- },
616
- }
617
- : {}),
618
- ...(kept.length || missed.length
619
- ? { ahead: { kept: kept.slice(0, 5), missed: missed.slice(0, 5) } }
620
- : {}),
621
- ...(chapter
622
- ? {
623
- chapter: {
624
- number: chapter.chapter,
625
- title: chapter.title,
626
- content: this.#presentLifeWriting(chapter.content, now, 800),
627
- reviews: periods.reviewsSince(
628
- periods.began(chapter.chapter),
629
- now,
630
- ),
631
- },
632
- }
633
- : {}),
634
- ...(previous
635
- ? {
636
- previousChapter: {
637
- number: previous.chapter,
638
- title: previous.title,
639
- ...(previousWords ? { summary: previousWords } : {}),
640
- },
641
- }
642
- : {}),
643
- ...(storyWords ? { story: storyWords } : {}),
644
- ...(anniversaries.length ? { anniversaries } : {}),
645
- self: this.owner.selfView(now, { open: true }).slice(0, 10),
646
- ...(this.owner.livingForView(now, { since: start, open: true })
647
- ? {
648
- livingFor: this.owner.livingForView(now, {
649
- since: start,
650
- open: true,
651
- }),
652
- }
653
- : {}),
654
- };
655
- const result = await this.owner.chat.models.call(
656
- this.owner.profile(),
657
- "weekly",
658
- replyPrompt(nature, prompts(this.owner.repo), "weekly"),
659
- input,
660
- trace,
661
- );
662
- const week = text(result?.week, 1500);
663
- if (!week || hasCredential(week)) throw SyntaxError("回顾格式无效");
664
- const valid = new Set([
665
- ...diaries.map((d) => `d:${d.day}`),
666
- ...(input.livingFor ? [`s:${input.livingFor.thread}`] : []),
667
- ]);
668
- periods.write("week", {
669
- start,
670
- end: now,
671
- content: week,
672
- compare: result?.compare,
673
- sources: [...valid],
674
- runId: id,
675
- time: now,
676
- });
677
- const applied = this.owner.grow(
678
- { self: result.self, bonds: result.bonds },
679
- valid,
680
- "weekly",
681
- now,
682
- );
683
- const turned = this.owner.turnChapter(result?.chapter, {
684
- chapter,
685
- start,
686
- now,
687
- runId: id,
688
- });
689
- const told = text(result?.story, 1800);
690
- const prior = story?.content || "";
691
- const living = input.livingFor?.content || "";
692
- const livingMoved = Boolean(living) && !prior.includes(living);
693
- const retold =
694
- !!told &&
695
- told !== prior &&
696
- !hasCredential(told) &&
697
- (turned.opened || !story || livingMoved);
698
- if (retold)
699
- periods.write("story", {
700
- content: told,
701
- sources: [...valid],
702
- runId: id,
703
- time: now,
704
- });
705
- summary = { chapter: turned.number, story: retold, ...applied };
706
- status = "written";
707
- reason =
708
- turned.opened && chapter
709
- ? `回顾了这段日子,翻开了第 ${turned.number} 章`
710
- : turned.number
711
- ? `回顾了这段日子,写下了第 ${turned.number} 章`
712
- : "回顾了这段日子";
713
- } catch (error) {
714
- status = "error";
715
- reason =
716
- error instanceof SyntaxError
717
- ? "回顾格式无效,未保存"
718
- : String(error.message).slice(0, 300);
719
- trace.error = error.message;
720
- } finally {
721
- trace.reason = reason;
722
- this.owner.chat.finishQuietly(
723
- trace,
724
- status === "error" ? "error" : "complete",
725
- );
726
- this.owner.end(id, status, reason, trace, summary);
727
- this.owner.busy = false;
728
- }
729
- return { status, reason, runId: id };
730
- }
731
- turnChapter(value, { chapter, start, now, runId }) {
732
- const none = { number: null, opened: false };
733
- if (!value || typeof value !== "object") return none;
734
- if (!["continue", "close"].includes(value.action)) return none;
735
- const title = text(value.title, 60);
736
- const content = text(value.content, 2400);
737
- if (!title || !content || hasCredential(content)) return none;
738
- const periods = this.owner.mind.periods;
739
- if (!chapter)
740
- return {
741
- number: periods.writeChapter({
742
- number: 1,
743
- title,
744
- content,
745
- start: this.owner.mind.days.born() ?? start,
746
- end: now,
747
- runId,
748
- time: now,
749
- }),
750
- opened: true,
751
- };
752
- if (value.action === "continue")
753
- return {
754
- number: periods.writeChapter({
755
- number: chapter.chapter,
756
- title,
757
- content,
758
- start: periods.began(chapter.chapter) ?? chapter.period_start,
759
- end: now,
760
- runId,
761
- time: now,
762
- }),
763
- opened: false,
764
- };
765
- return {
766
- number: periods.writeChapter({
767
- number: chapter.chapter + 1,
768
- title,
769
- content,
770
- start: now,
771
- end: now,
772
- runId,
773
- time: now,
774
- }),
775
- opened: true,
776
- };
777
- }
778
- diaries({ before = "9999-12-31", limit = 14 } = {}) {
779
- return this.owner.db
780
- .prepare(
781
- "SELECT * FROM mind_diary WHERE day<=? ORDER BY day DESC, created DESC LIMIT ?",
782
- )
783
- .all(before, limit)
784
- .map((d) => ({ ...d, sources: parse(d.sources, []) }));
785
- }
786
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { localClock } from "../../core/conversation-cues.js";
3
+ import { replyPrompt, prompts } from "../../core/persona-manager.js";
4
+ import { estimateTokens, withFallback } from "../../core/model-manager.js";
5
+ import { diffSnapshots } from "../index.js";
6
+ import { lifeDayStart } from "../nature.js";
7
+ import { SELF_KINDS } from "../self.js";
8
+ import { aboutLife, ownLife } from "../salience.js";
9
+ import { DAY, HOUR, hasCredential, parse, text } from "../util.js";
10
+ const MINUTE = 60000;
11
+ const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
12
+ const SOLITUDE_INPUT_CAP = 24000;
13
+ const NIGHT_PENDING = 6;
14
+
15
+ // Owns this part of the lifecycle; the facade keeps the shared runtime state.
16
+ export class LifeDiary {
17
+ constructor(owner) {
18
+ this.owner = owner;
19
+ }
20
+ diaryDue(now = this.owner.now()) {
21
+ const s = this.owner.settings();
22
+ if (!s.diary) return null;
23
+ try {
24
+ this.owner.profile();
25
+ } catch {
26
+ return null;
27
+ }
28
+ if (!this.owner.mind.budget.allows("inner", now)) return null;
29
+ const phase = this.owner.phase(now).key;
30
+ const nature = this.owner.mind.nature.current(now);
31
+ const bedtime = nature.rhythm?.enabled
32
+ ? ["sleepy", "asleep"].includes(phase)
33
+ : localClock(now, this.owner.mind.timeZone()).hour >= 23;
34
+ const day = this.owner.lifeDay(now);
35
+ // A diary that failed to come out waits a while, and a day is given up
36
+ // after a few tries rather than retried every minute.
37
+ const has = (d) => {
38
+ if (
39
+ this.owner.db
40
+ .prepare("SELECT 1 FROM mind_diary WHERE day=? LIMIT 1")
41
+ .get(d)
42
+ )
43
+ return true;
44
+ const tries = this.owner.db
45
+ .prepare(
46
+ "SELECT COUNT(*) n, MAX(started) last FROM mind_runs WHERE kind='daily' AND json_extract(summary,'$.day')=?",
47
+ )
48
+ .get(d);
49
+ return tries.n >= 3 || (tries.last && now - tries.last < 3 * HOUR);
50
+ };
51
+ const start = this.owner.dayStart(now);
52
+ if (
53
+ bedtime &&
54
+ !has(day) &&
55
+ this.owner.mind.days.participated(start, now, { inclusive: true })
56
+ )
57
+ return { day, start, end: now };
58
+ const yesterday = this.owner.lifeDay(start - MINUTE);
59
+ const before = this.owner.dayStart(start - MINUTE);
60
+ if (
61
+ !bedtime &&
62
+ !has(yesterday) &&
63
+ this.owner.mind.days.participated(before, start)
64
+ )
65
+ return { day: yesterday, start: before, end: start };
66
+ return null;
67
+ }
68
+ dayStart(now) {
69
+ return lifeDayStart(
70
+ this.owner.mind.nature.current(now),
71
+ now,
72
+ this.owner.mind.timeZone(),
73
+ );
74
+ }
75
+ diaryLine(entry, now = this.owner.now()) {
76
+ const closed =
77
+ this.owner.mind.meetings.privateBeyond(entry.day, "", now) ||
78
+ !this.owner.mind.meetings.sayable(entry.content, "");
79
+ const compare =
80
+ !closed &&
81
+ entry.compare &&
82
+ this.owner.mind.meetings.sayable(entry.compare, "")
83
+ ? text(entry.compare, 120)
84
+ : "";
85
+ return {
86
+ ref: `d:${entry.day}`,
87
+ day: entry.day,
88
+ ...(entry.mood ? { mood: entry.mood } : {}),
89
+ content: closed
90
+ ? "这一天有留在私下的事,原文不带到回顾里。"
91
+ : text(entry.content, 400),
92
+ ...(compare ? { compare } : {}),
93
+ ...(closed ? { private: true } : {}),
94
+ };
95
+ }
96
+ openWords(value, limit) {
97
+ let out = String(value || "");
98
+ for (const saying of this.owner.mind.meetings.privateSayings(""))
99
+ if (saying.content) out = out.split(saying.content).join("");
100
+ out = out
101
+ .replace(/[。!?]{2,}/g, (mark) => mark[0])
102
+ .replace(/\n{3,}/g, "\n\n")
103
+ .trim();
104
+ if (!out || !this.owner.mind.meetings.sayable(out, "")) return "";
105
+ return text(out, limit);
106
+ }
107
+ openThreads(rows, now = this.owner.now()) {
108
+ const known = this.owner.mind.self.latest(now);
109
+ return (rows || []).filter((item) => {
110
+ const row = known.find((thread) => thread.thread === item.thread);
111
+ return !!row && this.owner.mind.meetings.stays(row, "");
112
+ });
113
+ }
114
+ chapterView(now, size = 120) {
115
+ const chapter = this.owner.mind.periods.current(now);
116
+ if (!chapter) return null;
117
+ const gist = this.#presentLifeWriting(chapter.content, now, size);
118
+ return {
119
+ number: chapter.chapter,
120
+ title: chapter.title,
121
+ ...(gist ? { gist } : {}),
122
+ };
123
+ }
124
+ #lifeMoved(now) {
125
+ const living = this.owner.mind.self.living({ now, before: now });
126
+ const chapter = this.owner.mind.periods.current(now);
127
+ return !!(
128
+ living &&
129
+ ownLife(living.content) &&
130
+ chapter &&
131
+ !aboutLife(living.content, chapter.content)
132
+ );
133
+ }
134
+ #presentLifeWriting(content, now, size) {
135
+ let body = String(content || "");
136
+ const living = this.owner.mind.self.living({ now, before: now });
137
+ if (
138
+ living &&
139
+ ownLife(living.content) &&
140
+ body &&
141
+ !aboutLife(living.content, body)
142
+ ) {
143
+ const duties = this.owner.mind.self
144
+ .latest(now)
145
+ .filter(
146
+ (row) =>
147
+ row.kind === "intention" &&
148
+ row.status !== "closed" &&
149
+ !ownLife(row.content),
150
+ );
151
+ body = body
152
+ .split(/(?<=[。\n])/)
153
+ .filter((sentence) => {
154
+ if (
155
+ /为自己而活的那件事/.test(sentence) &&
156
+ !aboutLife(living.content, sentence)
157
+ )
158
+ return false;
159
+ return !duties.some((row) => aboutLife(row.content, sentence));
160
+ })
161
+ .join("");
162
+ }
163
+ return this.owner.openWords(body, size);
164
+ }
165
+ async review({ day, start, end }) {
166
+ this.owner.busy = true;
167
+ const now = this.owner.now();
168
+ const id = this.owner.run("daily", `写 ${day} 的日记`);
169
+ const trace = this.owner.repo.trace("__mind__", "daily");
170
+ let status = "empty";
171
+ let reason = "今天没有写下什么";
172
+ let summary = { day };
173
+ try {
174
+ const nature = this.owner.mind.nature.current(now);
175
+ const since =
176
+ this.owner.db
177
+ .prepare(
178
+ `SELECT COALESCE(MIN(seq),1)-1 n FROM core_events WHERE time>? AND ${LIVE}`,
179
+ )
180
+ .get(start).n || 0;
181
+ const experiences = this.owner.experiences(since, end, {
182
+ limit: 6,
183
+ rows: 14,
184
+ });
185
+ const thoughts = this.owner.db
186
+ .prepare(
187
+ "SELECT * FROM mind_thoughts WHERE created>? AND created<=? AND hidden=0 ORDER BY created LIMIT 10",
188
+ )
189
+ .all(start, end);
190
+ const choices = this.owner.db
191
+ .prepare(
192
+ "SELECT choice,COUNT(*) n FROM mind_choices WHERE created>? AND created<=? GROUP BY choice",
193
+ )
194
+ .all(start, end);
195
+ const moods = this.owner.mind.affect
196
+ .history({ before: end, limit: 40 })
197
+ .filter((a) => a.created > start)
198
+ .slice(0, 12)
199
+ .map((a) => ({
200
+ at: localClock(a.created, this.owner.mind.timeZone()).local.slice(11),
201
+ feeling: a.feeling,
202
+ cause: text(a.cause, 50),
203
+ }));
204
+ const previousDay = this.owner.db
205
+ .prepare(
206
+ "SELECT day FROM mind_snapshots WHERE day<? ORDER BY day DESC LIMIT 1",
207
+ )
208
+ .get(day)?.day;
209
+ const yesterday = previousDay
210
+ ? this.owner.mind.snapshotOf(previousDay)
211
+ : null;
212
+ const yesterdayLife = yesterday
213
+ ? this.owner.mind.livedThen(yesterday)
214
+ : null;
215
+ const lastDiary = this.owner.db
216
+ .prepare(
217
+ "SELECT day,content,compare FROM mind_diary WHERE day<? ORDER BY day DESC, created DESC LIMIT 1",
218
+ )
219
+ .get(day);
220
+ const earlierDiary =
221
+ lastDiary &&
222
+ !this.owner.mind.meetings.privateBeyond(lastDiary.day, "", end)
223
+ ? text(lastDiary.content, 240)
224
+ : "";
225
+ // Only the short account of her life and the chapter she is in, so
226
+ // the diary does not grow with her age.
227
+ const story = this.owner.mind.periods.story(now);
228
+ const storyWords = this.#presentLifeWriting(story?.content, now, 600);
229
+ const chapter = this.owner.chapterView(now);
230
+ const anniversaries = this.owner.mind.days.anniversaries(end);
231
+ const expected = this.owner.mind.anticipations.today({ start, end });
232
+ const open = this.owner.mind.anticipations.due({
233
+ now: end,
234
+ limit: 5,
235
+ shareable: true,
236
+ });
237
+ const ahead = Object.fromEntries(
238
+ Object.entries({ ...expected, open }).filter(([, v]) => v.length),
239
+ );
240
+ const input = {
241
+ date: day,
242
+ dayOfLife: this.owner.mind.days.dayOfLife(end),
243
+ today: {
244
+ actions: this.owner.mind.time.lived({ since: start, before: end }),
245
+ moods,
246
+ thoughts: thoughts.map((t) => ({
247
+ id: t.id,
248
+ content: text(t.content, 200),
249
+ })),
250
+ choices: Object.fromEntries(choices.map((c) => [c.choice, c.n])),
251
+ feedback: this.owner.feedbackSince(start),
252
+ ...(Object.keys(ahead).length ? { ahead } : {}),
253
+ experiences,
254
+ meetings: this.owner.mind.meetings.held({
255
+ since: start,
256
+ before: end,
257
+ limit: 6,
258
+ }),
259
+ world: this.owner.mind.observations.recent({
260
+ since: start,
261
+ before: end,
262
+ limit: 6,
263
+ }),
264
+ },
265
+ yesterday: yesterday
266
+ ? {
267
+ day: yesterday.day,
268
+ mood: yesterday.affect?.mood,
269
+ ...(yesterdayLife
270
+ ? { livingFor: text(yesterdayLife.content, 80) }
271
+ : {}),
272
+ self: (yesterday.self || [])
273
+ .filter((t) => {
274
+ const row = this.owner.mind.self
275
+ .latest(yesterday.created || end)
276
+ .find((item) => item.thread === t.thread);
277
+ return row && this.owner.mind.meetings.stays(row, "");
278
+ })
279
+ .slice(0, 12)
280
+ .map(
281
+ (t) =>
282
+ `${SELF_KINDS[t.kind] || t.kind}:${t.content}(${t.strength})`,
283
+ ),
284
+ ...(earlierDiary ? { diary: earlierDiary } : {}),
285
+ }
286
+ : null,
287
+ self: this.owner.selfView(now, { open: true }),
288
+ faces: this.owner.faceView(experiences, now),
289
+ livedTraits: this.owner.mind.traits.current(nature, now),
290
+ livedPersona:
291
+ this.owner.mind.traits.persona(nature, now)?.content || "",
292
+ ...(this.owner.livingForView(end, { since: start, open: true })
293
+ ? {
294
+ livingFor: this.owner.livingForView(end, {
295
+ since: start,
296
+ open: true,
297
+ }),
298
+ }
299
+ : {}),
300
+ people: this.owner.peopleIn(experiences, now, { open: true }),
301
+ ...(storyWords ? { story: storyWords } : {}),
302
+ ...(chapter
303
+ ? {
304
+ chapter: {
305
+ ...chapter,
306
+ gist: this.owner.openWords(chapter.gist, 120) || undefined,
307
+ },
308
+ }
309
+ : {}),
310
+ ...(anniversaries.length ? { anniversaries } : {}),
311
+ };
312
+ if (!input.today.meetings.length) delete input.today.meetings;
313
+ if (!input.today.world.length) delete input.today.world;
314
+ while (
315
+ estimateTokens(input) > SOLITUDE_INPUT_CAP &&
316
+ input.today.experiences.some((e) => e.messages.length > 3)
317
+ )
318
+ for (const e of input.today.experiences)
319
+ if (e.messages.length > 3) e.messages.shift();
320
+ while (
321
+ estimateTokens(input) > SOLITUDE_INPUT_CAP &&
322
+ input.today.meetings?.length > 2
323
+ )
324
+ input.today.meetings.shift();
325
+ const result = await this.owner.chat.models.call(
326
+ this.owner.profile(),
327
+ "daily",
328
+ replyPrompt(nature, prompts(this.owner.repo), "daily"),
329
+ input,
330
+ trace,
331
+ );
332
+ const diary = text(result?.diary, 1200);
333
+ if (!diary || hasCredential(diary)) throw SyntaxError("日记格式无效");
334
+ const valid = new Set([
335
+ ...input.today.actions.map((a) => a.ref),
336
+ ...experiences.flatMap((e) => e.messages.map((m) => `m:${m.seq}`)),
337
+ ...thoughts.map((t) => `t:${t.id}`),
338
+ ...input.today.feedback.map((f) => f.ref),
339
+ ...open.map((a) => a.ref),
340
+ ...(input.livingFor ? [`s:${input.livingFor.thread}`] : []),
341
+ ...(input.today.meetings || []).map((m) => m.ref),
342
+ ...(input.today.world || []).map((item) => item.ref),
343
+ ]);
344
+ this.owner.db
345
+ .prepare(
346
+ "INSERT INTO mind_diary(id,day,created,content,mood,compare,sources,run_id) VALUES (?,?,?,?,?,?,?,?)",
347
+ )
348
+ .run(
349
+ randomUUID(),
350
+ day,
351
+ now,
352
+ diary,
353
+ text(result.mood, 16),
354
+ text(result.compare, 300),
355
+ JSON.stringify([...valid].slice(0, 24)),
356
+ id,
357
+ );
358
+ const applied = this.owner.grow(result, valid, "daily", now);
359
+ applied.letGo = this.owner.letGo(
360
+ result?.letGo,
361
+ new Set(thoughts.map((t) => t.id)),
362
+ now,
363
+ );
364
+ Object.assign(
365
+ applied,
366
+ this.owner.anticipate(result, {
367
+ valid,
368
+ shown: new Set(open.map((a) => a.ref.slice(2))),
369
+ origin: "daily",
370
+ now,
371
+ }),
372
+ );
373
+ if (result.mood)
374
+ this.owner.mind.affect.feel({
375
+ feeling: result.mood,
376
+ intensity: 0.2,
377
+ valence: 0,
378
+ cause: "写完了今天的日记",
379
+ origin: "daily",
380
+ time: now,
381
+ });
382
+ this.owner.mind.snapshot(now, day);
383
+ summary = { day, ...applied };
384
+ status = "written";
385
+ reason = `写下了 ${day} 的日记`;
386
+ } catch (error) {
387
+ status = "error";
388
+ reason =
389
+ error instanceof SyntaxError
390
+ ? "日记格式无效,未保存"
391
+ : String(error.message).slice(0, 300);
392
+ trace.error = error.message;
393
+ } finally {
394
+ trace.reason = reason;
395
+ this.owner.chat.finishQuietly(
396
+ trace,
397
+ status === "error" ? "error" : "complete",
398
+ );
399
+ this.owner.end(id, status, reason, trace, summary);
400
+ this.owner.busy = false;
401
+ }
402
+ return { status, reason, runId: id };
403
+ }
404
+ chapters() {
405
+ return this.owner.mind.periods.chapters();
406
+ }
407
+ chapterVersions(chapter) {
408
+ return this.owner.mind.periods.chapterVersions(chapter);
409
+ }
410
+ isNight(now = this.owner.now()) {
411
+ if (this.owner.mind.nature.current(now).rhythm?.enabled)
412
+ return this.owner.phase(now).key === "asleep";
413
+ const hour = localClock(now, this.owner.mind.timeZone()).hour;
414
+ return hour >= 3 && hour < 6;
415
+ }
416
+ nightDue(now = this.owner.now()) {
417
+ if (!this.owner.settings().night || !this.owner.isNight(now)) return null;
418
+ try {
419
+ this.owner.profile();
420
+ } catch {
421
+ return null;
422
+ }
423
+ const day = this.owner.lifeDay(now);
424
+ if (
425
+ this.owner.mind.budget.allows("upkeep", now) &&
426
+ this.owner.repo.store.settings().memoryEnabled !== false
427
+ ) {
428
+ const done = this.owner.db.prepare(
429
+ "SELECT 1 FROM mind_runs WHERE kind='night' AND json_extract(summary,'$.day')=? AND json_extract(summary,'$.session')=? LIMIT 1",
430
+ );
431
+ for (const { id } of this.owner.living())
432
+ if (
433
+ !this.owner.mind.memory.busy.has(id) &&
434
+ this.owner.mind.memory.pending(id) >= NIGHT_PENDING &&
435
+ !done.get(day, id)
436
+ )
437
+ return { kind: "memory", session: id, day };
438
+ }
439
+ if (this.owner.reviewDue(now)) return { kind: "review" };
440
+ return null;
441
+ }
442
+ async rememberAtNight(session, day, now = this.owner.now()) {
443
+ this.owner.busy = true;
444
+ const name =
445
+ this.owner.living().find((s) => s.id === session)?.name || session;
446
+ const id = this.owner.run("night", `夜里整理「${name}」里的事`);
447
+ const trace = this.owner.repo.trace(session, "memory");
448
+ let status = "empty";
449
+ let reason = "没有需要整理的";
450
+ try {
451
+ const profile = this.owner.profile();
452
+ const models = withFallback(
453
+ this.owner.chat.models,
454
+ this.owner.chat.fallbackFor(null, profile, trace),
455
+ );
456
+ const before = this.owner.mind.memory.pending(session);
457
+ await this.owner.mind.memory.consolidate(
458
+ session,
459
+ profile,
460
+ prompts(this.owner.repo).memory,
461
+ trace,
462
+ { force: true, models, now },
463
+ );
464
+ const left = this.owner.mind.memory.pending(session);
465
+ if (left < before) {
466
+ status = "written";
467
+ reason = `夜里整理了「${name}」的 ${before - left} 条消息`;
468
+ }
469
+ } catch (error) {
470
+ status = "error";
471
+ reason = String(error.message).slice(0, 300);
472
+ trace.error = error.message;
473
+ } finally {
474
+ trace.reason = reason;
475
+ this.owner.chat.finishQuietly(
476
+ trace,
477
+ status === "error" ? "error" : "complete",
478
+ );
479
+ this.owner.end(id, status, reason, trace, { day, session });
480
+ this.owner.busy = false;
481
+ }
482
+ return { status, reason, runId: id };
483
+ }
484
+ reviewDue(now = this.owner.now()) {
485
+ const s = this.owner.settings();
486
+ if (!s.diary || !this.owner.mind.budget.allows("inner", now)) return false;
487
+ const last = this.owner.mind.periods.lastReview(now);
488
+ const written = this.owner.db
489
+ .prepare(
490
+ "SELECT COUNT(DISTINCT day) n FROM mind_diary WHERE created>? AND created<=?",
491
+ )
492
+ .get(last?.created ?? 0, now).n;
493
+ if (written < 2) return false;
494
+ const wait = this.#lifeMoved(now) ? DAY : s.chapterDays * DAY - 6 * HOUR;
495
+ if (last && now - last.created < wait) return false;
496
+ const tries = this.owner.db
497
+ .prepare(
498
+ "SELECT COUNT(*) n, MAX(started) last FROM mind_runs WHERE kind='weekly' AND status='error' AND started>=?",
499
+ )
500
+ .get(this.owner.dayStart(now));
501
+ return !(tries.n >= 3 || (tries.last && now - tries.last < 3 * HOUR));
502
+ }
503
+ async reviewPeriod(now = this.owner.now()) {
504
+ this.owner.busy = true;
505
+ const id = this.owner.run("weekly", "回顾这一段日子");
506
+ const trace = this.owner.repo.trace("__mind__", "weekly");
507
+ let status = "empty";
508
+ let reason = "这次没有写下回顾";
509
+ let summary = null;
510
+ try {
511
+ const nature = this.owner.mind.nature.current(now);
512
+ const periods = this.owner.mind.periods;
513
+ const last = periods.lastReview(now);
514
+ const start =
515
+ last?.period_end ??
516
+ this.owner.mind.days.born() ??
517
+ now - this.owner.settings().chapterDays * DAY;
518
+ const diaries = [
519
+ ...new Map(
520
+ this.owner.db
521
+ .prepare(
522
+ "SELECT * FROM mind_diary WHERE created>? AND created<=? ORDER BY day, created",
523
+ )
524
+ .all(last?.created ?? 0, now)
525
+ .map((d) => [d.day, d]),
526
+ ).values(),
527
+ ].slice(-10);
528
+ const first = diaries[0]?.day;
529
+ const earlier = first
530
+ ? this.owner.db
531
+ .prepare(
532
+ "SELECT day FROM mind_snapshots WHERE day<? ORDER BY day DESC LIMIT 1",
533
+ )
534
+ .get(first)?.day
535
+ : null;
536
+ const latest = this.owner.db
537
+ .prepare(
538
+ "SELECT day FROM mind_snapshots WHERE created<=? ORDER BY day DESC LIMIT 1",
539
+ )
540
+ .get(now)?.day;
541
+ const earlierSnap = earlier ? this.owner.mind.snapshotOf(earlier) : null;
542
+ const latestSnap = latest ? this.owner.mind.snapshotOf(latest) : null;
543
+ const asLived = (snap) =>
544
+ snap ? { ...snap, livingFor: this.owner.mind.livedThen(snap) } : null;
545
+ const change =
546
+ earlierSnap && latestSnap
547
+ ? diffSnapshots(asLived(earlierSnap), asLived(latestSnap))
548
+ : null;
549
+ const chapter = periods.current(now);
550
+ const previous = chapter
551
+ ? periods.chapters(now).find((c) => c.chapter === chapter.chapter - 1)
552
+ : null;
553
+ const story = periods.story(now);
554
+ const { kept, missed } = this.owner.mind.anticipations.today({
555
+ start,
556
+ end: now,
557
+ shareable: true,
558
+ });
559
+ const anniversaries = this.owner.mind.days.anniversaries(now);
560
+ const line = (t) =>
561
+ `${SELF_KINDS[t.kind] || t.kind}:${text(t.content, 60)}`;
562
+ const lastReview = last
563
+ ? this.#presentLifeWriting(last.content, now, 300)
564
+ : "";
565
+ const storyWords = story
566
+ ? this.#presentLifeWriting(story.content, now, 600)
567
+ : "";
568
+ const previousWords = previous
569
+ ? this.#presentLifeWriting(previous.content, now, 160)
570
+ : "";
571
+ const input = {
572
+ dayOfLife: this.owner.mind.days.dayOfLife(now),
573
+ period: { from: first, to: diaries.at(-1)?.day },
574
+ diaries: diaries.map((d) => this.owner.diaryLine(d, now)),
575
+ ...(last
576
+ ? {
577
+ lastReview: {
578
+ ...(lastReview ? { content: lastReview } : {}),
579
+ ...(last.compare ? { compare: text(last.compare, 120) } : {}),
580
+ },
581
+ }
582
+ : {}),
583
+ ...(change
584
+ ? {
585
+ changes: {
586
+ appeared: this.owner
587
+ .openThreads(change.appeared, now)
588
+ .slice(0, 6)
589
+ .map(line),
590
+ faded: this.owner
591
+ .openThreads(change.faded, now)
592
+ .slice(0, 6)
593
+ .map(line),
594
+ changed: this.owner
595
+ .openThreads(change.changed, now)
596
+ .slice(0, 6)
597
+ .map(
598
+ (t) =>
599
+ `${text(t.before.content, 40)} → ${text(t.content, 40)}`,
600
+ ),
601
+ people: change.people
602
+ .sort(
603
+ (a, b) => Math.abs(b.shift ?? 1) - Math.abs(a.shift ?? 1),
604
+ )
605
+ .slice(0, 5)
606
+ .map(
607
+ (p) =>
608
+ `${p.name}:${p.shift === null ? "新认识的" : p.shift > 0 ? "更近了" : "远了一些"}`,
609
+ ),
610
+ ...(change.livingFor
611
+ ? {
612
+ livingFor: `${text(change.livingFor.from, 40) || "还没有"} → ${text(change.livingFor.to, 40) || "还没有"}`,
613
+ }
614
+ : {}),
615
+ },
616
+ }
617
+ : {}),
618
+ ...(kept.length || missed.length
619
+ ? { ahead: { kept: kept.slice(0, 5), missed: missed.slice(0, 5) } }
620
+ : {}),
621
+ ...(chapter
622
+ ? {
623
+ chapter: {
624
+ number: chapter.chapter,
625
+ title: chapter.title,
626
+ content: this.#presentLifeWriting(chapter.content, now, 800),
627
+ reviews: periods.reviewsSince(
628
+ periods.began(chapter.chapter),
629
+ now,
630
+ ),
631
+ },
632
+ }
633
+ : {}),
634
+ ...(previous
635
+ ? {
636
+ previousChapter: {
637
+ number: previous.chapter,
638
+ title: previous.title,
639
+ ...(previousWords ? { summary: previousWords } : {}),
640
+ },
641
+ }
642
+ : {}),
643
+ ...(storyWords ? { story: storyWords } : {}),
644
+ ...(anniversaries.length ? { anniversaries } : {}),
645
+ self: this.owner.selfView(now, { open: true }).slice(0, 10),
646
+ ...(this.owner.livingForView(now, { since: start, open: true })
647
+ ? {
648
+ livingFor: this.owner.livingForView(now, {
649
+ since: start,
650
+ open: true,
651
+ }),
652
+ }
653
+ : {}),
654
+ };
655
+ const result = await this.owner.chat.models.call(
656
+ this.owner.profile(),
657
+ "weekly",
658
+ replyPrompt(nature, prompts(this.owner.repo), "weekly"),
659
+ input,
660
+ trace,
661
+ );
662
+ const week = text(result?.week, 1500);
663
+ if (!week || hasCredential(week)) throw SyntaxError("回顾格式无效");
664
+ const valid = new Set([
665
+ ...diaries.map((d) => `d:${d.day}`),
666
+ ...(input.livingFor ? [`s:${input.livingFor.thread}`] : []),
667
+ ]);
668
+ periods.write("week", {
669
+ start,
670
+ end: now,
671
+ content: week,
672
+ compare: result?.compare,
673
+ sources: [...valid],
674
+ runId: id,
675
+ time: now,
676
+ });
677
+ const applied = this.owner.grow(
678
+ { self: result.self, bonds: result.bonds },
679
+ valid,
680
+ "weekly",
681
+ now,
682
+ );
683
+ const turned = this.owner.turnChapter(result?.chapter, {
684
+ chapter,
685
+ start,
686
+ now,
687
+ runId: id,
688
+ });
689
+ const told = text(result?.story, 1800);
690
+ const prior = story?.content || "";
691
+ const living = input.livingFor?.content || "";
692
+ const livingMoved = Boolean(living) && !prior.includes(living);
693
+ const retold =
694
+ !!told &&
695
+ told !== prior &&
696
+ !hasCredential(told) &&
697
+ (turned.opened || !story || livingMoved);
698
+ if (retold)
699
+ periods.write("story", {
700
+ content: told,
701
+ sources: [...valid],
702
+ runId: id,
703
+ time: now,
704
+ });
705
+ summary = { chapter: turned.number, story: retold, ...applied };
706
+ status = "written";
707
+ reason =
708
+ turned.opened && chapter
709
+ ? `回顾了这段日子,翻开了第 ${turned.number} 章`
710
+ : turned.number
711
+ ? `回顾了这段日子,写下了第 ${turned.number} 章`
712
+ : "回顾了这段日子";
713
+ } catch (error) {
714
+ status = "error";
715
+ reason =
716
+ error instanceof SyntaxError
717
+ ? "回顾格式无效,未保存"
718
+ : String(error.message).slice(0, 300);
719
+ trace.error = error.message;
720
+ } finally {
721
+ trace.reason = reason;
722
+ this.owner.chat.finishQuietly(
723
+ trace,
724
+ status === "error" ? "error" : "complete",
725
+ );
726
+ this.owner.end(id, status, reason, trace, summary);
727
+ this.owner.busy = false;
728
+ }
729
+ return { status, reason, runId: id };
730
+ }
731
+ turnChapter(value, { chapter, start, now, runId }) {
732
+ const none = { number: null, opened: false };
733
+ if (!value || typeof value !== "object") return none;
734
+ if (!["continue", "close"].includes(value.action)) return none;
735
+ const title = text(value.title, 60);
736
+ const content = text(value.content, 2400);
737
+ if (!title || !content || hasCredential(content)) return none;
738
+ const periods = this.owner.mind.periods;
739
+ if (!chapter)
740
+ return {
741
+ number: periods.writeChapter({
742
+ number: 1,
743
+ title,
744
+ content,
745
+ start: this.owner.mind.days.born() ?? start,
746
+ end: now,
747
+ runId,
748
+ time: now,
749
+ }),
750
+ opened: true,
751
+ };
752
+ if (value.action === "continue")
753
+ return {
754
+ number: periods.writeChapter({
755
+ number: chapter.chapter,
756
+ title,
757
+ content,
758
+ start: periods.began(chapter.chapter) ?? chapter.period_start,
759
+ end: now,
760
+ runId,
761
+ time: now,
762
+ }),
763
+ opened: false,
764
+ };
765
+ return {
766
+ number: periods.writeChapter({
767
+ number: chapter.chapter + 1,
768
+ title,
769
+ content,
770
+ start: now,
771
+ end: now,
772
+ runId,
773
+ time: now,
774
+ }),
775
+ opened: true,
776
+ };
777
+ }
778
+ diaries({ before = "9999-12-31", limit = 14 } = {}) {
779
+ return this.owner.db
780
+ .prepare(
781
+ "SELECT * FROM mind_diary WHERE day<=? ORDER BY day DESC, created DESC LIMIT ?",
782
+ )
783
+ .all(before, limit)
784
+ .map((d) => ({ ...d, sources: parse(d.sources, []) }));
785
+ }
786
+ }