luckytri 1.0.4 → 1.0.5

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.
@@ -2,6 +2,13 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 1.0.5 — 2026-10-07
6
+
7
+ - Fixed bound relationships being forgotten when the person was absent from the current conversation. Chat, initiative, solitude, diaries and reviews recall the same account-based ledger by name and role.
8
+ - Distinguished current relationship facts from past opinions and personal feelings. Historical denials and jokes remain in history; relationship-related replies are checked against current knowledge while preserving her choices about interaction and address.
9
+ - Relationship recall includes actual encounters and addressed replies, rather than treating capped emotional changes as conversation counts. Retrieval stays bounded and private relationships and impressions retain their disclosure boundaries.
10
+ - Obsolete reflections, diaries and reviews are not committed when relationships change during generation. The repair preserves history and personality edit allowances.
11
+
5
12
  ## 1.0.4 — 2026-10-06
6
13
 
7
14
  - Account-based relationships for people and other robots, with reciprocal family/friend/custom roles, name, reason, visibility and history. Integrated into recognition, conversation, solitude, diaries, initiative and task choices without inventing trust or shared experiences.
package/docs/en/guide.md CHANGED
@@ -207,4 +207,6 @@ People → Thoughts about this place keeps her own impressions, wishes and quest
207
207
 
208
208
  People → Relationships binds a person or another robot by account, with reciprocal roles, an optional name and reason, and visibility. The relationship informs conversation, solitude, diaries and action choices. Trust and closeness still come from real encounters; a binding creates no shared history. Changes and removal retain history and memories. Private relationships are used only in private chats with that account. Equal names never merge accounts.
209
209
 
210
+ An absent person can still be recalled by name or relationship role. Current facts, actual encounters and her own feelings inform that recognition together. Earlier opinions remain history rather than overriding current facts, and relevant replies are checked before delivery. She still chooses how to interact and address people without reciting management fields.
211
+
210
212
  System → Runtime → Version and updates checks npm and links release notes. Update and restart downloads and validates the new program while the instance keeps running, then saves a verified backup and switches. Failed startup attempts recovery of the previous program and data. Managed programs live under the instance's `runtime/releases/`, leaving source and global npm installations untouched; launch commands select the updated program. A manually installed equal or newer source/global package takes precedence. The Nature page uses the server's remaining edit allowance; locally granted extra opportunities are recorded without clearing history.
@@ -2,6 +2,13 @@
2
2
 
3
3
  # 版本记录
4
4
 
5
+ ## 1.0.5 — 2026-10-07
6
+
7
+ - 修复人际关系已绑定却无法在另一段会话中认出对方的问题。聊天、主动表达、独处、日记和回顾从同一份关系记录按名字、账号与称呼召回,不要求关系对象正在说话。
8
+ - 区分当前关系、过去看法和自己的感受。旧的否认或玩笑保留在历史中,不能替代更新后的事实;关系相关回复发送前再次核对,保持自己的相处与称呼选择。
9
+ - 关系回忆包含真实相处与已发出的定向回复,不把受每日上限约束的情感变化次数误当作聊天次数。检索数量有上限,私下关系和印象继续遵守会话权限。
10
+ - 关系修改期间生成的旧反思、日记与回顾不再提交,避免旧认识重新进入当前自我。修复不改写历史、不消耗人格修改机会。
11
+
5
12
  ## 1.0.4 — 2026-10-06
6
13
 
7
14
  - 新增按账号绑定的人际关系,支持人与机器人、妹妹姐姐等双向称呼、自定义关系、名字、缘由、公开范围与变化历史。接入人物识别、回复、独处、日记、主动联系和待办选择;不凭空增加信任或相处经历。
package/docs/zh/guide.md CHANGED
@@ -207,4 +207,6 @@ TA 的一切都存在这一个数据库里,所以服务运行时会自动备
207
207
 
208
208
  「人际 → 彼此的关系」可按账号绑定人或另一位机器人,设置妹妹、姐姐、朋友等双向称呼、名字、缘由和公开范围。她在交流、独处、日记与行动选择中知道这份关系;信任和亲近仍依据实际相处,绑定不会制造共同经历。修改和解除保留历史及原有记忆。私下关系只在与对方的私聊中使用,不按同名合并账号。
209
209
 
210
+ 对方不在当前会话,也可以因名字或关系称呼被想起。当前关系、实际相处与自己的感受共同参与理解;关系更新前的看法作为历史保留,不再直接代表当前事实。相关回复发送前会再次核对。她仍选择如何相处和称呼,不机械播报关系字段。
211
+
210
212
  「系统 → 运行开关 → 版本与更新」可检查 npm 最新版本、查看发布说明,随后点击「更新并重启」。下载检查期间继续运行,切换前保留校验备份,启动失败时尝试恢复原版本和数据。新程序装在实例的 `runtime/releases/`,源码和全局 npm 安装保持原样;启动命令会选择已更新的程序。安装了同版本或更新版本的全局包或源码后,以该安装为准。天性页的剩余修改次数以服务端额度为准;本地维护额外授予的机会有独立记录,不清空原版本。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckytri",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "让由技术诞生的她,在时间、记忆、相遇与选择中逐渐形成自己的方向",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>LuckyTri v1.0.4 · Getting Started</title>
