luckytri 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/en/CHANGELOG.md +44 -24
  4. package/docs/en/architecture.md +4 -0
  5. package/docs/en/cognition.md +34 -0
  6. package/docs/en/guide.md +8 -6
  7. package/docs/zh/CHANGELOG.md +44 -24
  8. package/docs/zh/architecture.md +4 -0
  9. package/docs/zh/cognition.md +34 -0
  10. package/docs/zh/guide.md +8 -6
  11. package/package.json +112 -112
  12. package/public/app/assets/ChatsPage-DH4ZPRXB.js +3 -0
  13. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-DRsHeX-L.js} +1 -1
  14. package/public/app/assets/Field-BfIUcaNa.js +1 -0
  15. package/public/app/assets/HeartPage-BxgSUJJ_.js +1 -0
  16. package/public/app/assets/LifePage-CAKr8h_q.js +1 -0
  17. package/public/app/assets/MemoryPage-CFq8L_TC.js +1 -0
  18. package/public/app/assets/NaturePage-BU1iDno3.js +3 -0
  19. package/public/app/assets/PeoplePage-C6i_nAfr.js +1 -0
  20. package/public/app/assets/PluginsPage-DRV8lzk3.js +2 -0
  21. package/public/app/assets/SystemPage-21AlQBiC.js +1 -0
  22. package/public/app/assets/SystemPage-CMcuUdLm.css +1 -0
  23. package/public/app/assets/TimePage-DUzjLnZV.js +3 -0
  24. package/public/app/assets/index-WAvrlAcO.js +4 -0
  25. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-V_blbx9R.js} +1 -1
  26. package/public/app/assets/media-C7j5HqxK.js +1 -0
  27. package/public/app/assets/{mind-DtTVrW6Z.js → mind-BBAstMqH.js} +1 -1
  28. package/public/app/index.html +3 -3
  29. package/public/guide.en.html +13 -5
  30. package/public/guide.html +12 -5
  31. package/public/tutorial.en.md +3 -1
  32. package/public/tutorial.md +3 -1
  33. package/server/core/backoff.js +123 -0
  34. package/server/core/context-builder.js +307 -288
  35. package/server/core/contribution-check.js +50 -0
  36. package/server/core/conversation-cues.js +377 -385
  37. package/server/core/conversation-grounding.js +547 -304
  38. package/server/core/dialogue-context.js +247 -244
  39. package/server/core/dialogue-contract.js +33 -9
  40. package/server/core/embedding.js +246 -0
  41. package/server/core/expression-strategy.js +90 -0
  42. package/server/core/initiative-context.js +88 -68
  43. package/server/core/model-json.js +223 -190
  44. package/server/core/model-manager.js +256 -56
  45. package/server/core/model-presets.js +1045 -900
  46. package/server/core/orchestrator.js +41 -3
  47. package/server/core/persona-manager.js +364 -359
  48. package/server/core/reader-check.js +166 -62
  49. package/server/core/reply-delivery.js +688 -537
  50. package/server/core/repository.js +36 -0
  51. package/server/core/response-generator.js +101 -92
  52. package/server/core/response-validator.js +18 -3
  53. package/server/core/retired-prompts.js +53 -51
  54. package/server/core/time-expressions.js +143 -0
  55. package/server/core/turn-processor.js +175 -5
  56. package/server/core/turn.js +299 -44
  57. package/server/core/understanding.js +85 -0
  58. package/server/core/working-memory.js +149 -0
  59. package/server/i18n/decisions.js +225 -209
  60. package/server/i18n/errors.js +14 -0
  61. package/server/i18n/models.js +120 -111
  62. package/server/knowledge/api.js +362 -342
  63. package/server/knowledge/embedding-profile.js +158 -114
  64. package/server/knowledge/manager.js +6 -1
  65. package/server/mind/api.js +68 -0
  66. package/server/mind/continuity.js +634 -562
  67. package/server/mind/day-planner.js +52 -3
  68. package/server/mind/episodes.js +242 -0
  69. package/server/mind/expression-grounding.js +24 -21
  70. package/server/mind/index.js +13 -2
  71. package/server/mind/initiative.js +356 -338
  72. package/server/mind/life/index.js +6 -0
  73. package/server/mind/meetings.js +1112 -1069
  74. package/server/mind/memory.js +1091 -888
  75. package/server/mind/recall/index.js +776 -0
  76. package/server/mind/recall/schema.js +132 -0
  77. package/server/mind/recall/sources.js +220 -0
  78. package/server/mind/recall/store.js +368 -0
  79. package/server/mind/salience.js +202 -201
  80. package/server/mind/schema.js +568 -567
  81. package/server/mind/self.js +537 -496
  82. package/server/mind/time/activity-recall.js +65 -0
  83. package/server/mind/time/index.js +6 -2
  84. package/server/mind/time/tasks.js +36 -10
  85. package/server/mind/time/works.js +528 -527
  86. package/server/mind/view.js +63 -17
  87. package/server/storage/archive.js +1 -1
  88. package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
  89. package/public/app/assets/Field-BjFv4Rfn.js +0 -1
  90. package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
  91. package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
  92. package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
  93. package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
  94. package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
  95. package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
  96. package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
  97. package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
  98. package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
  99. package/public/app/assets/index-CyRtYxP3.js +0 -4
  100. package/public/app/assets/media-Bwtx8blz.js +0 -1
