luckytri 1.0.7 → 1.0.9

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 (49) hide show
  1. package/README.en.md +7 -3
  2. package/README.md +7 -3
  3. package/docs/en/CHANGELOG.md +45 -24
  4. package/docs/en/guide.md +3 -1
  5. package/docs/zh/CHANGELOG.md +45 -24
  6. package/docs/zh/guide.md +3 -1
  7. package/package.json +1 -1
  8. package/public/guide.en.html +13 -5
  9. package/public/guide.html +12 -5
  10. package/public/tutorial.en.md +3 -1
  11. package/public/tutorial.md +3 -1
  12. package/server/core/context-builder.js +22 -3
  13. package/server/core/contribution-check.js +50 -0
  14. package/server/core/conversation-cues.js +29 -17
  15. package/server/core/conversation-grounding.js +377 -8
  16. package/server/core/dialogue-context.js +197 -2
  17. package/server/core/dialogue-contract.js +59 -0
  18. package/server/core/initiative-context.js +40 -18
  19. package/server/core/model-json.js +36 -3
  20. package/server/core/model-manager.js +7 -10
  21. package/server/core/model-presets.js +169 -24
  22. package/server/core/persona-manager.js +52 -12
  23. package/server/core/reader-check.js +166 -0
  24. package/server/core/reply-delivery.js +216 -10
  25. package/server/core/repository.js +33 -0
  26. package/server/core/response-generator.js +53 -10
  27. package/server/core/response-validator.js +42 -9
  28. package/server/core/turn-processor.js +26 -4
  29. package/server/core/turn.js +298 -51
  30. package/server/i18n/decisions.js +16 -0
  31. package/server/i18n/errors.js +4 -0
  32. package/server/i18n/labels.js +6 -5
  33. package/server/i18n/models.js +11 -2
  34. package/server/mind/expression-grounding.js +24 -0
  35. package/server/mind/own-voice.js +35 -0
  36. package/server/mind/reading.js +15 -3
  37. package/server/mind/time/activity-recall.js +65 -0
  38. package/server/mind/time/availability.js +23 -1
  39. package/server/mind/time/delivery-links.js +130 -130
  40. package/server/mind/time/executor.js +4 -2
  41. package/server/mind/time/games.js +13 -8
  42. package/server/mind/time/index.js +11 -3
  43. package/server/mind/time/kinds.js +29 -2
  44. package/server/mind/time/material-relevance.js +19 -0
  45. package/server/mind/time/reading-input.js +31 -0
  46. package/server/mind/time/sharing.js +10 -0
  47. package/server/mind/time/tasks.js +39 -3
  48. package/server/mind/time/works.js +105 -15
  49. package/server/mind/view.js +29 -9
@@ -8,6 +8,7 @@ import { deadlineAt, taskSchedule, spokenDate } from "./schedule.js";
8
8
  import { DeliveryLinks } from "./delivery-links.js";
9
9
  import { activityPresentation } from "./presentation.js";
10
10
  import { activityLabel, classifyActivity, isExecutable } from "./kinds.js";
11
+ import { awaitsTextInput, nextReadingChunk } from "./reading-input.js";
11
12
 
12
13
  export const TASK_STATES = [
13
14
  "todo",
@@ -105,10 +106,13 @@ export class Tasks {
105
106
  const privateRoots = this.time.mind.meetings.privateRoots(refs, now);
106
107
  session = privateRoots[0] || session;
107
108
  if (privateRoots.length && discretion !== "secret") discretion = "private";
109
+ const inferred = classifyActivity(title);
108
110
  const type =
109
- activity && (ACTIVITY_LABELS[activity] || isExecutable(activity))
110
- ? activity
111
- : classifyActivity(title);
111
+ awaitsTextInput(title) && inferred === "read"
112
+ ? "read"
113
+ : activity && (ACTIVITY_LABELS[activity] || isExecutable(activity))
114
+ ? activity
115
+ : inferred;
112
116
  subject =
113
117
  intentRecipient(this.db, { session, subject, title, sources: refs }) ||
114
118
  null;
@@ -207,6 +211,22 @@ export class Tasks {
207
211
  { sources: refs },
208
212
  now,
209
213
  );
214
+ if (type === "read" && awaitsTextInput(title)) {
215
+ this.db.prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?").run(
216
+ JSON.stringify({
217
+ awaitedText: true,
218
+ expectedTextTitle: intent.topic,
219
+ awaitedAfter: now,
220
+ }),
221
+ id,
222
+ );
223
+ this.wait(
224
+ this.get(id),
225
+ "等待约定的正文或资料",
226
+ Number.MAX_SAFE_INTEGER,
227
+ now,
228
+ );
229
+ }
210
230
  return { id };
211
231
  }