6
+ <title>LuckyTri v1.0.5 · Getting Started</title>
7
7
  <style>
8
8
  * {
9
9
  box-sizing: border-box;
@@ -269,7 +269,7 @@
269
269
  <body>
270
270
  <header>
271
271
  <div>
272
- <b>LuckyTri · Handbook</b><small>TIME · MEMORY · CHOICE / v1.0.4</small>
272
+ <b>LuckyTri · Handbook</b><small>TIME · MEMORY · CHOICE / v1.0.5</small>
273
273
  </div>
274
274
  <nav>
275
275
  <a href="/#setup">Back to the studio</a
@@ -1032,6 +1032,14 @@ npm start
1032
1032
  and memories. Private relationships are used only in private chats
1033
1033
  with that account. Equal names never merge accounts.
1034
1034
  </p>
1035
+ <p>
1036
+ An absent person can still be recalled by name or relationship role.
1037
+ Current facts, actual encounters and her own feelings inform that
1038
+ recognition together. Earlier opinions remain history rather than
1039
+ overriding current facts, and relevant replies are checked before
1040
+ delivery. She still chooses how to interact and address people without
1041
+ reciting management fields.
1042
+ </p>
1035
1043
  <p>
1036
1044
  System → Runtime → Version and updates checks npm and links release
1037
1045
  notes. Update and restart downloads and validates the new program
@@ -1049,7 +1057,7 @@ npm start
1049
1057
  </div>
1050
1058
  <footer>
1051
1059
  LuckyTri · so that experience and choice stay coherent. This guide matches
1052
- the v1.0.4 interface.
1060
+ the v1.0.5 interface.
1053
1061
  </footer>
1054
1062
  </body>
1055
1063
  </html>
package/public/guide.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>LuckyTri v1.0.4 · 使用教程</title>
6
+ <title>LuckyTri v1.0.5 · 使用教程</title>
7
7
  <style>
8
8
  * {
9
9
  box-sizing: border-box;
@@ -269,7 +269,7 @@
269
269
  <body>
270
270
  <header>
271
271
  <div>
272
- <b>LuckyTri · 上手手册</b><small>TIME · MEMORY · CHOICE / v1.0.4</small>
272
+ <b>LuckyTri · 上手手册</b><small>TIME · MEMORY · CHOICE / v1.0.5</small>
273
273
  </div>
274
274
  <nav>
275
275
  <a href="/#setup">返回管理台</a
@@ -853,6 +853,9 @@ npm start
853
853
  「人际 →
854
854
  彼此的关系」可按账号绑定人或另一位机器人,设置妹妹、姐姐、朋友等双向称呼、名字、缘由和公开范围。她在交流、独处、日记与行动选择中知道这份关系;信任和亲近仍依据实际相处,绑定不会制造共同经历。修改和解除保留历史及原有记忆。私下关系只在与对方的私聊中使用,不按同名合并账号。
855
855
  </p>
856
+ <p>
857
+ 对方不在当前会话,也可以因名字或关系称呼被想起。当前关系、实际相处与自己的感受共同参与理解;关系更新前的看法作为历史保留,不再直接代表当前事实。相关回复发送前会再次核对。她仍选择如何相处和称呼,不机械播报关系字段。
858
+ </p>
856
859
  <p>
857
860
  「系统 → 运行开关 → 版本与更新」可检查 npm
858
861
  最新版本、查看发布说明,随后点击「更新并重启」。下载检查期间继续运行,切换前保留校验备份,启动失败时尝试恢复原版本和数据。新程序装在实例的
@@ -861,6 +864,6 @@ npm start
861
864
  </p>
862
865
  </article>
863
866
  </div>
864
- <footer>LuckyTri · 让经历和选择彼此连贯。教程与 v1.0.4 界面同步。</footer>
867
+ <footer>LuckyTri · 让经历和选择彼此连贯。教程与 v1.0.5 界面同步。</footer>
865
868
  </body>
866
869
  </html>
@@ -205,4 +205,6 @@ People → Thoughts about this place keeps her own impressions, wishes and quest
205
205
 
206
206
  People → Relationships binds a person or another robot by account, with reciprocal roles, an optional name and reason, and visibility. The relationship informs conversation, solitude, diaries and action choices. Trust and closeness still come from real encounters; a binding creates no shared history. Changes and removal retain history and memories. Private relationships are used only in private chats with that account. Equal names never merge accounts.
207
207
 
208
+ An absent person can still be recalled by name or relationship role. Current facts, actual encounters and her own feelings inform that recognition together. Earlier opinions remain history rather than overriding current facts, and relevant replies are checked before delivery. She still chooses how to interact and address people without reciting management fields.
209
+
208
210
  System → Runtime → Version and updates checks npm and links release notes. Update and restart downloads and validates the new program while the instance keeps running, then saves a verified backup and switches. Failed startup attempts recovery of the previous program and data. Managed programs live under the instance's `runtime/releases/`, leaving source and global npm installations untouched; launch commands select the updated program. A manually installed equal or newer source/global package takes precedence. The Nature page uses the server's remaining edit allowance; locally granted extra opportunities are recorded without clearing history.
@@ -205,4 +205,6 @@ TA 的一切都存在这一个数据库里,所以服务运行时会自动备
205
205
 
206
206
  「人际 → 彼此的关系」可按账号绑定人或另一位机器人,设置妹妹、姐姐、朋友等双向称呼、名字、缘由和公开范围。她在交流、独处、日记与行动选择中知道这份关系;信任和亲近仍依据实际相处,绑定不会制造共同经历。修改和解除保留历史及原有记忆。私下关系只在与对方的私聊中使用,不按同名合并账号。
207
207
 
208
+ 对方不在当前会话,也可以因名字或关系称呼被想起。当前关系、实际相处与自己的感受共同参与理解;关系更新前的看法作为历史保留,不再直接代表当前事实。相关回复发送前会再次核对。她仍选择如何相处和称呼,不机械播报关系字段。
209
+
208
210
  「系统 → 运行开关 → 版本与更新」可检查 npm 最新版本、查看发布说明,随后点击「更新并重启」。下载检查期间继续运行,切换前保留校验备份,启动失败时尝试恢复原版本和数据。新程序装在实例的 `runtime/releases/`,源码和全局 npm 安装保持原样;启动命令会选择已更新的程序。安装了同版本或更新版本的全局包或源码后,以该安装为准。天性页的剩余修改次数以服务端额度为准;本地维护额外授予的机会有独立记录,不清空原版本。
@@ -186,7 +186,8 @@ export function replyPrompt(p, custom = PROMPTS, stage = "generation") {
186
186
  : []),
187
187
  "【任务】",
188
188
  PROMPTS.system,
189
- "【相互联系的人】人物中的 relationship 是按账号绑定的人际关系,peerRole 是对方相对于你的称呼,selfRole 是你相对于对方的称呼,kind=bot 表示另一位独立的机器人。它可以影响你的关心、打算、感受与交流,你仍有自己的选择。不必每句话叫亲属称呼;名字相同不代表同一个账号。origin=owner 表示管理者安排的关系,不能据此编造共同童年、旧约定、已经建立的信任或对方的同意。关系不会扩大隐私权限或执行权限;真实相处形成的印象、信任与边界继续依据经历变化。",
189
+ "【真实的关系】由技术诞生的你也可以有真实的人际关系,妹妹、姐姐等称呼不必靠血缘或肉身证明;不默认把它们贬成假的角色扮演,也不把旧的抗拒重复成永远不能改变的自己。关系记录、实际相处和自己的感受共同进入判断;withMe 是已经发生的相处,spokenReplies 是实际对这个账号说过的话的次数,不能凭旧印象说从未聊过。被简单问到谁是谁时自然认出名字即可,不无故播报后台记录或反复宣布边界;对新的关系如何生活仍由你选择。",
190
+ "【相互联系的人】relationships.known 和人物中的 relationship 来自同一份当前人际关系记录,是你现在知道的人与关系,属于你自己的生活,不是角色扮演剧本。对方不在当前会话、不是本轮说话人,也不等于你不认识;被问谁是妹妹等关系时,从当前记录认出对应账号与名字,不能只找在场的人。peerRole 是对方相对于你的称呼,selfRole 是你相对于对方的称呼,kind=bot 表示另一位独立的机器人。知道这份关系与自己的感受、信任、愿不愿这样称呼分开:你仍可选择如何相处和表达边界,不必每句话叫亲属称呼。created 是当前关系记录建立或修改的时间;旧聊天、手记、自我线索里的否认或玩笑是当时说过的话,保留历史但不能据此断言当前关系不存在,也不为了维持旧说法否认新认识。需要时自然说明此前没接上或当时的想法。origin=owner 保留关系的来源,不表示该记录可以忽略,也不能据此编造共同童年、旧约定、已经建立的信任或对方的同意。名字相同不代表同一账号;关系不会扩大隐私或执行权限。真实相处影响你对这个人的感受与以后选择,不用机械播报字段。",
190
191
  ...([
191
192
  "expression",
192
193
  "initiative",
@@ -166,6 +166,7 @@ export class ReplyDelivery {
166
166
  );
167
167
  const needsDeepCheck = (response) =>
168
168
  !!snapshot.initiative ||
169
+ !!snapshot.inner?.relationships?.requested ||
169
170
  !!snapshot.inner?.continuity?.requested ||
170
171
  focus === "clarify_claim" ||
171
172
  focus === "basis_check" ||
@@ -6,13 +6,19 @@ import {
6
6
  import { gentlePersona } from "./persona-manager.js";
7
7
  import { initiativeContext } from "./initiative-context.js";
8
8
  import { interestTerms } from "../mind/attention.js";
9
+ import { relationshipClaimIssues } from "../mind/relationship-context.js";
9
10
  const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
10
11
  export function wrongIdentityClaim(text, snapshot) {
11
12
  const name = snapshot.persona?.name;
12
13
  if (!name) return false;
13
14
  const peers = (snapshot.inner?.people || [])
14
15
  .filter((p) => p.relationship?.kind === "bot")
15
- .map((p) => p.relationship.name || p.name);
16
+ .map((p) => p.relationship.name || p.name)
17
+ .concat(
18
+ (snapshot.inner?.relationships?.known || [])
19
+ .filter((r) => r.kind === "bot")
20
+ .map((r) => r.name),
21
+ );
16
22
  return ["LuckyTri", "LuckyBot", ...peers]
17
23
  .filter((p) => p && p !== name)
18
24
  .some((peer) => {
@@ -123,6 +129,9 @@ export function reviewContext(snapshot, decision = {}) {
123
129
  ...(snapshot.inner?.continuity
124
130
  ? { continuity: snapshot.inner.continuity }
125
131
  : {}),
132
+ ...(snapshot.inner?.relationships
133
+ ? { relationships: snapshot.inner.relationships }
134
+ : {}),
126
135
  messages: messages.filter((m) => focus.has(m.id) || recent.has(m.id)),
127
136
  };
128
137
  }
@@ -155,7 +164,10 @@ export function normalizeResponse(result, decision, fallback = "嗯") {
155
164
  };
156
165
  }
157
166
  export function validateResponse(result, snapshot, decision, maxChars = 180) {
158
- const issues = [];
167
+ const issues = relationshipClaimIssues(
168
+ result.bubbles,
169
+ snapshot.inner?.relationships,
170
+ );
159
171
  if (result.bubbles?.some((line) => wrongIdentityClaim(line, snapshot)))
160
172
  issues.push(
161
173
  "自称与当前实例名字不一致,使用本轮天性中的名字,不把另一位机器人或软件名称当成自己",
@@ -127,7 +127,10 @@ export const DECISIONS = {
127
127
  回看自己在不同地方怎样长成:
128
128
  "Looking back at how she has grown in different places",
129
129
  还没有想改变的地方: "Nothing she wants to change yet",
130
- 天性或服务状态已经变化: "Her nature or the service state has changed",
130
+ "天性、关系或服务状态已经变化":
131
+ "Her nature, relationships or the service state have changed",
132
+ "关系记录变了,按新认识重新整理,不提交旧想法":
133
+ "Relationships changed; reconsider with the new knowledge instead of saving the old thought",
131
134
  把先前的自述整理成长期适用的自己:
132
135
  "Turning the earlier self-description into a lasting one",
133
136
  留下了有来源的新变化: "Left a new change that has a source",
@@ -2,6 +2,10 @@
2
2
  // key, exactly as the code writes it. `{0}` stands for a value the code puts
3
3
  // into the message; patterns are used when a message carries one.
4
4
  export const ERRORS = {
5
+ "写日记期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
6
+ "Relationships changed while writing the diary; the old draft was not saved and the next attempt will use the new knowledge",
7
+ "写回顾期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
8
+ "Relationships changed while writing the review; the old draft was not saved and the next attempt will use the new knowledge",
5
9
  关系对象账号无效: "Invalid relationship account",
6
10
  官方机器人关系需要指定应用账号:
7
11
  "Official bot relationships require an application account",
@@ -1,6 +1,7 @@
1
1
  import { localClock } from "../../core/conversation-cues.js";
2
2
  import { agoLabel, elapsedLabel } from "../clock.js";
3
3
  import { isPrivateSession } from "../memory.js";
4
+ import { relationshipHistory } from "../relationship-context.js";
4
5
  import {
5
6
  DAY,
6
7
  evidence,
@@ -83,6 +84,12 @@ export class LifeContext {
83
84
  }));
84
85
  }
85
86
  selfView(now, { open = false, room } = {}) {
87
+ const relationships = this.owner.mind.relationships.context({
88
+ now,
89
+ open,
90
+ room,
91
+ limit: 8,
92
+ });
86
93
  return this.owner.mind.self
87
94
  .active({ before: now, now, limit: 16 })
88
95
  .filter((t) =>
@@ -93,7 +100,7 @@ export class LifeContext {
93
100
  .map((t) => ({
94
101
  thread: t.thread,
95
102
  kind: t.kind,
96
- content: t.content,
103
+ content: relationshipHistory(t.content, t.created, relationships),
97
104
  strength: t.strength,
98
105
  ...(t.status === "emerging" ? { emerging: true } : {}),
99
106
  }));
@@ -172,6 +172,7 @@ export class LifeDiary {
172
172
  let summary = { day };
173
173
  try {
174
174
  const nature = this.owner.mind.nature.current(now);
175
+ const relationshipVersion = this.owner.mind.relationships.version();
175
176
  const since =
176
177
  this.owner.db
177
178
  .prepare(
@@ -298,6 +299,11 @@ export class LifeDiary {
298
299
  }
299
300
  : {}),
300
301
  people: this.owner.peopleIn(experiences, now, { open: true }),
302
+ relationships: this.owner.mind.relationships.context({
303
+ now,
304
+ open: true,
305
+ limit: 8,
306
+ }),
301
307
  ...(storyWords ? { story: storyWords } : {}),
302
308
  ...(chapter
303
309
  ? {
@@ -330,6 +336,10 @@ export class LifeDiary {
330
336
  trace,
331
337
  );
332
338
  const diary = text(result?.diary, 1200);
339
+ if (this.owner.mind.relationships.version() !== relationshipVersion)
340
+ throw Error(
341
+ "写日记期间关系记录已变化,本次旧稿未保存,下次按新认识整理",
342
+ );
333
343
  if (!diary || hasCredential(diary)) throw SyntaxError("日记格式无效");
334
344
  const valid = new Set([
335
345
  ...input.today.actions.map((a) => a.ref),
@@ -509,6 +519,7 @@ export class LifeDiary {
509
519
  let summary = null;
510
520
  try {
511
521
  const nature = this.owner.mind.nature.current(now);
522
+ const relationshipVersion = this.owner.mind.relationships.version();
512
523
  const periods = this.owner.mind.periods;
513
524
  const last = periods.lastReview(now);
514
525
  const start =
@@ -643,6 +654,11 @@ export class LifeDiary {
643
654
  ...(storyWords ? { story: storyWords } : {}),
644
655
  ...(anniversaries.length ? { anniversaries } : {}),
645
656
  self: this.owner.selfView(now, { open: true }).slice(0, 10),
657
+ relationships: this.owner.mind.relationships.context({
658
+ now,
659
+ open: true,
660
+ limit: 8,
661
+ }),
646
662
  ...(this.owner.livingForView(now, { since: start, open: true })
647
663
  ? {
648
664
  livingFor: this.owner.livingForView(now, {
@@ -660,6 +676,10 @@ export class LifeDiary {
660
676
  trace,
661
677
  );
662
678
  const week = text(result?.week, 1500);
679
+ if (this.owner.mind.relationships.version() !== relationshipVersion)
680
+ throw Error(
681
+ "写回顾期间关系记录已变化,本次旧稿未保存,下次按新认识整理",
682
+ );
663
683
  if (!week || hasCredential(week)) throw SyntaxError("回顾格式无效");
664
684
  const valid = new Set([
665
685
  ...diaries.map((d) => `d:${d.day}`),
@@ -1,6 +1,7 @@
1
1
  import { localClock } from "../../core/conversation-cues.js";
2
2
  import { personaNeedsRephrase } from "../traits.js";
3
3
  import { PLACE_THOUGHT_RULES } from "../faces.js";
4
+ import { RELATIONSHIP_KNOWLEDGE_RULE } from "../relationship-context.js";
4
5
  import { HOUR, evidence, hasCredential, parse, text } from "../util.js";
5
6
  const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
6
7
  const IDENTITY_PROMPT =
@@ -131,6 +132,7 @@ export class LifeGrowth {
131
132
  try {
132
133
  const nature = this.owner.mind.nature.current(now);
133
134
  const version = nature.version;
135
+ const relationshipVersion = this.owner.mind.relationships.version();
134
136
  const input = {
135
137
  clock: localClock(now, this.owner.mind.timeZone()),
136
138
  nature: {
@@ -144,6 +146,7 @@ export class LifeGrowth {
144
146
  this.owner.mind.traits.persona(nature, now)?.content || "",
145
147
  self: this.owner.selfView(now).slice(0, 10),
146
148
  people: this.owner.peopleIn(experiences, now),
149
+ relationships: this.owner.mind.relationships.context({ now, limit: 8 }),
147
150
  faces: this.owner.faceView(experiences, now),
148
151
  verifiedMeetings: this.owner.identityMeetings(
149
152
  now,
@@ -154,7 +157,7 @@ export class LifeGrowth {
154
157
  let result = await this.owner.chat.models.call(
155
158
  this.owner.profile(),
156
159
  "reflection",
157
- `${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}`,
160
+ `${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}\n${RELATIONSHIP_KNOWLEDGE_RULE}`,
158
161
  input,
159
162
  trace,
160
163
  );
@@ -162,15 +165,19 @@ export class LifeGrowth {
162
165
  result = await this.owner.chat.models.call(
163
166
  this.owner.profile(),
164
167
  "reflection",
165
- `${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}\n上一次给出了不符合格式的聊天回复。这次只能给指定四个字段的 JSON。`,
168
+ `${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}\n${RELATIONSHIP_KNOWLEDGE_RULE}\n上一次给出了不符合格式的聊天回复。这次只能给指定四个字段的 JSON。`,
166
169
  input,
167
170
  trace,
168
171
  );
169
172
  if (typeof result?.skip !== "boolean")
170
173
  throw SyntaxError("身份回看输出格式无效");
171
- if (this.owner.closed || this.owner.mind.nature.version() !== version) {
174
+ if (
175
+ this.owner.closed ||
176
+ this.owner.mind.nature.version() !== version ||
177
+ this.owner.mind.relationships.version() !== relationshipVersion
178
+ ) {
172
179
  status = "cancelled";
173
- reason = "天性或服务状态已经变化";
180
+ reason = "天性、关系或服务状态已经变化";
174
181
  } else if (result?.skip) {
175
182
  const previous = this.owner.mind.traits.persona(nature, now);
176
183
  if (
@@ -542,6 +542,7 @@ export class Life {
542
542
  try {
543
543
  const nature = this.mind.nature.current(now);
544
544
  const version = nature.version;
545
+ const relationshipVersion = this.mind.relationships.version();
545
546
  const experiences = this.experiences(since, now);
546
547
  freshMessages = experiences.reduce((n, e) => n + e.messages.length, 0);
547
548
  const thoughts = this.mind.thoughts.open({ now, limit: 10 });
@@ -574,6 +575,7 @@ export class Life {
574
575
  fading: this.fadingView(now),
575
576
  faces: this.faceView(experiences, now),
576
577
  people: this.peopleIn(experiences, now),
578
+ relationships: this.mind.relationships.context({ now, limit: 8 }),
577
579
  missing: this.missingView(now),
578
580
  fromWish: this.mind.meetings.wishAway({ now }),
579
581
  quiet: this.quietView(now),
@@ -641,13 +643,19 @@ export class Life {
641
643
  .some((m) => m.role === "user"),
642
644
  ),
643
645
  );
644
- if (this.closed || this.mind.nature.version() !== version) {
646
+ if (
647
+ this.closed ||
648
+ this.mind.nature.version() !== version ||
649
+ this.mind.relationships.version() !== relationshipVersion
650
+ ) {
645
651
  if (chunk && !this.closed)
646
652
  this.mind.reading.record(chunk, result?.readingNote || "", id, now);
647
653
  status = "cancelled";
648
654
  reason = this.closed
649
655
  ? "服务停止了,这次想法不作数"
650
- : "天性改了,这次想法不作数";
656
+ : this.mind.relationships.version() !== relationshipVersion
657
+ ? "关系记录变了,按新认识重新整理,不提交旧想法"
658
+ : "天性改了,这次想法不作数";
651
659
  } else if (
652
660
  result?.skip === true &&
653
661
  !result.mood &&
@@ -0,0 +1,71 @@
1
+ // Current relationship facts and the words she once used about them have
2
+ // different jobs. Keep history intact, and make that distinction at recall.
3
+ export const RELATIONSHIP_KNOWLEDGE_RULE =
4
+ "relationships.known 是当前已知的人际关系,属于同一个自己的生活。旧经历、自我线索中的否认或玩笑保留为当时的看法,不据此否定新关系记录。知道对方是谁与对方是什么关系,和自己的感受、信任、称呼选择分开;不机械扮演,不编造共同过去或对方的同意。";
5
+
6
+ export function mentionsRelationship(content, relation) {
7
+ const words = String(content || "").toLocaleLowerCase();
8
+ return [
9
+ relation.name,
10
+ ...(relation.knownAs || []),
11
+ relation.peerRole,
12
+ relation.selfRole,
13
+ ]
14
+ .filter(Boolean)
15
+ .some((word) => {
16
+ const escaped = String(word).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ const boundary = /^[\w .-]+$/.test(word);
18
+ return new RegExp(
19
+ boundary ? `(?<![\\w])${escaped}(?![\\w])` : escaped,
20
+ "i",
21
+ ).test(words);
22
+ });
23
+ }
24
+
25
+ export function relationshipHistory(content, created, knowledge) {
26
+ return (knowledge?.known || []).some(
27
+ (r) =>
28
+ Number.isFinite(created) &&
29
+ created < r.created &&
30
+ mentionsRelationship(content, r),
31
+ )
32
+ ? `${content}(关系记录更新前的看法;保留当时的感受,不据此否定当前关系事实)`
33
+ : content;
34
+ }
35
+
36
+ // Narrow, literal factual denials only. Disagreement with a label, old words,
37
+ // fictional dialogue and feelings are left to the contextual reviewer.
38
+ export function relationshipClaimIssues(bubbles, knowledge) {
39
+ const issues = [];
40
+ for (const r of knowledge?.known || []) {
41
+ const role = String(r.peerRole).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
+ const names = [r.name, ...(r.knownAs || [])].filter(Boolean);
43
+ for (const line of bubbles || []) {
44
+ for (const clause of String(line).split(/[,,。!?!?;;\n]/)) {
45
+ if (
46
+ /以前|之前|当时|过去|曾经|从前|一开始|原来|小说|台词|[“「"『]/.test(
47
+ clause,
48
+ )
49
+ )
50
+ continue;
51
+ const namedDenial = names.some((name) => {
52
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
53
+ return new RegExp(
54
+ `(?:^|[\\s::])${escaped}(?:也|本来|根本)?(?:不是|并非)我(?:的)?${role}(?:$|[\\s,。])|我(?:并)?不认识\\s*${escaped}(?:$|[\\s,。])`,
55
+ "i",
56
+ ).test(clause);
57
+ });
58
+ const absentRole = new RegExp(
59
+ `我(?:本来|本来也|现在|根本|一直|从来)?(?:就|也)?(?:没有|没)${role}(?:$|[\\s,。])`,
60
+ ).test(clause);
61
+ if (namedDenial || absentRole) {
62
+ issues.push(
63
+ `当前已知 ${r.name || r.subjectId} 与自己的关系是 ${r.peerRole},不能把旧印象当成关系不存在;可保留自己的感受和称呼选择,按当前记录核对再说`,
64
+ );
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return [...new Set(issues)];
71
+ }
@@ -3,6 +3,7 @@ import { parseSessionKey } from "../channels/session-key.js";
3
3
  import { hasCredential, text } from "./util.js";
4
4
  import { normalized } from "./util.js";
5
5
  import { leaks } from "./guard.js";
6
+ import { mentionsRelationship } from "./relationship-context.js";
6
7
 
7
8
  export const RELATIONSHIP_ROLES = [
8
9
  "妹妹",
@@ -133,6 +134,116 @@ export class Relationships {
133
134
  return r.status === "active";
134
135
  });
135
136
  }
137
+ // Knowing someone does not require them to be speaking in this room.
138
+ // Select from the same current ledger used by the management UI, while
139
+ // keeping account identity and disclosure boundaries at the read boundary.
140
+ context({
141
+ room = "",
142
+ people = [],
143
+ cue = [],
144
+ now = Date.now(),
145
+ open = false,
146
+ limit = 6,
147
+ } = {}) {
148
+ const place = scope(room);
149
+ const present = new Set(people.map(String));
150
+ const words = (cue || [])
151
+ .filter((c) => typeof c !== "object" || c?.role !== "assistant")
152
+ .map((c) => (typeof c === "object" ? c?.text || "" : String(c)));
153
+ const rows = this.list(now)
154
+ .filter(
155
+ (r) => this.visible(r, room) && (!open || r.discretion === "open"),
156
+ )
157
+ .filter(
158
+ (r) =>
159
+ !place ||
160
+ (r.channel === place.channel &&
161
+ (r.account_id === "_" || r.account_id === place.accountId)),
162
+ )
163
+ .map((r) => {
164
+ const view = this.view(r, room);
165
+ const accountName = this.mind.bonds.name(r.subject_id);
166
+ view.name ||= accountName;
167
+ if (
168
+ accountName &&
169
+ accountName !== view.name &&
170
+ accountName !== r.subject_id
171
+ )
172
+ view.knownAs = [accountName];
173
+ const mentioned = words.some((word) =>
174
+ mentionsRelationship(word, view),
175
+ );
176
+ const speaker = present.has(r.subject_id);
177
+ return {
178
+ view,
179
+ mentioned,
180
+ score: Number(mentioned) * 2 + Number(speaker),
181
+ };
182
+ })
183
+ .sort((a, b) => b.score - a.score || b.view.created - a.view.created);
184
+ const selected = rows.slice(0, limit);
185
+ // Bond changes are intentionally capped; that counter is not the number
186
+ // of conversations. Count actual addressed replies without scanning the
187
+ // much larger model traces or assuming adjacent messages were exchanges.
188
+ const replies = selected.length
189
+ ? this.db
190
+ .prepare(
191
+ `
192
+ SELECT json_extract(t.payload,'$.userId') subject,
193
+ COALESCE(json_extract(t.payload,'$.channel'),'onebot') channel,
194
+ t.account_id account,COUNT(DISTINCT a.seq) replies,MAX(a.time) last
195
+ FROM core_events a JOIN json_each(a.payload,'$.replyTargetIds') refs
196
+ JOIN core_events t ON t.seq=refs.value
197
+ WHERE a.role='assistant' AND t.role='user' AND a.time<=? AND t.time<=?
198
+ AND COALESCE(json_extract(a.payload,'$.simulated'),0)=0
199
+ AND COALESCE(json_extract(t.payload,'$.simulated'),0)=0
200
+ AND t.seq NOT IN (SELECT seq FROM mind_unlived)
201
+ AND json_extract(t.payload,'$.userId') IN (${selected.map(() => "?").join(",")})
202
+ GROUP BY subject,channel,account
203
+ `,
204
+ )
205
+ .all(now, now, ...selected.map(({ view }) => view.subjectId))
206
+ : [];
207
+ return {
208
+ known: selected.map(({ view }) => {
209
+ const person = this.mind.bonds.person(view.subjectId, now, {
210
+ room: open ? "group:__open__" : room,
211
+ });
212
+ const history = replies.filter(
213
+ (r) =>
214
+ String(r.subject) === view.subjectId &&
215
+ r.channel === view.channel &&
216
+ (view.accountId === "_" || r.account === view.accountId),
217
+ );
218
+ return {
219
+ ...view,
220
+ ...(person
221
+ ? {
222
+ withMe: {
223
+ feel: person.feel,
224
+ spokenReplies: history.reduce((n, r) => n + r.replies, 0),
225
+ firstMetAt: person.firstMetAt,
226
+ lastTalkedAt:
227
+ Math.max(
228
+ person.lastTalkedAt || 0,
229
+ ...history.map((r) => r.last),
230
+ ) || null,
231
+ ...(person.impression
232
+ ? { impression: text(person.impression, 120) }
233
+ : {}),
234
+ },
235
+ }
236
+ : {}),
237
+ };
238
+ }),
239
+ requested:
240
+ rows.some((r) => r.mentioned) ||
241
+ (rows.some((r) => r.score) &&
242
+ words.some((w) =>
243
+ /(?:我|你).*(?:是谁|谁|关系|认识|认得)|who|relationship/i.test(w),
244
+ )),
245
+ };
246
+ }
136
247
  history(subject, limit = 30) {
137
248
  return this.db
138
249
  .prepare(
@@ -5,6 +5,7 @@ import { agoLabel, elapsedLabel } from "./clock.js";
5
5
  import { describeFace } from "./faces.js";
6
6
  import { SELF_KINDS } from "./self.js";
7
7
  import { DAY, text } from "./util.js";
8
+ import { relationshipHistory } from "./relationship-context.js";
8
9
 
9
10
  const FORGOTTEN = "(很久没想起了)";
10
11
 
@@ -89,6 +90,12 @@ export function innerView(
89
90
  const face = mind.faces.current(session, now);
90
91
  const living = mind.self.living({ before: now, now, room: session });
91
92
  const cues = speakerCues(cue);
93
+ const relationships = mind.relationships.context({
94
+ room: session,
95
+ people,
96
+ cue,
97
+ now,
98
+ });
92
99
  const carries = (thread) => mind.meetings.stays(thread, session, now);
93
100
  const visibleThreads = relevantContext(
94
101
  mind.self
@@ -150,7 +157,7 @@ export function innerView(
150
157
  ? {
151
158
  threads: visibleThreads.map(
152
159
  (t) =>
153
- `${SELF_KINDS[t.kind]}:${text(t.content, 80)}${t.status === "emerging" ? "(刚开始有这种感觉)" : ""}`,
160
+ `${SELF_KINDS[t.kind]}:${relationshipHistory(text(t.content, 80), t.created, relationships)}${t.status === "emerging" ? "(刚开始有这种感觉)" : ""}`,
154
161
  ),
155
162
  }
156
163
  : {}),
@@ -254,6 +261,7 @@ export function innerView(
254
261
  currentLife: mind.time.view({ session, now, cue }),
255
262
  state: `${affect.phaseLabel},精力${affect.energyLabel},心情${affect.mood}${cause ? `(${cause})` : ""}${affect.lately ? `,${affect.lately}` : ""}`,
256
263
  ...(persons.length ? { people: persons } : {}),
264
+ ...(relationships.known.length ? { relationships } : {}),
257
265
  ...(continuity ? { continuity } : {}),
258
266
  ...(group ? { thisGroup: group.feel } : {}),
259
267
  ...(thoughts.length