@@ -9,7 +9,18 @@ import {
9
9
  } from "./time/availability.js";
10
10
  import { rhythmPhase } from "./nature.js";
11
11
  import { isExecutable, pluginAvailable, withActivities } from "./time/kinds.js";
12
+ import { providerWaitMs } from "../core/backoff.js";
12
13
  const MINUTE = 60000;
14
+ // A task that came back without a plan waits longer each time. Asking again
15
+ // every ten minutes spent hundreds of calls a day on the same answer.
16
+ export const DAY_PLAN_SCHEDULE = [
17
+ 10 * MINUTE,
18
+ 30 * MINUTE,
19
+ 2 * 60 * MINUTE,
20
+ 6 * 60 * MINUTE,
21
+ 12 * 60 * MINUTE,
22
+ ];
23
+ const planKey = (id) => `day-plan:${id}`;
13
24
 
14
25
  export function chosenSlot(value, life, now, requestedAt = now) {
15
26
  const at = value.startAt
@@ -126,7 +137,9 @@ export class DayPlanner {
126
137
  (t) =>
127
138
  (!primary || t.priority > primary.priority) &&
128
139
  this.needs(t, now) &&
129
- (state.requested || now - (attempts[t.id] || 0) >= 10 * MINUTE),
140
+ (state.requested ||
141
+ (now - (attempts[t.id] || 0) >= 10 * MINUTE &&
142
+ life.mind.backoff.ready(planKey(t.id), now))),
130
143
  )
131
144
  .sort((a, b) => time.tasks.score(b, now) - time.tasks.score(a, now));
132
145
  const task = tasks.find((t) => !unsupportedAction(t)) || tasks[0];
@@ -155,7 +168,8 @@ export class DayPlanner {
155
168
  trace = life.repo.trace("__mind__", "activity"),
156
169
  runId = life.run("day-plan", "想具体做什么、几点开始和做多久");
157
170
  let status = "error",
158
- reason = "";
171
+ reason = "",
172
+ failure = null;
159
173
  try {
160
174
  const projects = time.works
161
175
  .projects({ limit: 20 })
@@ -275,11 +289,34 @@ export class DayPlanner {
275
289
  const project = projects.find(
276
290
  (p) => p.id === answer.projectId && p.kind === activity,
277
291
  );
292
+ // The chosen time may already be taken. Move to the next free
293
+ // stretch she is awake for, instead of asking the model again.
294
+ const wanted = Math.max(slot.startAt, task.ready_at);
295
+ const startAt = time.tasks.freeStart(
296
+ task.id,
297
+ wanted,
298
+ slot.durationMinutes,
299
+ life.now(),
300
+ {
301
+ acceptable: (at) =>
302
+ rhythmPhase(
303
+ life.mind.nature.current(at),
304
+ at,
305
+ life.mind.timeZone(),
306
+ ).key !== "asleep",
307
+ },
308
+ );
309
+ if (startAt === null) throw Error("一周内没有空出的时间段");
310
+ if (startAt !== wanted)
311
+ slot.reason = text(
312
+ `${slot.reason}(原定时间已有安排,顺延到 ${localClock(startAt, life.mind.timeZone()).local})`,
313
+ 240,
314
+ );
278
315
  time.tasks.arrange(
279
316
  task.id,
280
317
  {
281
318
  ...slot,
282
- startAt: Math.max(slot.startAt, task.ready_at),
319
+ startAt,
283
320
  activity,
284
321
  title:
285
322
  task.kind === "promise" && task.activity !== "unknown"
@@ -296,8 +333,20 @@ export class DayPlanner {
296
333
  } catch (error) {
297
334
  reason = text(error.message, 160);
298
335
  trace.error = reason;
336
+ failure = error;
299
337
  return { status: "error", reason };
300
338
  } finally {
339
+ if (status === "planned") life.mind.backoff.succeed(planKey(task.id));
340
+ else if (["waiting", "resting", "error"].includes(status))
341
+ life.mind.backoff.fail(
342
+ planKey(task.id),
343
+ failure || reason || status,
344
+ life.now(),
345
+ {
346
+ schedule: DAY_PLAN_SCHEDULE,
347
+ minimumMs: providerWaitMs(failure),
348
+ },
349
+ );
301
350
  this.working = false;
302
351
  life.chat.finishQuietly(
303
352
  trace,
@@ -0,0 +1,242 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { applySpeakerNames } from "../core/speaker-names.js";
3
+ import { elapsedLabel } from "./clock.js";
4
+ import { secretRequest } from "./guard.js";
5
+ import { isPrivateSession } from "./memory.js";
6
+ import { grounded } from "./salience.js";
7
+ import { hasCredential, parse, text, zonedTime } from "./util.js";
8
+
9
+ export const EPISODE_STATUS = new Set(["open", "resolved", "abandoned"]);
10
+ const RELATIONS = new Set(["continues", "resolves"]);
11
+
12
+ // An affair from its start to how it stands now: an interview that was
13
+ // coming, then happened, then had a result. Each step is its own row in the
14
+ // same thread, citing the messages it rests on, so the past is never
15
+ // rewritten and recall can tell what happened, what changed and what is
16
+ // still open.
17
+ export class Episodes {
18
+ constructor(mind) {
19
+ this.mind = mind;
20
+ this.db = mind.db;
21
+ }
22
+ get(id) {
23
+ return this.db.prepare("SELECT * FROM mind_episodes WHERE id=?").get(id);
24
+ }
25
+ thread(threadId, before = Number.MAX_SAFE_INTEGER) {
26
+ return this.db
27
+ .prepare(
28
+ "SELECT * FROM mind_episodes WHERE thread_id=? AND created<=? ORDER BY created,rowid",
29
+ )
30
+ .all(threadId, before);
31
+ }
32
+ latest(threadId, before = Number.MAX_SAFE_INTEGER) {
33
+ return this.thread(threadId, before).at(-1) || null;
34
+ }
35
+ visible(row, session) {
36
+ if (row.discretion === "open") return true;
37
+ return this.mind.memory.scopes(session).includes(row.session_id);
38
+ }
39
+ // Threads still open that concern these people or this room, newest
40
+ // first, as seen at `now`.
41
+ open({ session, people = [], now = Date.now(), limit = 6 } = {}) {
42
+ const rows = this.db
43
+ .prepare(
44
+ `SELECT e.* FROM mind_episodes e JOIN (
45
+ SELECT thread_id,MAX(created) last FROM mind_episodes WHERE created<=? GROUP BY thread_id
46
+ ) l ON l.thread_id=e.thread_id AND l.last=e.created
47
+ WHERE e.status='open' ORDER BY e.created DESC LIMIT 200`,
48
+ )
49
+ .all(now);
50
+ const who = new Set(people.map(String));
51
+ return rows
52
+ .filter((row) => this.visible(row, session))
53
+ .filter(
54
+ (row) =>
55
+ row.session_id === session ||
56
+ parse(row.participants, []).some((id) => who.has(String(id))),
57
+ )
58
+ .slice(0, limit);
59
+ }
60
+ due(now = Date.now(), limit = 4) {
61
+ return this.db
62
+ .prepare(
63
+ `SELECT e.* FROM mind_episodes e JOIN (
64
+ SELECT thread_id,MAX(created) last FROM mind_episodes WHERE created<=? GROUP BY thread_id
65
+ ) l ON l.thread_id=e.thread_id AND l.last=e.created
66
+ WHERE e.status='open' AND e.follow_up_at>0 AND e.follow_up_at<=?
67
+ ORDER BY e.follow_up_at LIMIT ?`,
68
+ )
69
+ .all(now, now, limit);
70
+ }
71
+ // Open threads offered to the consolidation model so a new step can
72
+ // continue or resolve one instead of starting a duplicate.
73
+ candidates(session, block, now = Date.now()) {
74
+ const people = [
75
+ ...new Set(
76
+ block.filter((m) => m.role === "user").map((m) => String(m.userId)),
77
+ ),
78
+ ];
79
+ const refs = new Map();
80
+ const list = this.open({ session, people, now, limit: 8 }).map((row, i) => {
81
+ const ref = `e${i + 1}`;
82
+ refs.set(ref, row);
83
+ return {
84
+ ref,
85
+ title: row.title,
86
+ sofar: text(row.outcome || row.content, 120),
87
+ since: elapsedLabel(
88
+ row.happened_start || row.created,
89
+ now,
90
+ this.mind.timeZone(),
91
+ ),
92
+ };
93
+ });
94
+ return { list, refs };
95
+ }
96
+ // Writes what the consolidation model reported, keeping only steps whose
97
+ // words were actually said in the cited messages of this stretch.
98
+ record(
99
+ list,
100
+ { block, session, refs = new Map(), names, privateChat, now, runId = null },
101
+ ) {
102
+ const written = [];
103
+ const bySeq = new Map(block.map((m) => [m.seq, m]));
104
+ for (const raw of (Array.isArray(list) ? list : []).slice(0, 6)) {
105
+ if (
106
+ !raw ||
107
+ typeof raw.title !== "string" ||
108
+ typeof raw.content !== "string"
109
+ )
110
+ continue;
111
+ const cited = (Array.isArray(raw.sources) ? raw.sources : [])
112
+ .map(Number)
113
+ .map((seq) => bySeq.get(seq))
114
+ .filter(Boolean);
115
+ if (!cited.length || cited.some((m) => m.artifact)) continue;
116
+ const words = cited.map((m) => m.text || "");
117
+ const content = text(raw.content, 300);
118
+ const outcome = text(raw.outcome, 160);
119
+ if (
120
+ hasCredential(`${raw.title}${content}${outcome}`) ||
121
+ !grounded(content, words) ||
122
+ (outcome && !grounded(outcome, words))
123
+ )
124
+ continue;
125
+ const status = EPISODE_STATUS.has(raw.status) ? raw.status : "open";
126
+ const relation = RELATIONS.has(raw.relation)
127
+ ? raw.relation
128
+ : status === "resolved"
129
+ ? "resolves"
130
+ : "continues";
131
+ const previous = raw.continues ? refs.get(String(raw.continues)) : null;
132
+ const speakers = new Set(
133
+ cited.filter((m) => m.role === "user").map((m) => String(m.userId)),
134
+ );
135
+ const participants = [
136
+ ...new Set([
137
+ ...speakers,
138
+ ...(Array.isArray(raw.participants) ? raw.participants : [])
139
+ .map(String)
140
+ .filter((id) => block.some((m) => String(m.userId) === id)),
141
+ ]),
142
+ ];
143
+ const quiet = block.some(
144
+ (m) =>
145
+ m.role === "user" &&
146
+ m.seq <= Math.max(...cited.map((c) => c.seq)) &&
147
+ secretRequest(m.text || ""),
148
+ );
149
+ const discretion =
150
+ quiet || previous?.discretion === "secret"
151
+ ? "secret"
152
+ : privateChat || previous?.discretion === "private"
153
+ ? "private"
154
+ : "open";
155
+ const follow = raw.followUp
156
+ ? zonedTime(String(raw.followUp), this.mind.timeZone())
157
+ : null;
158
+ const id = randomUUID();
159
+ const thread = previous?.thread_id || id;
160
+ this.db
161
+ .prepare(
162
+ `INSERT INTO mind_episodes(id,thread_id,created,session_id,title,content,outcome,status,participants,
163
+ happened_start,happened_end,follow_up_at,basis,importance,discretion,sources,run_id)
164
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
165
+ )
166
+ .run(
167
+ id,
168
+ thread,
169
+ now,
170
+ previous?.session_id && previous.discretion !== "open"
171
+ ? previous.session_id
172
+ : session,
173
+ text(applySpeakerNames(raw.title, names), 40),
174
+ applySpeakerNames(content, names),
175
+ applySpeakerNames(outcome, names),
176
+ status,
177
+ JSON.stringify(participants),
178
+ previous?.happened_start || Math.min(...cited.map((m) => m.time)),
179
+ status === "open" ? null : Math.max(...cited.map((m) => m.time)),
180
+ status === "open" && Number.isFinite(follow) && follow > now
181
+ ? follow
182
+ : null,
183
+ raw.basis === "told" ? "stated" : "witnessed",
184
+ Math.min(1, Math.max(0, Number(raw.importance) || 0.5)),
185
+ discretion,
186
+ JSON.stringify(cited.map((m) => m.seq)),
187
+ runId,
188
+ );
189
+ if (previous)
190
+ this.db
191
+ .prepare(
192
+ "INSERT OR IGNORE INTO mind_links(from_id,to_id,relation,created,sources) VALUES (?,?,?,?,?)",
193
+ )
194
+ .run(
195
+ `episode:${id}`,
196
+ `episode:${previous.id}`,
197
+ relation,
198
+ now,
199
+ JSON.stringify(cited.map((m) => m.seq)),
200
+ );
201
+ this.mind.recall?.sync("episode", id);
202
+ written.push(id);
203
+ }
204
+ return written;
205
+ }
206
+ // One remembered affair, told as it went and as it stands: the beginning,
207
+ // what changed and where it is now. A private thread stays private.
208
+ recollection(row, session, now = Date.now()) {
209
+ const steps = this.thread(row.thread_id, now);
210
+ const last = steps.at(-1) || row;
211
+ const first = steps[0] || row;
212
+ const zone = this.mind.timeZone();
213
+ const local = this.mind.memory.scopes(session).includes(last.session_id);
214
+ return {
215
+ kind: "episode",
216
+ title: last.title,
217
+ began: elapsedLabel(first.happened_start || first.created, now, zone),
218
+ ...(steps.length > 1
219
+ ? {
220
+ sofar: steps
221
+ .slice(0, -1)
222
+ .slice(-2)
223
+ .map((s) => text(s.outcome || s.content, 60)),
224
+ }
225
+ : {}),
226
+ now: text(last.outcome || last.content, 120),
227
+ status:
228
+ last.status === "open"
229
+ ? last.follow_up_at && last.follow_up_at <= now
230
+ ? "还没结果,说好的时间已经过了"
231
+ : "还没结束"
232
+ : last.status === "resolved"
233
+ ? "已经有结果"
234
+ : "后来放下了",
235
+ updated: elapsedLabel(last.created, now, zone),
236
+ ...(last.basis === "stated" ? { basis: "对方告诉你的" } : {}),
237
+ ...(!local && isPrivateSession(last.session_id)
238
+ ? { discretion: "私下知道的,不要当众说出口,也不要说出从哪听来" }
239
+ : {}),
240
+ };
241
+ }
242
+ }
@@ -1,21 +1,24 @@
1
- export function claimsLivedAction(content) {
2
- return String(content || "")
3
- .split(/[。!?\n]/)
4
- .some(
5
- (clause) =>
6
- !/想|打算|准备|希望|假如|设想|如果/.test(clause) &&
7
- /(?:刚才|刚刚|今天|这次|然后|真的|已经)[^。!?]{0,25}(?:读了|读上|读完|翻开|翻出来|写完|完成了|打开了|开始读|动手了)/.test(
8
- clause,
9
- ),
10
- );
11
- }
12
-
13
- export function claimsSharedHistory(content) {
14
- return /(?:之前|上次|早些时候)[^。!?\n]{0,25}(?:他|她|对方|那个人|你)[^。!?\n]{0,20}(?:说过|聊过|看过|读过|回话|私下)|(?:他|她|对方|那个人|你)[^。!?\n]{0,15}(?:之前|上次|读过|看过|说过)/.test(
15
- String(content || ""),
16
- );
17
- }
18
-
19
- export const EXPRESSION_GROUNDING = `核对一个准备保存的自我念头是否把愿望写成行动,或把别人的话题误认成共同经历。只输出JSON。
20
- candidate只是草稿,不是事实。notes/self是过去的想法、愿望或理解;它们可以支持新的感受和看法,不能单独证明这次真的开始、读完或做完。actions/currentLife是实际活动记录。sources只表示引用了哪里,不自动证明动作。按实际记录核对这次声称新做过的具体动作和对象;不能用无关作品或一条待办证明完成。声称共同经历时,还须核对实际参与者和各自说过的话;在别处读到另一人的讨论不证明当前对象参与过。自己的新想法、偏好、假设、打算可以没有执行记录,也不需要已采取外部行动。只指出确实无依据的新经历与归属,不评文风。
21
- 输出 {"ok":true或false,"reason":"具体哪项新经历有或没有实际记录"}。`;
1
+ export function claimsLivedAction(content) {
2
+ return String(content || "")
3
+ .split(/[。!?\n]/)
4
+ .some(
5
+ (clause) =>
6
+ !/想|打算|准备|希望|假如|设想|如果/.test(clause) &&
7
+ (/(?:刚才|刚刚|今天|这次|然后|真的|已经)[^。!?]{0,25}(?:读了|读上|读完|读[一二两三几\d]+[段页章节遍]|坐下来读|翻开|翻出来|写完|完成了|打开了|开始读|动手了)/.test(
8
+ clause,
9
+ ) ||
10
+ /我(?:自己)?[^。!?]{0,10}(?:读上了|开始读了|翻开了|完成了|写完了)/.test(
11
+ clause,
12
+ )),
13
+ );
14
+ }
15
+
16
+ export function claimsSharedHistory(content) {
17
+ return /(?:之前|上次|早些时候)[^。!?\n]{0,25}(?:他|她|对方|那个人|你)[^。!?\n]{0,20}(?:说过|聊过|看过|读过|回话|私下)|(?:他|她|对方|那个人|你)[^。!?\n]{0,15}(?:之前|上次|读过|看过|说过)/.test(
18
+ String(content || ""),
19
+ );
20
+ }
21
+
22
+ export const EXPRESSION_GROUNDING = `核对一个准备保存的自我念头是否把愿望写成行动,或把别人的话题误认成共同经历。只输出JSON。
23
+ candidate只是草稿,不是事实。notes/self是过去的想法、愿望或理解;它们可以支持新的感受和看法,不能单独证明这次真的开始、读完或做完。actions/currentLife是实际活动记录。sources只表示引用了哪里,不自动证明动作。按实际记录核对这次声称新做过的具体动作和对象;不能用无关作品或一条待办证明完成。声称共同经历时,还须核对实际参与者和各自说过的话;在别处读到另一人的讨论不证明当前对象参与过。自己的新想法、偏好、假设、打算可以没有执行记录,也不需要已采取外部行动。只指出确实无依据的新经历与归属,不评文风。
24
+ 输出 {"ok":true或false,"reason":"具体哪项新经历有或没有实际记录"}。`;
@@ -22,6 +22,10 @@ import { innerView } from "./view.js";
22
22
  import { Senses } from "./senses.js";
23
23
  import { Observations } from "./observations.js";
24
24
  import { clamp, dayKey, parse, text } from "./util.js";
25
+ import { Backoff } from "../core/backoff.js";
26
+ import { EmbeddingService } from "../core/embedding.js";
27
+ import { Recall } from "./recall/index.js";
28
+ import { Episodes } from "./episodes.js";
25
29
 
26
30
  // The wish a day's snapshot says she was living. Older snapshots did not
27
31
  // store it; those fall back to her own-life threads from that day.
@@ -86,11 +90,13 @@ const TURN_CHANGES = new Set(
86
90
  // One of her, across every conversation. Sessions only say where something
87
91
  // happened; they are no longer walls between parts of her.
88
92
  export class Mind {
89
- constructor(repo, { models } = {}) {
93
+ constructor(repo, { models, now = () => Date.now() } = {}) {
90
94
  this.repo = repo;
91
95
  this.db = repo.db;
92
96
  this.store = repo.store;
93
97
  migrateMind(this.db, this.store);
98
+ this.backoff = new Backoff(this.db);
99
+ this.embedding = new EmbeddingService(repo, models, { now });
94
100
  this.db.exec(
95
101
  "CREATE INDEX IF NOT EXISTS core_events_time ON core_events(time)",
96
102
  );
@@ -113,6 +119,8 @@ export class Mind {
113
119
  this.time = new TimeSystem(this);
114
120
  this.senses = new Senses();
115
121
  this.observations = new Observations(this);
122
+ this.episodes = new Episodes(this);
123
+ this.recall = new Recall(this);
116
124
  this.body = () => [];
117
125
  this.days.reconcile();
118
126
  }
@@ -120,7 +128,7 @@ export class Mind {
120
128
  return this.repo.config("life", {})?.timeZone || "Asia/Shanghai";
121
129
  }
122
130
  view(options) {
123
- return innerView(this, options);
131
+ return this.meetings.memo(() => innerView(this, options));
124
132
  }
125
133
  // The whole system is one body she can read: what is connected, and
126
134
  // whether each plugin is on. The studio shows the same state.
@@ -361,6 +369,9 @@ export class Mind {
361
369
  } else if (kind === "observation") {
362
370
  tomb(this.observations.revoke(id));
363
371
  } else throw Error("不能撤销这类内容");
372
+ // A withdrawn memory is no longer searchable either.
373
+ if (["self", "meeting", "thought", "anticipation"].includes(kind))
374
+ this.recall.sync(kind, id);
364
375
  this.store.revision++;
365
376
  }
366
377
  revocations(limit = 100) {