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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/CHANGELOG.md +44 -24
- package/docs/en/architecture.md +4 -0
- package/docs/en/cognition.md +34 -0
- package/docs/en/guide.md +8 -6
- package/docs/zh/CHANGELOG.md +44 -24
- package/docs/zh/architecture.md +4 -0
- package/docs/zh/cognition.md +34 -0
- package/docs/zh/guide.md +8 -6
- package/package.json +112 -112
- package/public/app/assets/ChatsPage-DH4ZPRXB.js +3 -0
- package/public/app/assets/{Empty-C7yzOBzy.js → Empty-DRsHeX-L.js} +1 -1
- package/public/app/assets/Field-BfIUcaNa.js +1 -0
- package/public/app/assets/HeartPage-BxgSUJJ_.js +1 -0
- package/public/app/assets/LifePage-CAKr8h_q.js +1 -0
- package/public/app/assets/MemoryPage-CFq8L_TC.js +1 -0
- package/public/app/assets/NaturePage-BU1iDno3.js +3 -0
- package/public/app/assets/PeoplePage-C6i_nAfr.js +1 -0
- package/public/app/assets/PluginsPage-DRV8lzk3.js +2 -0
- package/public/app/assets/SystemPage-21AlQBiC.js +1 -0
- package/public/app/assets/SystemPage-CMcuUdLm.css +1 -0
- package/public/app/assets/TimePage-DUzjLnZV.js +3 -0
- package/public/app/assets/index-WAvrlAcO.js +4 -0
- package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-V_blbx9R.js} +1 -1
- package/public/app/assets/media-C7j5HqxK.js +1 -0
- package/public/app/assets/{mind-DtTVrW6Z.js → mind-BBAstMqH.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +13 -5
- package/public/guide.html +12 -5
- package/public/tutorial.en.md +3 -1
- package/public/tutorial.md +3 -1
- package/server/core/backoff.js +123 -0
- package/server/core/context-builder.js +307 -288
- package/server/core/contribution-check.js +50 -0
- package/server/core/conversation-cues.js +377 -385
- package/server/core/conversation-grounding.js +547 -304
- package/server/core/dialogue-context.js +247 -244
- package/server/core/dialogue-contract.js +33 -9
- package/server/core/embedding.js +246 -0
- package/server/core/expression-strategy.js +90 -0
- package/server/core/initiative-context.js +88 -68
- package/server/core/model-json.js +223 -190
- package/server/core/model-manager.js +256 -56
- package/server/core/model-presets.js +1045 -900
- package/server/core/orchestrator.js +41 -3
- package/server/core/persona-manager.js +364 -359
- package/server/core/reader-check.js +166 -62
- package/server/core/reply-delivery.js +688 -537
- package/server/core/repository.js +36 -0
- package/server/core/response-generator.js +101 -92
- package/server/core/response-validator.js +18 -3
- package/server/core/retired-prompts.js +53 -51
- package/server/core/time-expressions.js +143 -0
- package/server/core/turn-processor.js +175 -5
- package/server/core/turn.js +299 -44
- package/server/core/understanding.js +85 -0
- package/server/core/working-memory.js +149 -0
- package/server/i18n/decisions.js +225 -209
- package/server/i18n/errors.js +14 -0
- package/server/i18n/models.js +120 -111
- package/server/knowledge/api.js +362 -342
- package/server/knowledge/embedding-profile.js +158 -114
- package/server/knowledge/manager.js +6 -1
- package/server/mind/api.js +68 -0
- package/server/mind/continuity.js +634 -562
- package/server/mind/day-planner.js +52 -3
- package/server/mind/episodes.js +242 -0
- package/server/mind/expression-grounding.js +24 -21
- package/server/mind/index.js +13 -2
- package/server/mind/initiative.js +356 -338
- package/server/mind/life/index.js +6 -0
- package/server/mind/meetings.js +1112 -1069
- package/server/mind/memory.js +1091 -888
- package/server/mind/recall/index.js +776 -0
- package/server/mind/recall/schema.js +132 -0
- package/server/mind/recall/sources.js +220 -0
- package/server/mind/recall/store.js +368 -0
- package/server/mind/salience.js +202 -201
- package/server/mind/schema.js +568 -567
- package/server/mind/self.js +537 -496
- package/server/mind/time/activity-recall.js +65 -0
- package/server/mind/time/index.js +6 -2
- package/server/mind/time/tasks.js +36 -10
- package/server/mind/time/works.js +528 -527
- package/server/mind/view.js +63 -17
- package/server/storage/archive.js +1 -1
- package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
- package/public/app/assets/Field-BjFv4Rfn.js +0 -1
- package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
- package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
- package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
- package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
- package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
- package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
- package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
- package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
- package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
- package/public/app/assets/index-CyRtYxP3.js +0 -4
- package/public/app/assets/media-Bwtx8blz.js +0 -1
package/server/mind/memory.js
CHANGED
|
@@ -1,888 +1,1091 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
parseSessionKey,
|
|
4
|
-
scopedSessionAliases,
|
|
5
|
-
} from "../channels/session-key.js";
|
|
6
|
-
import { localClock } from "../core/conversation-cues.js";
|
|
7
|
-
import { lexicalTerms } from "../knowledge/retrieval.js";
|
|
8
|
-
import { indexMemory } from "../knowledge/schema.js";
|
|
9
|
-
import {
|
|
10
|
-
applySpeakerNames,
|
|
11
|
-
readableName,
|
|
12
|
-
speakerNames,
|
|
13
|
-
} from "../core/speaker-names.js";
|
|
14
|
-
import { agoLabel } from "./clock.js";
|
|
15
|
-
import { hasCredential, secretRequest } from "./guard.js";
|
|
16
|
-
import { MEMORY_IDLE_DAYS, grounded } from "./salience.js";
|
|
17
|
-
import { DAY, clamp, similar, text } from "./util.js";
|
|
18
|
-
import { recallIntent } from "./continuity.js";
|
|
19
|
-
import { consolidationDecision } from "./memory-policy.js";
|
|
20
|
-
import { requestedName } from "../core/person-name.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
m.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
).
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
this.repo.db
|
|
621
|
-
.prepare(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
)) {
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
parseSessionKey,
|
|
4
|
+
scopedSessionAliases,
|
|
5
|
+
} from "../channels/session-key.js";
|
|
6
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
7
|
+
import { lexicalTerms } from "../knowledge/retrieval.js";
|
|
8
|
+
import { indexMemory } from "../knowledge/schema.js";
|
|
9
|
+
import {
|
|
10
|
+
applySpeakerNames,
|
|
11
|
+
readableName,
|
|
12
|
+
speakerNames,
|
|
13
|
+
} from "../core/speaker-names.js";
|
|
14
|
+
import { agoLabel } from "./clock.js";
|
|
15
|
+
import { hasCredential, secretRequest } from "./guard.js";
|
|
16
|
+
import { MEMORY_IDLE_DAYS, grounded } from "./salience.js";
|
|
17
|
+
import { DAY, clamp, parse, similar, text } from "./util.js";
|
|
18
|
+
import { recallIntent } from "./continuity.js";
|
|
19
|
+
import { consolidationDecision } from "./memory-policy.js";
|
|
20
|
+
import { requestedName } from "../core/person-name.js";
|
|
21
|
+
import { isFormatFailure, providerWaitMs } from "../core/backoff.js";
|
|
22
|
+
|
|
23
|
+
const RECALL_CONFIDENCE = 0.5;
|
|
24
|
+
const RECALL_LIMIT = 12;
|
|
25
|
+
const RECALL_TERMS = 64;
|
|
26
|
+
const FTS_CANDIDATES = 200;
|
|
27
|
+
const BLOCK_USERS = 40;
|
|
28
|
+
const MIN_BLOCK_USERS = 10;
|
|
29
|
+
// One stretch of talk adds at most this many views to who she is.
|
|
30
|
+
const MEMORY_VIEWS_PER_BLOCK = 2;
|
|
31
|
+
const SENSITIVE = /密码|验证码|密钥|身份证|银行卡|api.?key|token/i;
|
|
32
|
+
const DISCRETIONS = new Set(["open", "private", "secret"]);
|
|
33
|
+
|
|
34
|
+
// A request to keep something quiet stays with that person for the rest of
|
|
35
|
+
// this stretch. Other people's messages in between do not wash it out, and
|
|
36
|
+
// it does not make anyone else's words a secret.
|
|
37
|
+
function askedSecret(block, subject, beforeSeq) {
|
|
38
|
+
if (!subject) return false;
|
|
39
|
+
return block.some(
|
|
40
|
+
(m) =>
|
|
41
|
+
m.role === "user" &&
|
|
42
|
+
String(m.userId) === String(subject) &&
|
|
43
|
+
m.seq <= beforeSeq &&
|
|
44
|
+
secretRequest(m.text),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Words from the newest messages first, so a long batch still searches for
|
|
49
|
+
// what was just said.
|
|
50
|
+
function recallQuery(rows) {
|
|
51
|
+
const terms = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const row of [...rows].reverse()) {
|
|
54
|
+
if (row.role === "assistant") continue;
|
|
55
|
+
for (const term of lexicalTerms(row.text || ""))
|
|
56
|
+
if (!seen.has(term)) {
|
|
57
|
+
seen.add(term);
|
|
58
|
+
terms.push(term);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return terms
|
|
62
|
+
.slice(0, RECALL_TERMS)
|
|
63
|
+
.map((term) => `"${term.replace(/"/g, "")}"`)
|
|
64
|
+
.filter((term) => term.length > 2)
|
|
65
|
+
.join(" OR ");
|
|
66
|
+
}
|
|
67
|
+
const marks = (list) => list.map(() => "?").join(",");
|
|
68
|
+
|
|
69
|
+
// How a fact was learned, from the certainty its sources carry.
|
|
70
|
+
function basisOf(sources) {
|
|
71
|
+
const certainty = (Array.isArray(sources) ? sources : []).map(
|
|
72
|
+
(s) => s?.certainty,
|
|
73
|
+
);
|
|
74
|
+
if (certainty.includes("inferred")) return "inferred";
|
|
75
|
+
if (certainty.includes("hearsay")) return "hearsay";
|
|
76
|
+
return "stated";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isPrivateSession(session) {
|
|
80
|
+
const value = String(session || "");
|
|
81
|
+
if (value.startsWith("__private__")) return true;
|
|
82
|
+
try {
|
|
83
|
+
return parseSessionKey(value).kind === "private";
|
|
84
|
+
} catch {
|
|
85
|
+
return value.startsWith("private");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// One memory for all of her conversations. Where a fact came from travels
|
|
90
|
+
// with it, and so does how freely she may repeat it.
|
|
91
|
+
export class MemoryManager {
|
|
92
|
+
constructor(repo, models, mind) {
|
|
93
|
+
this.repo = repo;
|
|
94
|
+
this.models = models;
|
|
95
|
+
this.mind = mind;
|
|
96
|
+
this.busy = new Set();
|
|
97
|
+
this.lastAttempt = new Map();
|
|
98
|
+
}
|
|
99
|
+
scopes(session) {
|
|
100
|
+
const scopes = new Set([session, "__shared__"]);
|
|
101
|
+
try {
|
|
102
|
+
for (const id of scopedSessionAliases(this.repo.db, session))
|
|
103
|
+
scopes.add(id);
|
|
104
|
+
const parsed = parseSessionKey(session);
|
|
105
|
+
if (parsed.kind === "private")
|
|
106
|
+
scopes.add(`__private__:${parsed.nativeId}`);
|
|
107
|
+
} catch {
|
|
108
|
+
/* keep session + shared */
|
|
109
|
+
}
|
|
110
|
+
return [...scopes];
|
|
111
|
+
}
|
|
112
|
+
retrieve(
|
|
113
|
+
session,
|
|
114
|
+
rows,
|
|
115
|
+
cutoff = Date.now(),
|
|
116
|
+
{ people = [], touch = true, forSpeech = false } = {},
|
|
117
|
+
) {
|
|
118
|
+
const db = this.repo.db;
|
|
119
|
+
const here = new Set(this.scopes(session));
|
|
120
|
+
here.delete("__shared__");
|
|
121
|
+
const speakers = new Set(
|
|
122
|
+
rows
|
|
123
|
+
.filter((r) => r.role !== "assistant")
|
|
124
|
+
.map((r) => String(r.userId || ""))
|
|
125
|
+
.filter(Boolean),
|
|
126
|
+
);
|
|
127
|
+
const present = new Set(people.map(String));
|
|
128
|
+
// Two people cannot be added together, and her own line does not count
|
|
129
|
+
// as the world talking about what she remembers.
|
|
130
|
+
const saidBy = new Map();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (row.role === "assistant") continue;
|
|
133
|
+
const id = String(row.userId || "");
|
|
134
|
+
if (!id) continue;
|
|
135
|
+
const terms = saidBy.get(id) || new Set();
|
|
136
|
+
for (const term of lexicalTerms(row.text || "")) terms.add(term);
|
|
137
|
+
saidBy.set(id, terms);
|
|
138
|
+
}
|
|
139
|
+
const oneSpeakerOverlap = (content) => {
|
|
140
|
+
const terms = lexicalTerms(content);
|
|
141
|
+
let best = 0;
|
|
142
|
+
for (const said of saidBy.values()) {
|
|
143
|
+
let count = 0;
|
|
144
|
+
for (const term of terms) if (said.has(term)) count++;
|
|
145
|
+
if (count > best) best = count;
|
|
146
|
+
}
|
|
147
|
+
return best;
|
|
148
|
+
};
|
|
149
|
+
const fts = new Set();
|
|
150
|
+
const match = recallQuery(rows);
|
|
151
|
+
if (match)
|
|
152
|
+
try {
|
|
153
|
+
for (const hit of db
|
|
154
|
+
.prepare(
|
|
155
|
+
"SELECT memory_id FROM core_memory_fts WHERE tokens MATCH ? ORDER BY rank LIMIT ?",
|
|
156
|
+
)
|
|
157
|
+
.all(match, FTS_CANDIDATES))
|
|
158
|
+
fts.add(hit.memory_id);
|
|
159
|
+
} catch {
|
|
160
|
+
/* MATCH syntax */
|
|
161
|
+
}
|
|
162
|
+
// Only what could matter is read: words in common, people here, or what
|
|
163
|
+
// was locked to stay in mind.
|
|
164
|
+
const ids = [...fts];
|
|
165
|
+
const subjects = [...new Set([...speakers, ...present])].filter(Boolean);
|
|
166
|
+
const candidates = db
|
|
167
|
+
.prepare(
|
|
168
|
+
`SELECT * FROM core_memories WHERE status='confirmed' AND created<=? AND (expires IS NULL OR expires>?) AND COALESCE(confidence,1)>=? AND (locked=1${ids.length ? ` OR id IN (${marks(ids)})` : ""}${subjects.length ? ` OR subject IN (${marks(subjects)})` : ""})`,
|
|
169
|
+
)
|
|
170
|
+
.all(cutoff, cutoff, RECALL_CONFIDENCE, ...ids, ...subjects);
|
|
171
|
+
const lived = this.mind?.days.lived(cutoff);
|
|
172
|
+
const scored = [];
|
|
173
|
+
for (const m of candidates) {
|
|
174
|
+
const local = here.has(m.session_id) || m.session_id === "__shared__";
|
|
175
|
+
if (!this.permission(m, session, cutoff).recall) continue;
|
|
176
|
+
const overlap = oneSpeakerOverlap(m.content);
|
|
177
|
+
const relevance = overlap + (fts.has(m.id) ? 2 : 0);
|
|
178
|
+
const about = speakers.has(m.subject)
|
|
179
|
+
? 3
|
|
180
|
+
: present.has(m.subject)
|
|
181
|
+
? 1.5
|
|
182
|
+
: 0;
|
|
183
|
+
if (!relevance && !about && !m.locked) continue;
|
|
184
|
+
if (!local && !relevance && about < 3) continue;
|
|
185
|
+
// Something long untouched comes back only when it is really being
|
|
186
|
+
// talked about, or its person is speaking.
|
|
187
|
+
const idle = lived
|
|
188
|
+
? lived.since(Math.max(m.created || 0, m.last_access || 0))
|
|
189
|
+
: 0;
|
|
190
|
+
const strong =
|
|
191
|
+
overlap >= 2 || speakers.has(String(m.subject)) || !!m.locked;
|
|
192
|
+
if (!strong && Number(m.importance || 0) < 0.6 && idle > MEMORY_IDLE_DAYS)
|
|
193
|
+
continue;
|
|
194
|
+
scored.push({
|
|
195
|
+
m,
|
|
196
|
+
local,
|
|
197
|
+
strong,
|
|
198
|
+
score:
|
|
199
|
+
relevance +
|
|
200
|
+
about +
|
|
201
|
+
(local ? 1 : 0) +
|
|
202
|
+
(m.locked ? 2 : 0) +
|
|
203
|
+
Number(m.importance || 0) +
|
|
204
|
+
(Number(m.confidence ?? 1) - 0.5) +
|
|
205
|
+
0.5 * 0.5 ** (idle / 30),
|
|
206
|
+
why: fts.has(m.id) ? "fts" : about ? "subject" : "related",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
scored.sort((a, b) => b.score - a.score || b.m.created - a.m.created);
|
|
210
|
+
const selected = [];
|
|
211
|
+
if (recallIntent(rows)) {
|
|
212
|
+
// An explicit question about our past needs a range of places and
|
|
213
|
+
// aspects, not twelve local events with almost identical scores.
|
|
214
|
+
const elsewhere = scored.filter(
|
|
215
|
+
(item) =>
|
|
216
|
+
!item.local &&
|
|
217
|
+
item.m.discretion === "open" &&
|
|
218
|
+
speakers.has(String(item.m.subject)),
|
|
219
|
+
);
|
|
220
|
+
const covered = new Set();
|
|
221
|
+
for (const item of elsewhere) {
|
|
222
|
+
const key = `${item.m.subject}:${item.m.session_id}`;
|
|
223
|
+
if (covered.has(key)) continue;
|
|
224
|
+
selected.push(item);
|
|
225
|
+
covered.add(key);
|
|
226
|
+
if (selected.length === 4) break;
|
|
227
|
+
}
|
|
228
|
+
const kinds = new Set(selected.map((item) => item.m.type));
|
|
229
|
+
for (const item of elsewhere) {
|
|
230
|
+
if (selected.length >= 4) break;
|
|
231
|
+
if (selected.includes(item) || kinds.has(item.m.type)) continue;
|
|
232
|
+
selected.push(item);
|
|
233
|
+
kinds.add(item.m.type);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
selected.push(
|
|
237
|
+
...scored
|
|
238
|
+
.filter((item) => !selected.includes(item))
|
|
239
|
+
.slice(0, RECALL_LIMIT - selected.length),
|
|
240
|
+
);
|
|
241
|
+
if (touch) {
|
|
242
|
+
const access = db.prepare(
|
|
243
|
+
"UPDATE core_memories SET last_access=? WHERE id=?",
|
|
244
|
+
);
|
|
245
|
+
// A weak overlap does not count as remembering. Only a real cue, the
|
|
246
|
+
// person speaking, or a locked memory starts the quiet stretch again.
|
|
247
|
+
for (const { m, strong } of selected)
|
|
248
|
+
if (strong) access.run(cutoff, m.id);
|
|
249
|
+
}
|
|
250
|
+
return selected.map(({ m, local, why }) => {
|
|
251
|
+
const age = cutoff - Number(m.created || cutoff);
|
|
252
|
+
return {
|
|
253
|
+
id: m.id,
|
|
254
|
+
subject: m.subject,
|
|
255
|
+
content:
|
|
256
|
+
forSpeech && !this.permission(m, session, cutoff).disclose
|
|
257
|
+
? "(关于此人的一条受限记忆;没有在这里复述具体内容的授权)"
|
|
258
|
+
: m.content,
|
|
259
|
+
type: m.type,
|
|
260
|
+
confidence: m.confidence,
|
|
261
|
+
importance: m.importance,
|
|
262
|
+
whySelected: why,
|
|
263
|
+
source: local
|
|
264
|
+
? "这里"
|
|
265
|
+
: isPrivateSession(m.session_id)
|
|
266
|
+
? "私聊里"
|
|
267
|
+
: "别的群里",
|
|
268
|
+
// Old news may no longer be true; she should know how old it is.
|
|
269
|
+
...(age >= 7 * DAY ? { when: `${agoLabel(age)}知道的` } : {}),
|
|
270
|
+
...(m.discretion === "private" && !local
|
|
271
|
+
? { discretion: "私下知道的,不要当众说出口,也不要说出从哪听来" }
|
|
272
|
+
: {}),
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
secretsOutside(session) {
|
|
277
|
+
return this.#outside(session, "secret");
|
|
278
|
+
}
|
|
279
|
+
permission(memory, session, now = Date.now()) {
|
|
280
|
+
const local = this.scopes(session).includes(memory.session_id);
|
|
281
|
+
const saved = this.repo.db
|
|
282
|
+
.prepare(
|
|
283
|
+
"SELECT recall,disclose,expires FROM mind_memory_permissions WHERE memory_id=? AND session_id=?",
|
|
284
|
+
)
|
|
285
|
+
.get(memory.id, session);
|
|
286
|
+
if (saved && (!saved.expires || saved.expires > now))
|
|
287
|
+
return { recall: !!saved.recall, disclose: !!saved.disclose };
|
|
288
|
+
return {
|
|
289
|
+
recall: memory.discretion !== "secret" || local,
|
|
290
|
+
disclose: memory.discretion === "open" || local,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
permissions(id) {
|
|
294
|
+
if (!this.repo.db.prepare("SELECT 1 FROM core_memories WHERE id=?").get(id))
|
|
295
|
+
throw Error("记忆不存在");
|
|
296
|
+
return this.repo.db
|
|
297
|
+
.prepare(
|
|
298
|
+
"SELECT session_id,recall,disclose,expires,updated FROM mind_memory_permissions WHERE memory_id=?",
|
|
299
|
+
)
|
|
300
|
+
.all(id);
|
|
301
|
+
}
|
|
302
|
+
setPermission(id, { session, recall, disclose, expires = null }) {
|
|
303
|
+
if (
|
|
304
|
+
!this.repo.db
|
|
305
|
+
.prepare("SELECT 1 FROM core_memories WHERE id=? AND status!='deleted'")
|
|
306
|
+
.get(id)
|
|
307
|
+
)
|
|
308
|
+
throw Error("记忆不存在");
|
|
309
|
+
if (!this.repo.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
|
|
310
|
+
throw Error("会话不存在");
|
|
311
|
+
if (
|
|
312
|
+
typeof recall !== "boolean" ||
|
|
313
|
+
typeof disclose !== "boolean" ||
|
|
314
|
+
(disclose && !recall)
|
|
315
|
+
)
|
|
316
|
+
throw Error("允许复述时也必须允许想起");
|
|
317
|
+
if (
|
|
318
|
+
expires !== null &&
|
|
319
|
+
(!Number.isSafeInteger(expires) || expires <= Date.now())
|
|
320
|
+
)
|
|
321
|
+
throw Error("授权到期时间无效");
|
|
322
|
+
const value = { recall, disclose, expires };
|
|
323
|
+
const db = this.repo.db,
|
|
324
|
+
now = Date.now();
|
|
325
|
+
db.exec("SAVEPOINT memory_permission");
|
|
326
|
+
try {
|
|
327
|
+
db.prepare(
|
|
328
|
+
"INSERT INTO mind_memory_permissions VALUES (?,?,?,?,?,?) ON CONFLICT(memory_id,session_id) DO UPDATE SET recall=excluded.recall,disclose=excluded.disclose,expires=excluded.expires,updated=excluded.updated",
|
|
329
|
+
).run(id, session, +recall, +disclose, expires, now);
|
|
330
|
+
db.prepare(
|
|
331
|
+
"INSERT INTO mind_permission_events(memory_id,session_id,created,value) VALUES (?,?,?,?)",
|
|
332
|
+
).run(id, session, now, JSON.stringify(value));
|
|
333
|
+
db.exec("RELEASE memory_permission");
|
|
334
|
+
} catch (error) {
|
|
335
|
+
db.exec("ROLLBACK TO memory_permission");
|
|
336
|
+
db.exec("RELEASE memory_permission");
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
this.repo.store.revision++;
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
// Private facts may be recalled, with a warning. They still must not leave
|
|
343
|
+
// in the words she actually sends.
|
|
344
|
+
privateOutside(session) {
|
|
345
|
+
return this.#outside(session, "private");
|
|
346
|
+
}
|
|
347
|
+
#outside(session, discretion) {
|
|
348
|
+
const here = new Set(this.scopes(session));
|
|
349
|
+
return this.repo.db
|
|
350
|
+
.prepare(
|
|
351
|
+
"SELECT id,session_id,subject,content FROM core_memories WHERE discretion=? AND status='confirmed' ORDER BY updated DESC LIMIT 200",
|
|
352
|
+
)
|
|
353
|
+
.all(discretion)
|
|
354
|
+
.filter(
|
|
355
|
+
(m) =>
|
|
356
|
+
!here.has(m.session_id) &&
|
|
357
|
+
!this.permission({ ...m, discretion }, session).disclose,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
update(id, patch) {
|
|
361
|
+
const db = this.repo.db,
|
|
362
|
+
old = db.prepare("SELECT * FROM core_memories WHERE id=?").get(id);
|
|
363
|
+
if (!old) throw Error("记忆不存在");
|
|
364
|
+
const m = { ...old, ...patch };
|
|
365
|
+
if (patch.discretion !== undefined && !DISCRETIONS.has(patch.discretion))
|
|
366
|
+
throw Error("分寸标签无效");
|
|
367
|
+
db.exec("SAVEPOINT memory_update");
|
|
368
|
+
try {
|
|
369
|
+
db.prepare(
|
|
370
|
+
"INSERT INTO core_memory_versions(memory_id,time,value) VALUES (?,?,?)",
|
|
371
|
+
).run(id, Date.now(), JSON.stringify(old));
|
|
372
|
+
db.prepare(
|
|
373
|
+
"UPDATE core_memories SET content=?,status=?,locked=?,confidence=?,importance=?,expires=?,sources=?,discretion=?,updated=?,version=version+1 WHERE id=?",
|
|
374
|
+
).run(
|
|
375
|
+
m.content,
|
|
376
|
+
m.status,
|
|
377
|
+
+m.locked,
|
|
378
|
+
m.confidence,
|
|
379
|
+
m.importance,
|
|
380
|
+
m.expires || null,
|
|
381
|
+
m.sources,
|
|
382
|
+
m.discretion || "open",
|
|
383
|
+
Date.now(),
|
|
384
|
+
id,
|
|
385
|
+
);
|
|
386
|
+
indexMemory(db, id, m.content);
|
|
387
|
+
db.exec("RELEASE memory_update");
|
|
388
|
+
} catch (e) {
|
|
389
|
+
db.exec("ROLLBACK TO memory_update");
|
|
390
|
+
db.exec("RELEASE memory_update");
|
|
391
|
+
throw e;
|
|
392
|
+
}
|
|
393
|
+
this.mind?.recall?.sync("fact", id);
|
|
394
|
+
this.repo.store.revision++;
|
|
395
|
+
}
|
|
396
|
+
// Her own forgetting leaves the record and hides it from ordinary recall.
|
|
397
|
+
// A person's request to delete still removes the source, the index and the vector.
|
|
398
|
+
archive(id) {
|
|
399
|
+
this.update(id, { status: "archived" });
|
|
400
|
+
return { id, status: "archived" };
|
|
401
|
+
}
|
|
402
|
+
// Withdrawal leaves a tombstone, so a later consolidation cannot bring the
|
|
403
|
+
// same belief back.
|
|
404
|
+
revoke(id, reason = "") {
|
|
405
|
+
const row = this.repo.db
|
|
406
|
+
.prepare("SELECT * FROM core_memories WHERE id=?")
|
|
407
|
+
.get(id);
|
|
408
|
+
if (!row) throw Error("记忆不存在");
|
|
409
|
+
if (row.locked) throw Error("记忆已锁定,请先解锁");
|
|
410
|
+
this.update(id, { status: "deleted" });
|
|
411
|
+
this.repo.db
|
|
412
|
+
.prepare(
|
|
413
|
+
"INSERT INTO mind_revocations(id,created,target_kind,target_id,content,reason) VALUES (?,?,?,?,?,?)",
|
|
414
|
+
)
|
|
415
|
+
.run(
|
|
416
|
+
randomUUID(),
|
|
417
|
+
Date.now(),
|
|
418
|
+
"memory",
|
|
419
|
+
id,
|
|
420
|
+
`${row.subject}\n${row.content}`,
|
|
421
|
+
text(reason, 200),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
revokedFor(subject) {
|
|
425
|
+
return this.repo.db
|
|
426
|
+
.prepare(
|
|
427
|
+
"SELECT content FROM mind_revocations WHERE target_kind='memory' AND content LIKE ?",
|
|
428
|
+
)
|
|
429
|
+
.all(`${subject}\n%`)
|
|
430
|
+
.map((row) => row.content.slice(String(subject).length + 1));
|
|
431
|
+
}
|
|
432
|
+
deleteBatch(session, ids = [], { all = false } = {}) {
|
|
433
|
+
const db = this.repo.db;
|
|
434
|
+
if (
|
|
435
|
+
typeof session !== "string" ||
|
|
436
|
+
!session ||
|
|
437
|
+
!db.prepare("SELECT id FROM sessions WHERE id=?").get(session)
|
|
438
|
+
)
|
|
439
|
+
throw Error("会话不存在");
|
|
440
|
+
if (!all && !Array.isArray(ids)) throw Error("请选择要删除的记忆");
|
|
441
|
+
const uniqueIds = [...new Set((Array.isArray(ids) ? ids : []).map(String))];
|
|
442
|
+
if (!all && (!uniqueIds.length || uniqueIds.length > 500))
|
|
443
|
+
throw Error("请选择 1–500 条记忆");
|
|
444
|
+
if (all && uniqueIds.length) throw Error("全选删除不需要传入记忆 ID");
|
|
445
|
+
const rows = all
|
|
446
|
+
? db
|
|
447
|
+
.prepare(
|
|
448
|
+
"SELECT id,session_id,locked FROM core_memories WHERE session_id=? AND status!='deleted'",
|
|
449
|
+
)
|
|
450
|
+
.all(session)
|
|
451
|
+
: uniqueIds.map((id) =>
|
|
452
|
+
db
|
|
453
|
+
.prepare(
|
|
454
|
+
"SELECT id,session_id,locked FROM core_memories WHERE id=?",
|
|
455
|
+
)
|
|
456
|
+
.get(id),
|
|
457
|
+
);
|
|
458
|
+
if (!all && (rows.some((row) => !row) || rows.length !== uniqueIds.length))
|
|
459
|
+
throw Error("只能删除当前会话中存在的记忆");
|
|
460
|
+
if (rows.some((row) => row.session_id !== session))
|
|
461
|
+
throw Error("只能删除当前会话的记忆,共享或继承记忆请单独管理");
|
|
462
|
+
if (rows.some((row) => row.locked))
|
|
463
|
+
throw Error("包含已锁定记忆,请先解锁后再删除");
|
|
464
|
+
if (!rows.length) return { deleted: 0 };
|
|
465
|
+
const placeholders = rows.map(() => "?").join(",");
|
|
466
|
+
const rowIds = rows.map((row) => row.id);
|
|
467
|
+
db.exec("BEGIN IMMEDIATE");
|
|
468
|
+
try {
|
|
469
|
+
db.prepare(
|
|
470
|
+
`DELETE FROM core_memory_versions WHERE memory_id IN (${placeholders})`,
|
|
471
|
+
).run(...rowIds);
|
|
472
|
+
db.prepare(
|
|
473
|
+
`DELETE FROM core_memory_fts WHERE memory_id IN (${placeholders})`,
|
|
474
|
+
).run(...rowIds);
|
|
475
|
+
db.prepare(`DELETE FROM core_memories WHERE id IN (${placeholders})`).run(
|
|
476
|
+
...rowIds,
|
|
477
|
+
);
|
|
478
|
+
// Deleted means deleted: the searchable copy and its vectors go too.
|
|
479
|
+
for (const id of rowIds) this.mind?.recall?.store.remove(`fact:${id}`);
|
|
480
|
+
db.prepare(
|
|
481
|
+
`DELETE FROM mind_links WHERE from_id IN (${placeholders}) OR to_id IN (${placeholders})`,
|
|
482
|
+
).run(
|
|
483
|
+
...rowIds.map((id) => `fact:${id}`),
|
|
484
|
+
...rowIds.map((id) => `fact:${id}`),
|
|
485
|
+
);
|
|
486
|
+
db.exec("COMMIT");
|
|
487
|
+
} catch (e) {
|
|
488
|
+
db.exec("ROLLBACK");
|
|
489
|
+
throw e;
|
|
490
|
+
}
|
|
491
|
+
this.repo.store.revision++;
|
|
492
|
+
return { deleted: rows.length };
|
|
493
|
+
}
|
|
494
|
+
insert({
|
|
495
|
+
session,
|
|
496
|
+
subject,
|
|
497
|
+
content,
|
|
498
|
+
type = "event",
|
|
499
|
+
confidence = 0.8,
|
|
500
|
+
importance = 0.5,
|
|
501
|
+
sources = [],
|
|
502
|
+
discretion = "open",
|
|
503
|
+
locked = false,
|
|
504
|
+
time = Date.now(),
|
|
505
|
+
}) {
|
|
506
|
+
const db = this.repo.db;
|
|
507
|
+
const same = db
|
|
508
|
+
.prepare(
|
|
509
|
+
"SELECT id FROM core_memories WHERE subject=? AND content=? AND status!='deleted'",
|
|
510
|
+
)
|
|
511
|
+
.get(subject, content);
|
|
512
|
+
if (same) {
|
|
513
|
+
// Said again is remembered more firmly, not remembered twice.
|
|
514
|
+
this.reinforce(same.id, sources, time);
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
if (this.revokedFor(subject).some((old) => similar(old, content, 0.75)))
|
|
518
|
+
return null;
|
|
519
|
+
const id = randomUUID();
|
|
520
|
+
db.prepare(
|
|
521
|
+
"INSERT INTO core_memories(id,session_id,subject,content,type,confidence,importance,status,locked,sources,created,updated,discretion,basis,last_confirmed) VALUES (?,?,?,?,?,?,?,'confirmed',?,?,?,?,?,?,?)",
|
|
522
|
+
).run(
|
|
523
|
+
id,
|
|
524
|
+
session,
|
|
525
|
+
subject,
|
|
526
|
+
content,
|
|
527
|
+
type,
|
|
528
|
+
clamp(confidence),
|
|
529
|
+
clamp(importance),
|
|
530
|
+
+locked,
|
|
531
|
+
JSON.stringify(sources),
|
|
532
|
+
time,
|
|
533
|
+
time,
|
|
534
|
+
DISCRETIONS.has(discretion) ? discretion : "open",
|
|
535
|
+
basisOf(sources),
|
|
536
|
+
time,
|
|
537
|
+
);
|
|
538
|
+
indexMemory(db, id, content);
|
|
539
|
+
this.mind?.recall?.sync("fact", id);
|
|
540
|
+
return id;
|
|
541
|
+
}
|
|
542
|
+
// Another time the person said the same thing. The memory keeps its words
|
|
543
|
+
// and gains the new source, a count and the date it was last confirmed.
|
|
544
|
+
reinforce(id, sources = [], time = Date.now()) {
|
|
545
|
+
const db = this.repo.db;
|
|
546
|
+
const row = db
|
|
547
|
+
.prepare(
|
|
548
|
+
"SELECT sources,evidence_count FROM core_memories WHERE id=? AND status='confirmed'",
|
|
549
|
+
)
|
|
550
|
+
.get(id);
|
|
551
|
+
if (!row) return false;
|
|
552
|
+
const old = parse(row.sources, []);
|
|
553
|
+
const known = new Set(old.map((s) => `${s?.id}`));
|
|
554
|
+
const added = (Array.isArray(sources) ? sources : []).filter(
|
|
555
|
+
(s) => s && !known.has(`${s.id}`),
|
|
556
|
+
);
|
|
557
|
+
if (!added.length && old.length) return false;
|
|
558
|
+
db.prepare(
|
|
559
|
+
"UPDATE core_memories SET sources=?,evidence_count=evidence_count+1,last_confirmed=MAX(COALESCE(last_confirmed,0),?) WHERE id=?",
|
|
560
|
+
).run(JSON.stringify([...old, ...added].slice(-12)), time, id);
|
|
561
|
+
this.mind?.recall?.sync("fact", id);
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
// Two statements that cannot both hold and could not be settled. Both stay
|
|
565
|
+
// remembered; the link lets recall show them side by side with their dates.
|
|
566
|
+
conflict(newer, older, sources = [], time = Date.now()) {
|
|
567
|
+
this.repo.db
|
|
568
|
+
.prepare(
|
|
569
|
+
"INSERT OR IGNORE INTO mind_links(from_id,to_id,relation,created,sources) VALUES (?,?,'conflicts',?,?)",
|
|
570
|
+
)
|
|
571
|
+
.run(`fact:${newer}`, `fact:${older}`, time, JSON.stringify(sources));
|
|
572
|
+
}
|
|
573
|
+
// "记住,我……" is a friend asking to be remembered, not a draft for review.
|
|
574
|
+
remember(message) {
|
|
575
|
+
const value = String(message.text || "");
|
|
576
|
+
if (/不要记|别记|不许记/.test(value)) return null;
|
|
577
|
+
const match = value.match(
|
|
578
|
+
/(?:请)?(?:记住|记一下)[,,::\s]*(我[^\n]{2,200})[。!!]?$/u,
|
|
579
|
+
);
|
|
580
|
+
const name = requestedName(value);
|
|
581
|
+
if ((!match && !name) || SENSITIVE.test(value) || hasCredential(value))
|
|
582
|
+
return null;
|
|
583
|
+
const said = name
|
|
584
|
+
? `我希望被称呼为${name}`
|
|
585
|
+
: match[1].replace(/[。!!]+$/, "").trim();
|
|
586
|
+
const content = /我/.test(said.slice(1))
|
|
587
|
+
? `原话:${said}`
|
|
588
|
+
: said.replace(/^我/, "");
|
|
589
|
+
const privateChat = message.kind === "private";
|
|
590
|
+
const quiet =
|
|
591
|
+
secretRequest(value) ||
|
|
592
|
+
this.#recentSecret(message.sessionId, message.userId, message.seq);
|
|
593
|
+
const id = this.insert({
|
|
594
|
+
session: message.sessionId,
|
|
595
|
+
subject: String(message.userId),
|
|
596
|
+
content,
|
|
597
|
+
type: "preference",
|
|
598
|
+
confidence: 0.95,
|
|
599
|
+
importance: 0.7,
|
|
600
|
+
sources: [
|
|
601
|
+
{
|
|
602
|
+
id: message.seq,
|
|
603
|
+
text: value.slice(-300),
|
|
604
|
+
speaker: message.userId,
|
|
605
|
+
time: message.time,
|
|
606
|
+
certainty: "self_report",
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
discretion: quiet ? "secret" : privateChat ? "private" : "open",
|
|
610
|
+
time: Number(message.time) || Date.now(),
|
|
611
|
+
});
|
|
612
|
+
if (id) this.repo.store.revision++;
|
|
613
|
+
return id;
|
|
614
|
+
}
|
|
615
|
+
// "别告诉别人" then, after more of their own messages, "记住,我……" is still
|
|
616
|
+
// a secret until this stretch is consolidated. Only words she could have
|
|
617
|
+
// heard count. A request from the previous stretch does not carry over.
|
|
618
|
+
#recentSecret(session, userId, seq) {
|
|
619
|
+
if (!session || seq == null) return false;
|
|
620
|
+
const rows = this.repo.db
|
|
621
|
+
.prepare(
|
|
622
|
+
`SELECT payload FROM core_events
|
|
623
|
+
WHERE session_id=? AND role='user' AND seq<?
|
|
624
|
+
AND seq>COALESCE((SELECT seq FROM core_cursors WHERE session_id=?),0)
|
|
625
|
+
AND json_extract(payload,'$.userId')=?
|
|
626
|
+
AND seq NOT IN (SELECT seq FROM mind_unlived)
|
|
627
|
+
ORDER BY seq DESC LIMIT 40`,
|
|
628
|
+
)
|
|
629
|
+
.all(session, seq, session, String(userId));
|
|
630
|
+
return rows.some((row) => {
|
|
631
|
+
let payload = {};
|
|
632
|
+
try {
|
|
633
|
+
payload = JSON.parse(row.payload);
|
|
634
|
+
} catch {
|
|
635
|
+
payload = {};
|
|
636
|
+
}
|
|
637
|
+
return secretRequest(payload.text);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
pending(session) {
|
|
641
|
+
return this.repo.db
|
|
642
|
+
.prepare(
|
|
643
|
+
"SELECT COUNT(*) n FROM core_events WHERE session_id=? AND role='user' AND seq>COALESCE((SELECT seq FROM core_cursors WHERE session_id=?),0) AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0",
|
|
644
|
+
)
|
|
645
|
+
.get(session, session).n;
|
|
646
|
+
}
|
|
647
|
+
due(session, now = Date.now()) {
|
|
648
|
+
return this.mind
|
|
649
|
+
? consolidationDecision(this.mind, session, now)
|
|
650
|
+
: { due: this.pending(session) >= BLOCK_USERS };
|
|
651
|
+
}
|
|
652
|
+
// What she already remembers about the people in this stretch, so the
|
|
653
|
+
// same fact is not written twice and a changed one can replace the old.
|
|
654
|
+
known(session, block) {
|
|
655
|
+
const here = new Set(this.scopes(session));
|
|
656
|
+
const known = [];
|
|
657
|
+
const refs = new Map();
|
|
658
|
+
const read = this.repo.db.prepare(
|
|
659
|
+
"SELECT id,subject,content,session_id,discretion FROM core_memories WHERE subject=? AND status='confirmed' ORDER BY importance DESC, updated DESC LIMIT 5",
|
|
660
|
+
);
|
|
661
|
+
const subjects = new Set(
|
|
662
|
+
block.filter((m) => m.role === "user").map((m) => String(m.userId)),
|
|
663
|
+
);
|
|
664
|
+
for (const subject of subjects)
|
|
665
|
+
for (const k of read.all(subject)) {
|
|
666
|
+
if (known.length >= 20) return { known, refs };
|
|
667
|
+
if (k.discretion !== "open" && !here.has(k.session_id)) continue;
|
|
668
|
+
const ref = `k${known.length + 1}`;
|
|
669
|
+
refs.set(ref, k);
|
|
670
|
+
known.push({ ref, subject: k.subject, content: text(k.content, 80) });
|
|
671
|
+
}
|
|
672
|
+
return { known, refs };
|
|
673
|
+
}
|
|
674
|
+
// A newer fact replaces an older one; the old stays visible with its
|
|
675
|
+
// versions but is no longer recalled.
|
|
676
|
+
supersede(id, by) {
|
|
677
|
+
const old = this.repo.db
|
|
678
|
+
.prepare("SELECT * FROM core_memories WHERE id=?")
|
|
679
|
+
.get(id);
|
|
680
|
+
if (!old || old.locked || old.status !== "confirmed" || id === by)
|
|
681
|
+
return false;
|
|
682
|
+
this.update(id, { status: "superseded" });
|
|
683
|
+
this.repo.db
|
|
684
|
+
.prepare("UPDATE core_memories SET superseded_by=? WHERE id=?")
|
|
685
|
+
.run(by, id);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
// Things said in this stretch that are still to come. Someone's plans and
|
|
689
|
+
// dates must come from their own words; a promise must be her own.
|
|
690
|
+
anticipate(list, { block, session, names, privateChat, now }) {
|
|
691
|
+
if (!this.mind?.anticipations) return 0;
|
|
692
|
+
let added = 0;
|
|
693
|
+
for (const a of (Array.isArray(list) ? list : []).slice(0, 8)) {
|
|
694
|
+
if (!a || typeof a.content !== "string" || !a.content.trim()) continue;
|
|
695
|
+
const kind = ["event", "promise", "date"].includes(a.kind)
|
|
696
|
+
? a.kind
|
|
697
|
+
: null;
|
|
698
|
+
const cited = block.filter((m) =>
|
|
699
|
+
(Array.isArray(a.sources) ? a.sources : []).map(Number).includes(m.seq),
|
|
700
|
+
);
|
|
701
|
+
if (!kind || !cited.length || cited.some((m) => m.artifact)) continue;
|
|
702
|
+
let subject = a.subject == null ? null : String(a.subject);
|
|
703
|
+
if (kind === "promise") {
|
|
704
|
+
if (!cited.every((m) => m.role === "assistant")) continue;
|
|
705
|
+
if (
|
|
706
|
+
subject &&
|
|
707
|
+
!block.some((m) => m.role === "user" && String(m.userId) === subject)
|
|
708
|
+
)
|
|
709
|
+
subject = null;
|
|
710
|
+
} else if (
|
|
711
|
+
!subject ||
|
|
712
|
+
!cited.some((m) => m.role === "user" && String(m.userId) === subject)
|
|
713
|
+
)
|
|
714
|
+
continue;
|
|
715
|
+
const first = Math.min(...cited.map((m) => m.seq));
|
|
716
|
+
const last = Math.max(...cited.map((m) => m.seq));
|
|
717
|
+
const secret =
|
|
718
|
+
block.some(
|
|
719
|
+
(m) =>
|
|
720
|
+
m.role === "user" &&
|
|
721
|
+
m.seq <= last &&
|
|
722
|
+
m.seq >= first - 6 &&
|
|
723
|
+
secretRequest(m.text),
|
|
724
|
+
) || askedSecret(block, subject, last);
|
|
725
|
+
if (
|
|
726
|
+
!grounded(
|
|
727
|
+
a.content,
|
|
728
|
+
cited.map((m) => m.text || ""),
|
|
729
|
+
)
|
|
730
|
+
)
|
|
731
|
+
continue;
|
|
732
|
+
const result = this.mind.anticipations.add({
|
|
733
|
+
kind,
|
|
734
|
+
subject,
|
|
735
|
+
session,
|
|
736
|
+
content: applySpeakerNames(a.content.trim(), names),
|
|
737
|
+
due: a.due,
|
|
738
|
+
recurrence: a.recurrence,
|
|
739
|
+
discretion: secret ? "secret" : privateChat ? "private" : "open",
|
|
740
|
+
sources: cited.map((m) => m.seq),
|
|
741
|
+
origin: "memory",
|
|
742
|
+
time: now,
|
|
743
|
+
});
|
|
744
|
+
if (result.id) added++;
|
|
745
|
+
}
|
|
746
|
+
return added;
|
|
747
|
+
}
|
|
748
|
+
async consolidate(
|
|
749
|
+
session,
|
|
750
|
+
profile,
|
|
751
|
+
prompt,
|
|
752
|
+
trace,
|
|
753
|
+
{
|
|
754
|
+
force = false,
|
|
755
|
+
simulated = false,
|
|
756
|
+
models = this.models,
|
|
757
|
+
now = Date.now(),
|
|
758
|
+
} = {},
|
|
759
|
+
) {
|
|
760
|
+
if (this.busy.has(session)) return;
|
|
761
|
+
if (!force && now - (this.lastAttempt.get(session) || 0) < 60000) return;
|
|
762
|
+
const backoff = this.mind?.backoff;
|
|
763
|
+
const backoffKey = `memory:${session}`;
|
|
764
|
+
if (!force && backoff && !backoff.ready(backoffKey, now)) return;
|
|
765
|
+
this.busy.add(session);
|
|
766
|
+
let blockCursor = null;
|
|
767
|
+
try {
|
|
768
|
+
const db = this.repo.db;
|
|
769
|
+
const cursor =
|
|
770
|
+
db
|
|
771
|
+
.prepare("SELECT seq FROM core_cursors WHERE session_id=?")
|
|
772
|
+
.get(session)?.seq || 0;
|
|
773
|
+
const aside = new Set(
|
|
774
|
+
db
|
|
775
|
+
.prepare("SELECT seq FROM mind_unlived")
|
|
776
|
+
.all()
|
|
777
|
+
.map((row) => row.seq),
|
|
778
|
+
);
|
|
779
|
+
const rows = this.repo
|
|
780
|
+
.eventsAfter(session, cursor, { simulated, limit: 200 })
|
|
781
|
+
.filter(
|
|
782
|
+
(m) =>
|
|
783
|
+
m.time <= now &&
|
|
784
|
+
!aside.has(m.seq) &&
|
|
785
|
+
!/^\[(图片|表情|媒体)\]+$/.test(m.text || ""),
|
|
786
|
+
);
|
|
787
|
+
const users = rows.filter((m) => m.role === "user");
|
|
788
|
+
if (!users.length || (!force && !this.due(session, now).due)) return;
|
|
789
|
+
this.lastAttempt.set(session, now);
|
|
790
|
+
blockCursor = cursor;
|
|
791
|
+
// A stretch the model could not turn into valid notes twice is tried
|
|
792
|
+
// in smaller pieces; the same input is not resent unchanged forever.
|
|
793
|
+
const failing = backoff?.state(backoffKey).meta || {};
|
|
794
|
+
const blockUsers =
|
|
795
|
+
failing.cursor === cursor && failing.blockUsers
|
|
796
|
+
? failing.blockUsers
|
|
797
|
+
: BLOCK_USERS;
|
|
798
|
+
// Her answers to the block's last messages belong with that block.
|
|
799
|
+
const next = users[blockUsers]?.seq ?? Number.MAX_SAFE_INTEGER;
|
|
800
|
+
const block = rows.filter((m) => m.seq < next);
|
|
801
|
+
const last = block.at(-1).seq;
|
|
802
|
+
const names = speakerNames(db, [session]);
|
|
803
|
+
const self = this.mind?.nature.current().name || "self";
|
|
804
|
+
const zone = this.mind?.timeZone() || "Asia/Shanghai";
|
|
805
|
+
const { known, refs } = this.mind?.recall
|
|
806
|
+
? await this.mind.recall.knownFor(session, block)
|
|
807
|
+
: this.known(session, block);
|
|
808
|
+
const threads = this.mind?.episodes?.candidates(session, block, now) || {
|
|
809
|
+
list: [],
|
|
810
|
+
refs: new Map(),
|
|
811
|
+
};
|
|
812
|
+
// How she read these messages when she answered them.
|
|
813
|
+
const readings = new Map(
|
|
814
|
+
db
|
|
815
|
+
.prepare(
|
|
816
|
+
`SELECT seq,tone,stance FROM core_message_cues WHERE seq IN (${block.map(() => "?").join(",")})`,
|
|
817
|
+
)
|
|
818
|
+
.all(...block.map((m) => m.seq))
|
|
819
|
+
.map((row) => [
|
|
820
|
+
row.seq,
|
|
821
|
+
{
|
|
822
|
+
...(row.tone ? { tone: row.tone } : {}),
|
|
823
|
+
...(row.stance ? { stance: row.stance } : {}),
|
|
824
|
+
},
|
|
825
|
+
]),
|
|
826
|
+
);
|
|
827
|
+
const value = await models.call(
|
|
828
|
+
profile,
|
|
829
|
+
"memory",
|
|
830
|
+
prompt +
|
|
831
|
+
"\nartifact 标记的是作品正文,fiction 的人物/剧情和 reference 的游戏资料情节不能作为现实人物事实;不要把正文中的约定提取成她现实接受的承诺。读者讨论作品时保留作品语境。",
|
|
832
|
+
{
|
|
833
|
+
sessionId: session,
|
|
834
|
+
self,
|
|
835
|
+
privateChat: isPrivateSession(session),
|
|
836
|
+
...(known.length ? { known } : {}),
|
|
837
|
+
...(threads.list.length ? { openEpisodes: threads.list } : {}),
|
|
838
|
+
messages: block.map((m) => ({
|
|
839
|
+
id: m.seq,
|
|
840
|
+
userId: m.role === "assistant" ? "self" : m.userId,
|
|
841
|
+
name:
|
|
842
|
+
m.role === "assistant"
|
|
843
|
+
? self
|
|
844
|
+
: readableName(m.name, m.userId) ||
|
|
845
|
+
names.get(String(m.userId)) ||
|
|
846
|
+
m.userId,
|
|
847
|
+
role: m.role,
|
|
848
|
+
...(m.artifact ? { artifact: m.artifact } : {}),
|
|
849
|
+
...(readings.get(m.seq) ? { readAs: readings.get(m.seq) } : {}),
|
|
850
|
+
text: String(m.text || "").slice(0, 600),
|
|
851
|
+
// A readable local time lets "明天" become a date.
|
|
852
|
+
localTime: localClock(m.time, zone).local,
|
|
853
|
+
})),
|
|
854
|
+
},
|
|
855
|
+
trace,
|
|
856
|
+
);
|
|
857
|
+
if (typeof value?.summary !== "string" || !Array.isArray(value.facts))
|
|
858
|
+
throw Error("记忆整理格式无效");
|
|
859
|
+
db.exec("BEGIN IMMEDIATE");
|
|
860
|
+
try {
|
|
861
|
+
const said = block.map((m) => m.text || "");
|
|
862
|
+
const raw = value.summary.trim();
|
|
863
|
+
const summary =
|
|
864
|
+
raw && grounded(raw, said) ? applySpeakerNames(raw, names) : "";
|
|
865
|
+
const privateChat = isPrivateSession(session);
|
|
866
|
+
const keptFacts = [];
|
|
867
|
+
for (const f of value.facts.slice(0, 30)) {
|
|
868
|
+
if (
|
|
869
|
+
typeof f?.content !== "string" ||
|
|
870
|
+
!f.content.trim() ||
|
|
871
|
+
f.content.length > 2000 ||
|
|
872
|
+
!Array.isArray(f.sources) ||
|
|
873
|
+
!f.sources.length ||
|
|
874
|
+
!Number.isFinite(f.confidence) ||
|
|
875
|
+
!Number.isFinite(f.importance) ||
|
|
876
|
+
["joke", "hearsay"].includes(f.certainty) ||
|
|
877
|
+
SENSITIVE.test(f.content) ||
|
|
878
|
+
hasCredential(f.content)
|
|
879
|
+
)
|
|
880
|
+
continue;
|
|
881
|
+
const sources = block.filter((m) => f.sources.includes(m.seq));
|
|
882
|
+
if (
|
|
883
|
+
sources.length !== f.sources.length ||
|
|
884
|
+
!sources.every(
|
|
885
|
+
(m) =>
|
|
886
|
+
m.role === "user" && String(m.userId) === String(f.subject),
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
continue;
|
|
890
|
+
if (
|
|
891
|
+
!grounded(
|
|
892
|
+
f.content,
|
|
893
|
+
sources.map((m) => m.text || ""),
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
continue;
|
|
897
|
+
// Said as a joke, in irony or as hearsay when she read it: not a
|
|
898
|
+
// fact about the person. A guess is kept as an inference at most.
|
|
899
|
+
const read = sources.map((m) => readings.get(m.seq) || {});
|
|
900
|
+
if (
|
|
901
|
+
read.every(
|
|
902
|
+
(r) =>
|
|
903
|
+
["joke", "hearsay"].includes(r.stance) ||
|
|
904
|
+
["joking", "teasing", "ironic"].includes(r.tone),
|
|
905
|
+
)
|
|
906
|
+
)
|
|
907
|
+
continue;
|
|
908
|
+
if (read.every((r) => r.stance === "guess")) {
|
|
909
|
+
f.certainty = "inferred";
|
|
910
|
+
f.confidence = Math.min(0.6, f.confidence);
|
|
911
|
+
}
|
|
912
|
+
const secret =
|
|
913
|
+
f.discretion === "secret" ||
|
|
914
|
+
askedSecret(block, f.subject, Math.max(...f.sources));
|
|
915
|
+
const content = applySpeakerNames(f.content.trim(), names);
|
|
916
|
+
const citations = sources.map((m) => ({
|
|
917
|
+
id: m.seq,
|
|
918
|
+
text: m.text,
|
|
919
|
+
speaker: m.userId,
|
|
920
|
+
time: m.time,
|
|
921
|
+
certainty: f.certainty || "unknown",
|
|
922
|
+
}));
|
|
923
|
+
const confirmed = (Array.isArray(f.confirms) ? f.confirms : [])
|
|
924
|
+
.map((ref) => refs.get(String(ref)))
|
|
925
|
+
.find((old) => old && String(old.subject) === String(f.subject));
|
|
926
|
+
if (confirmed) {
|
|
927
|
+
this.reinforce(confirmed.id, citations, now);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const discretion = secret
|
|
931
|
+
? "secret"
|
|
932
|
+
: privateChat || f.discretion === "private"
|
|
933
|
+
? "private"
|
|
934
|
+
: "open";
|
|
935
|
+
const id = this.insert({
|
|
936
|
+
session,
|
|
937
|
+
subject: String(f.subject),
|
|
938
|
+
content,
|
|
939
|
+
type: String(f.type || "event").slice(0, 20),
|
|
940
|
+
confidence:
|
|
941
|
+
f.certainty === "inferred"
|
|
942
|
+
? Math.min(0.6, f.confidence)
|
|
943
|
+
: f.confidence,
|
|
944
|
+
importance: f.importance,
|
|
945
|
+
sources: citations,
|
|
946
|
+
discretion,
|
|
947
|
+
time: now,
|
|
948
|
+
});
|
|
949
|
+
if (!id) continue;
|
|
950
|
+
keptFacts.push({
|
|
951
|
+
subject: String(f.subject),
|
|
952
|
+
content,
|
|
953
|
+
discretion,
|
|
954
|
+
});
|
|
955
|
+
for (const ref of Array.isArray(f.supersedes) ? f.supersedes : []) {
|
|
956
|
+
const old = refs.get(String(ref));
|
|
957
|
+
if (old && String(old.subject) === String(f.subject))
|
|
958
|
+
this.supersede(old.id, id);
|
|
959
|
+
}
|
|
960
|
+
for (const ref of Array.isArray(f.conflicts) ? f.conflicts : []) {
|
|
961
|
+
const old = refs.get(String(ref));
|
|
962
|
+
if (old && String(old.subject) === String(f.subject))
|
|
963
|
+
this.conflict(id, old.id, f.sources, now);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
db.prepare(
|
|
967
|
+
"INSERT OR IGNORE INTO core_stages VALUES (?,?,?,?,?,?)",
|
|
968
|
+
).run(
|
|
969
|
+
randomUUID(),
|
|
970
|
+
session,
|
|
971
|
+
block[0].seq,
|
|
972
|
+
last,
|
|
973
|
+
now,
|
|
974
|
+
JSON.stringify({ summary, facts: keptFacts }),
|
|
975
|
+
);
|
|
976
|
+
this.anticipate(value.anticipations, {
|
|
977
|
+
block,
|
|
978
|
+
session,
|
|
979
|
+
names,
|
|
980
|
+
privateChat,
|
|
981
|
+
now,
|
|
982
|
+
});
|
|
983
|
+
this.mind?.episodes?.record(value.episodes, {
|
|
984
|
+
block,
|
|
985
|
+
session,
|
|
986
|
+
refs: threads.refs,
|
|
987
|
+
names,
|
|
988
|
+
privateChat,
|
|
989
|
+
now,
|
|
990
|
+
});
|
|
991
|
+
// What she herself said and meant becomes part of who she is. Her
|
|
992
|
+
// opinions about topics are not: one stretch of talk can add only a
|
|
993
|
+
// couple of views, and the rest stays in the summary.
|
|
994
|
+
const valid = new Set(
|
|
995
|
+
block.filter((m) => m.role === "assistant").map((m) => `m:${m.seq}`),
|
|
996
|
+
);
|
|
997
|
+
let views = 0;
|
|
998
|
+
for (const note of (Array.isArray(value.self) ? value.self : []).slice(
|
|
999
|
+
0,
|
|
1000
|
+
6,
|
|
1001
|
+
)) {
|
|
1002
|
+
if (note?.kind === "view" && ++views > MEMORY_VIEWS_PER_BLOCK)
|
|
1003
|
+
continue;
|
|
1004
|
+
this.mind?.self.propose(
|
|
1005
|
+
{ ...note, action: "new", session },
|
|
1006
|
+
{ valid, origin: "memory", time: now },
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
db.prepare(
|
|
1010
|
+
"INSERT INTO core_cursors VALUES (?,?) ON CONFLICT(session_id) DO UPDATE SET seq=excluded.seq",
|
|
1011
|
+
).run(session, last);
|
|
1012
|
+
db.exec("COMMIT");
|
|
1013
|
+
this.lastAttempt.delete(session);
|
|
1014
|
+
backoff?.succeed(backoffKey);
|
|
1015
|
+
} catch (e) {
|
|
1016
|
+
db.exec("ROLLBACK");
|
|
1017
|
+
throw e;
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
if (backoff && blockCursor !== null)
|
|
1021
|
+
this.#failed(backoffKey, blockCursor, session, error, now);
|
|
1022
|
+
throw error;
|
|
1023
|
+
} finally {
|
|
1024
|
+
this.busy.delete(session);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// A provider refusal or outage only waits. A block whose notes keep coming
|
|
1028
|
+
// back malformed is halved, and the smallest block that still fails is set
|
|
1029
|
+
// aside with its reason so the stretches after it can be remembered.
|
|
1030
|
+
#failed(key, cursor, session, error, now) {
|
|
1031
|
+
const backoff = this.mind.backoff;
|
|
1032
|
+
if (!isFormatFailure(error)) {
|
|
1033
|
+
backoff.fail(key, error, now, { minimumMs: providerWaitMs(error) });
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const old = backoff.state(key).meta || {};
|
|
1037
|
+
const same = old.cursor === cursor;
|
|
1038
|
+
const failures = (same ? old.formatFailures || 0 : 0) + 1;
|
|
1039
|
+
const blockUsers = same && old.blockUsers ? old.blockUsers : BLOCK_USERS;
|
|
1040
|
+
if (failures < 2) {
|
|
1041
|
+
backoff.fail(key, error, now, {
|
|
1042
|
+
meta: { cursor, formatFailures: failures, blockUsers },
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (blockUsers > MIN_BLOCK_USERS) {
|
|
1047
|
+
backoff.fail(key, error, now, {
|
|
1048
|
+
meta: {
|
|
1049
|
+
cursor,
|
|
1050
|
+
formatFailures: 0,
|
|
1051
|
+
blockUsers: Math.max(MIN_BLOCK_USERS, Math.floor(blockUsers / 2)),
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const db = this.repo.db;
|
|
1057
|
+
const rows = this.repo
|
|
1058
|
+
.eventsAfter(session, cursor, { limit: 200 })
|
|
1059
|
+
.filter((m) => m.time <= now);
|
|
1060
|
+
const users = rows.filter((m) => m.role === "user");
|
|
1061
|
+
const next = users[blockUsers]?.seq ?? Number.MAX_SAFE_INTEGER;
|
|
1062
|
+
const block = rows.filter((m) => m.seq < next);
|
|
1063
|
+
if (!block.length) {
|
|
1064
|
+
backoff.fail(key, error, now);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1068
|
+
try {
|
|
1069
|
+
db.prepare("INSERT OR IGNORE INTO core_stages VALUES (?,?,?,?,?,?)").run(
|
|
1070
|
+
randomUUID(),
|
|
1071
|
+
session,
|
|
1072
|
+
block[0].seq,
|
|
1073
|
+
block.at(-1).seq,
|
|
1074
|
+
now,
|
|
1075
|
+
JSON.stringify({
|
|
1076
|
+
summary: "",
|
|
1077
|
+
facts: [],
|
|
1078
|
+
skipped: text(error.message, 200),
|
|
1079
|
+
}),
|
|
1080
|
+
);
|
|
1081
|
+
db.prepare(
|
|
1082
|
+
"INSERT INTO core_cursors VALUES (?,?) ON CONFLICT(session_id) DO UPDATE SET seq=excluded.seq",
|
|
1083
|
+
).run(session, block.at(-1).seq);
|
|
1084
|
+
db.exec("COMMIT");
|
|
1085
|
+
} catch (writeError) {
|
|
1086
|
+
db.exec("ROLLBACK");
|
|
1087
|
+
throw writeError;
|
|
1088
|
+
}
|
|
1089
|
+
backoff.succeed(key);
|
|
1090
|
+
}
|
|
1091
|
+
}
|