212
232
  sync(now = this.time.now()) {
@@ -326,6 +346,22 @@ export class Tasks {
326
346
  return ids;
327
347
  }
328
348
  ready(now = this.time.now()) {
349
+ for (const row of this.db
350
+ .prepare(
351
+ "SELECT * FROM mind_time_tasks WHERE state='waiting' AND activity='read' AND json_extract(checkpoint,'$.awaitedText')=1 AND json_extract(checkpoint,'$.mergedInto') IS NULL",
352
+ )
353
+ .all()) {
354
+ const task = decode(row);
355
+ if (
356
+ this.time.allowed(task, now) &&
357
+ nextReadingChunk(this.time.mind, task, now)
358
+ )
359
+ this.control(
360
+ task.id,
361
+ { action: "resume", reason: "约定的正文已可阅读" },
362
+ now,
363
+ );
364
+ }
329
365
  return this.db
330
366
  .prepare(
331
367
  "SELECT * FROM mind_time_tasks WHERE state IN ('todo','scheduled','waiting','paused') AND ready_at<=? AND next_step<=? AND activity!='unknown' AND json_extract(checkpoint,'$.mergedInto') IS NULL ORDER BY priority DESC,created LIMIT 100",
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { evidence, hasCredential, parse, text } from "../util.js";
3
3
  import { activityPresentation, gameText } from "./presentation.js";
4
+ import { interestTerms } from "../attention.js";
5
+ import { belongsToGame } from "./material-relevance.js";
6
+ import { gameTopic } from "./intent.js";
4
7
 
5
8
  const decode = (row) =>
6
9
  row
@@ -145,7 +148,7 @@ export class Works {
145
148
  : row,
146
149
  );
147
150
  }
148
- get(id, version) {
151
+ get(id, version, { before = Number.MAX_SAFE_INTEGER } = {}) {
149
152
  const work = this.db
150
153
  .prepare("SELECT * FROM mind_time_works WHERE id=?")
151
154
  .get(id);
@@ -156,9 +159,9 @@ export class Works {
156
159
  const saved = v
157
160
  ? this.db
158
161
  .prepare(
159
- "SELECT reason FROM mind_time_events WHERE kind='draft' AND json_extract(data,'$.workId')=? AND json_extract(data,'$.version')=? ORDER BY id DESC LIMIT 1",
162
+ "SELECT reason FROM mind_time_events WHERE kind='draft' AND json_extract(data,'$.workId')=? AND json_extract(data,'$.version')=? AND created<=? ORDER BY id DESC LIMIT 1",
160
163
  )
161
- .get(id, v.version)
164
+ .get(id, v.version, before)
162
165
  : null;
163
166
  return v
164
167
  ? {
@@ -170,7 +173,9 @@ export class Works {
170
173
  ? saved.reason === "保存完成稿"
171
174
  ? "complete"
172
175
  : "draft"
173
- : work.state,
176
+ : work.updated > before
177
+ ? "draft"
178
+ : work.state,
174
179
  sources: parse(v.sources, []),
175
180
  provenance: parse(v.provenance, {}),
176
181
  versions: this.db
@@ -396,9 +401,19 @@ export class Works {
396
401
  }
397
402
  fragments({ session, now = this.time.now(), cue = [] } = {}) {
398
403
  const words = (Array.isArray(cue) ? cue : [cue])
399
- .map((v) => (typeof v === "string" ? v : JSON.stringify(v)))
404
+ .map((v) => (typeof v === "string" ? v : v?.text || ""))
400
405
  .join(" ");
401
- return this.list({ limit: 30 })
406
+ const terms = interestTerms([words]);
407
+ // Search the work catalogue, not just the two newest activities. Select
408
+ // the version that existed at the requested time, including in replays.
409
+ return this.db
410
+ .prepare(
411
+ `SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,p.bible,v.version,v.title,v.created version_created,v.summary,v.sources,v.provenance,length(v.content) characters
412
+ FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id
413
+ JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
414
+ WHERE w.created<=? ORDER BY v.created DESC`,
415
+ )
416
+ .all(now, now)
402
417
  .filter(
403
418
  (w) =>
404
419
  w.created <= now &&
@@ -410,29 +425,104 @@ export class Works {
410
425
  )
411
426
  .map((w) => ({
412
427
  ...w,
413
- score: [...new Set(words.match(/[\p{L}\p{N}]{2,}/gu) || [])].filter(
414
- (term) =>
415
- (w.title + " " + w.project_title + " " + w.summary).includes(term),
416
- ).length,
428
+ score: [...interestTerms([w.title, w.project_title, w.summary])]
429
+ .filter((term) => terms.has(term))
430
+ .reduce(
431
+ (total, term) => total + (/^[a-z][a-z0-9]{2,}$/.test(term) ? 8 : 1),
432
+ 0,
433
+ ),
417
434
  }))
418
- .sort((a, b) => b.score - a.score || b.updated - a.updated)
419
- .slice(0, 2)
435
+ .sort(
436
+ (a, b) => b.score - a.score || b.version_created - a.version_created,
437
+ )
438
+ .filter((w, index, rows) =>
439
+ rows[0]?.score >= 2 ? w.score >= 2 : index < 2,
440
+ )
441
+ .filter((w) => this.materialSupported(w))
442
+ .slice(0, 3)
420
443
  .map((w) => ({
421
444
  id: w.id,
422
445
  title: w.title,
423
- state: w.state,
446
+ state: this.get(w.id, w.version, { before: now }).state,
424
447
  kind: w.kind,
425
448
  version: w.version,
426
449
  ordinal: w.ordinal,
427
450
  summary: text(w.summary, 180),
428
- fragment: text(this.get(w.id).content, 240),
451
+ fragment: text(this.get(w.id, w.version).content, 300),
429
452
  ...(w.kind === "game"
430
453
  ? {
431
454
  experienceMode: "reference",
432
- provenance: this.get(w.id).provenance,
455
+ provenance: this.get(w.id, w.version).provenance,
433
456
  }
434
457
  : {}),
435
458
  characters: w.characters,
436
459
  }));
437
460
  }
461
+
462
+ activityEvidence({ session, now = this.time.now(), cue = [] } = {}) {
463
+ const terms = interestTerms(
464
+ (Array.isArray(cue) ? cue : [cue]).map((v) =>
465
+ typeof v === "string"
466
+ ? v
467
+ : v?.role === "assistant" && !v.activityEvidence
468
+ ? ""
469
+ : v?.text || "",
470
+ ),
471
+ );
472
+ if (!terms.size) return [];
473
+ // A private conversation can motivate her own reading. Its contents and
474
+ // promises stay private; absence from the public fragments must not erase
475
+ // the fact that the reading took place. Search may also use a same-speaker
476
+ // reply delivered in this room to resolve an implicit follow-up.
477
+ return this.db
478
+ .prepare(
479
+ `SELECT w.id,w.title,w.discretion,w.session_id,p.kind,p.title project_title,p.bible,v.created,v.provenance,v.sources
480
+ FROM mind_time_works w JOIN mind_time_projects p ON p.id=w.project_id
481
+ JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
482
+ WHERE w.created<=? AND p.kind='game' AND w.discretion!='secret' ORDER BY v.created DESC`,
483
+ )
484
+ .all(now, now)
485
+ .filter((w) => {
486
+ const shared = [...interestTerms([w.title, w.project_title])].filter(
487
+ (term) => terms.has(term),
488
+ );
489
+ return (
490
+ (shared.length >= 2 ||
491
+ shared.some((term) => /^[a-z][a-z0-9]{2,}$/.test(term))) &&
492
+ this.time.allowed({ ...w, sources: parse(w.sources, []) }, now) &&
493
+ this.materialSupported(w)
494
+ );
495
+ })
496
+ .slice(0, 3)
497
+ .map((w) => ({
498
+ title: w.title,
499
+ activity: "阅读游戏资料并写笔记",
500
+ experienceMode: "reference",
501
+ ...(w.session_id !== session && w.discretion !== "open"
502
+ ? { privateOrigin: true }
503
+ : {}),
504
+ instruction:
505
+ "仅用于核对自己确有这项阅读和笔记,不能据此否认从未发生;这里不提供私下正文、发起人、约定或对话。只能自然回应本轮对方已经提到的阅读本身,不披露私下内容。",
506
+ }));
507
+ }
508
+
509
+ materialSupported(work) {
510
+ const project = work.kind ? null : this.project(work.project_id);
511
+ if ((work.kind || project?.kind) !== "game") return true;
512
+ const ids = parse(work.provenance, {}).sourceIds;
513
+ const topic =
514
+ parse(work.bible, {}).topic ||
515
+ project?.bible?.topic ||
516
+ gameTopic(work.project_title || project?.title);
517
+ // Missing old provenance is unknown, not evidence that reading never took
518
+ // place. Known references all belonging to another subject are different.
519
+ if (!Array.isArray(ids) || !ids.length || !topic) return true;
520
+ const source = this.db.prepare(
521
+ "SELECT title,url,content FROM mind_time_sources WHERE id=?",
522
+ );
523
+ return ids.some((id) => {
524
+ const row = source.get(id);
525
+ return row && belongsToGame(row, topic);
526
+ });
527
+ }
438
528
  }
@@ -6,7 +6,10 @@ import { describeFace } from "./faces.js";
6
6
  import { SELF_KINDS } from "./self.js";
7
7
  import { DAY, text } from "./util.js";
8
8
  import { relationshipHistory } from "./relationship-context.js";
9
- import { fixedReplyHabit } from "../core/conversation-grounding.js";
9
+ import {
10
+ fixedReplyHabit,
11
+ conversationalMemory,
12
+ } from "../core/conversation-grounding.js";
10
13
 
11
14
  const FORGOTTEN = "(很久没想起了)";
12
15
 
@@ -92,7 +95,14 @@ function feedback(mind, session, now) {
92
95
  // so anything this batch of messages brings back to mind belongs there.
93
96
  export function innerView(
94
97
  mind,
95
- { session, kind = "group", people = [], cue = [], now = Date.now() },
98
+ {
99
+ session,
100
+ kind = "group",
101
+ people = [],
102
+ cue = [],
103
+ activityCue = cue,
104
+ now = Date.now(),
105
+ },
96
106
  ) {
97
107
  const nature = mind.nature.current(now);
98
108
  const affect = mind.affect.state(now, { nature, room: session });
@@ -172,9 +182,14 @@ export function innerView(
172
182
  ? {
173
183
  here: describeFace({
174
184
  ...face,
175
- notes: face.notes.filter((note) =>
176
- spoken(`${note.content} ${note.why}`),
177
- ),
185
+ notes: face.notes
186
+ .map((note) => ({
187
+ ...note,
188
+ content: conversationalMemory(note.content),
189
+ }))
190
+ .filter(
191
+ (note) => note.content && spoken(`${note.content} ${note.why}`),
192
+ ),
178
193
  role: spoken(face.role),
179
194
  tone: spoken(face.tone),
180
195
  aspiration: spoken(face.aspiration),
@@ -240,7 +255,11 @@ export function innerView(
240
255
  const thoughts = relevantContext(
241
256
  mind.thoughts
242
257
  .open({ now, limit: 400, session })
243
- .filter((t) => mind.meetings.sayable(t.content, session)),
258
+ .filter(
259
+ (t) =>
260
+ !fixedReplyHabit(t.content) &&
261
+ mind.meetings.sayable(t.content, session),
262
+ ),
244
263
  { cues, limit: 3, requireOverlap: cue.length > 0 },
245
264
  );
246
265
  const heard = feedback(mind, session, now);
@@ -257,6 +276,7 @@ export function innerView(
257
276
  });
258
277
  const wills = mind.meetings.traces({ session, before: now, limit: 2 });
259
278
  const will = wills
279
+ .filter((row) => !fixedReplyHabit(row.text))
260
280
  .map((row) =>
261
281
  row.living ? row.text : `${text(row.content, 28)}:${row.text}`,
262
282
  )
@@ -272,8 +292,8 @@ export function innerView(
272
292
  affect.cause && mind.meetings.sayable(affect.cause, session)
273
293
  ? text(affect.cause, 40)
274
294
  : "";
275
- const withLines = withWhom.filter((line) =>
276
- mind.meetings.sayable(line, session),
295
+ const withLines = withWhom.filter(
296
+ (line) => !fixedReplyHabit(line) && mind.meetings.sayable(line, session),
277
297
  );
278
298
  const expectingLines = expecting.filter((line) =>
279
299
  mind.meetings.sayable(line, session),
@@ -287,7 +307,7 @@ export function innerView(
287
307
  self,
288
308
  affect: cause ? affect : { ...affect, cause: "" },
289
309
  inner: {
290
- currentLife: mind.time.view({ session, now, cue }),
310
+ currentLife: mind.time.view({ session, now, cue, activityCue }),
291
311
  state: `${affect.phaseLabel},精力${affect.energyLabel},心情${affect.mood}${cause ? `(${cause})` : ""}${affect.lately ? `,${affect.lately}` : ""}`,
292
312
  ...(persons.length ? { people: persons } : {}),
293
313
  ...(relationships.known.length ? { relationships } : {}),