dsh-agentic-pet 0.3.1 → 0.4.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.
@@ -1,9 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { randomUUID } from "node:crypto";
3
- import { existsSync } from "node:fs";
4
- import { appendFile, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { appendFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
4
  import { homedir } from "node:os";
6
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
7
6
  import { Context, Service } from "@deepseek-ai/cordis";
8
7
  //#region ../../../vendor/cosmokit/src/misc.ts
9
8
  /** Return true when a value is `null` or `undefined`. */
@@ -4618,6 +4617,19 @@ function formatAvailableActions(available, cap = 24) {
4618
4617
  * @module dsh-agentic-pet/inner-voice
4619
4618
  */
4620
4619
  const VOICE_SESSION_PREFIX = "whale598-";
4620
+ /**
4621
+ * Saved-setting migration for preset id renames: pet-settings.json holds the
4622
+ * user's 会话模式 choice by id, and a rename orphans that saved value (every
4623
+ * ensure would first try a dead id and fall back). Map the legacy ids onto
4624
+ * their successors; unknown ids pass through untouched.
4625
+ */
4626
+ const LEGACY_PRESET_IDS = {
4627
+ "pet-inner-voice": "whale598",
4628
+ "pet": "whale598-standard"
4629
+ };
4630
+ function migratePreset(preset) {
4631
+ return LEGACY_PRESET_IDS[preset] ?? preset;
4632
+ }
4621
4633
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
4622
4634
  const finite01 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? clamp(value, 0, 1) : fallback;
4623
4635
  /** Read `innerVoice` from pet-settings.json; anything missing or mistyped
@@ -4634,7 +4646,7 @@ async function readInnerVoiceConfig(home) {
4634
4646
  }
4635
4647
  return {
4636
4648
  enabled: v.enabled !== false,
4637
- preset: typeof v.preset === "string" && v.preset !== "" ? v.preset : "pet-inner-voice",
4649
+ preset: migratePreset(typeof v.preset === "string" && v.preset !== "" ? v.preset : "whale598"),
4638
4650
  wakeIntervalMinutes: typeof v.wakeIntervalMinutes === "number" && Number.isFinite(v.wakeIntervalMinutes) ? clamp(Math.round(v.wakeIntervalMinutes), 1, 60) : 2,
4639
4651
  speakProbability: finite01(v.speakProbability, .5),
4640
4652
  touchReplyProbability: finite01(v.touchReplyProbability, .3),
@@ -4644,7 +4656,7 @@ async function readInnerVoiceConfig(home) {
4644
4656
  } catch {
4645
4657
  return {
4646
4658
  enabled: true,
4647
- preset: "pet-inner-voice",
4659
+ preset: "whale598",
4648
4660
  wakeIntervalMinutes: 2,
4649
4661
  speakProbability: .5,
4650
4662
  touchReplyProbability: .3,
@@ -4731,10 +4743,44 @@ function mintVoiceSessionId(petId, generation, isArchived) {
4731
4743
  function buildGreetPrompt(facts) {
4732
4744
  return [
4733
4745
  "[启动问好]",
4734
- `用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)})。`,
4746
+ `用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)};羁绊 ${facts.bondTier},一起过了 ${String(facts.bondDays)} 天)。`,
4735
4747
  "主动打个招呼(pet_say)——按当下的时辰自然问好,一两句就好,别问问题等回答。"
4736
4748
  ].join(" ");
4737
4749
  }
4750
+ /** B 层 · 羁绊档位:days-with-interaction → tier name. Only grows, never
4751
+ * decays; the names are the display contract (settings page keeps its own
4752
+ * copy of the thresholds). */
4753
+ const BOND_TIERS = [
4754
+ {
4755
+ since: 0,
4756
+ name: "陌生"
4757
+ },
4758
+ {
4759
+ since: 5,
4760
+ name: "认识"
4761
+ },
4762
+ {
4763
+ since: 15,
4764
+ name: "熟了"
4765
+ },
4766
+ {
4767
+ since: 40,
4768
+ name: "亲近"
4769
+ },
4770
+ {
4771
+ since: 90,
4772
+ name: "离不开"
4773
+ },
4774
+ {
4775
+ since: 180,
4776
+ name: "只有你"
4777
+ }
4778
+ ];
4779
+ function bondTier(days) {
4780
+ let tier = BOND_TIERS[0]?.name ?? "陌生";
4781
+ for (const t of BOND_TIERS) if (days >= t.since) tier = t.name;
4782
+ return tier;
4783
+ }
4738
4784
  /** Shared mood vocabulary (pet_state computes the same from the ledger). */
4739
4785
  function moodOf(affection) {
4740
4786
  return affection >= 80 ? "happy" : affection >= 40 ? "content" : "neutral";
@@ -4752,7 +4798,7 @@ function buildWakePrompt(facts) {
4752
4798
  const user = facts.userActive ? `用户最近正在会话中工作(${String(facts.minutesSinceUserActivity ?? 0)} 分钟内有动静)` : "用户最近没有活动";
4753
4799
  return [
4754
4800
  "[自主唤醒]",
4755
- `好感 ${String(facts.affection)}(${moodOf(facts.affection)});${lastSpoke};${user}。`,
4801
+ `好感 ${String(facts.affection)}(${moodOf(facts.affection)});羁绊 ${facts.bondTier}(一起过了 ${String(facts.bondDays)} 天);${lastSpoke};${user}。`,
4756
4802
  "醒来想到什么就说一句(pet_say),想配动作就 pet_do;也可以只是安静待着(两者都不调,正文写「…」)。"
4757
4803
  ].join(" ");
4758
4804
  }
@@ -4771,19 +4817,132 @@ function buildChatPrompt(text) {
4771
4817
  return `[用户对你说] ${text.slice(0, 2e3)}`;
4772
4818
  }
4773
4819
  //#endregion
4820
+ //#region lib/types/petdata.js
4821
+ /**
4822
+ * The per-pet data store: everything the DRIVER persists about a pet lives
4823
+ * under `~/.dsh/whale598/<petId>/` — affection ledger, display name, event
4824
+ * history, tool-call history. One directory per pet, small files, single
4825
+ * writer (the driver), JSONL for append-only streams with bounded retention.
4826
+ *
4827
+ * Legacy layouts (`pet-affection.d/<id>.json`, the single legacy electron
4828
+ * book, names inside pet-settings.json) are READ as fallbacks and migrate to
4829
+ * the new layout on the next write — lazy migration, no data-loss window.
4830
+ * @module dsh-agentic-pet/petdata
4831
+ */
4832
+ /** The consolidated per-pet data root under the dsh home. */
4833
+ const WHALE_DIR = "whale598";
4834
+ function petDataDir(home, petId) {
4835
+ return join(home, WHALE_DIR, petId);
4836
+ }
4837
+ /** New-layout path first; legacy layouts as read fallbacks (per-pet legacy
4838
+ * dir → the single legacy electron book). The ledger SHAPE is the driver's
4839
+ * (TouchLedger); this module only owns paths and raw IO. */
4840
+ function affectionPaths(home, petId) {
4841
+ return {
4842
+ current: join(petDataDir(home, petId), "pet-affection.json"),
4843
+ legacy: [join(home, "pet-affection.d", `${petId}.json`), ...petId === "electron" ? [join(home, "pet-affection.json")] : []]
4844
+ };
4845
+ }
4846
+ /** Read the affection ledger raw: the current file wins; legacy files answer
4847
+ * only while the current one is absent (first write migrates the full
4848
+ * ledger). Returns null when no layout holds data. */
4849
+ async function readAffectionRaw(home, petId) {
4850
+ const { current, legacy } = affectionPaths(home, petId);
4851
+ for (const file of [current, ...legacy]) try {
4852
+ const parsed = JSON.parse(await readFile(file, "utf8"));
4853
+ if (parsed !== null && typeof parsed === "object" && Object.keys(parsed).length > 0) return parsed;
4854
+ } catch {}
4855
+ return null;
4856
+ }
4857
+ async function writeAffectionRaw(home, petId, ledger) {
4858
+ const dir = petDataDir(home, petId);
4859
+ await mkdir(dir, { recursive: true });
4860
+ await writeFile(join(dir, "pet-affection.json"), `${JSON.stringify(ledger)}\n`, "utf8");
4861
+ }
4862
+ function petNameFile(home, petId) {
4863
+ return join(petDataDir(home, petId), "pet-name.json");
4864
+ }
4865
+ /** The user's custom display name: new-layout file → legacy
4866
+ * pet-settings.json `names` map → undefined (caller applies the built-in
4867
+ * default). */
4868
+ async function readPetName(home, petId) {
4869
+ try {
4870
+ const parsed = JSON.parse(await readFile(petNameFile(home, petId), "utf8"));
4871
+ if (typeof parsed.name === "string" && parsed.name.trim() !== "") return parsed.name.trim();
4872
+ } catch {}
4873
+ try {
4874
+ const settings = JSON.parse(await readFile(join(home, "pet-settings.json"), "utf8"));
4875
+ const label = (typeof settings.names === "object" && settings.names !== null ? settings.names : {})[petId];
4876
+ if (typeof label === "string" && label.trim() !== "") return label.trim();
4877
+ } catch {}
4878
+ }
4879
+ /** Persist a custom name; `null` reverts to the built-in default (drops the
4880
+ * override file). Also strips the petId key from the legacy pet-settings
4881
+ * names map so the old layout cannot resurrect a cleared name. */
4882
+ async function writePetName(home, petId, name) {
4883
+ await mkdir(petDataDir(home, petId), { recursive: true });
4884
+ const file = petNameFile(home, petId);
4885
+ if (name === null || name.trim() === "") await rm(file, { force: true });
4886
+ else await writeFile(file, `${JSON.stringify({ name: name.trim() })}\n`, "utf8");
4887
+ try {
4888
+ const settingsPath = join(home, "pet-settings.json");
4889
+ const settings = JSON.parse(await readFile(settingsPath, "utf8"));
4890
+ if (typeof settings.names === "object" && settings.names !== null && petId in settings.names) {
4891
+ delete settings.names[petId];
4892
+ if (Object.keys(settings.names).length === 0) delete settings.names;
4893
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
4894
+ }
4895
+ } catch {}
4896
+ }
4897
+ const JSONL_MAX_LINES = 600;
4898
+ const JSONL_KEEP_LINES = 500;
4899
+ function eventsFile(home, petId) {
4900
+ return join(petDataDir(home, petId), "events.jsonl");
4901
+ }
4902
+ function toolCallsFile(home, petId) {
4903
+ return join(petDataDir(home, petId), "toolcalls.jsonl");
4904
+ }
4905
+ /** Append one record; bounded retention trims the file when it outgrows the
4906
+ * cap (best-effort — a failed trim never loses the append). */
4907
+ async function appendJsonl(file, entry) {
4908
+ await mkdir(dirname(file), { recursive: true });
4909
+ await appendFile(file, `${JSON.stringify(entry)}\n`, "utf8");
4910
+ try {
4911
+ const lines = (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "");
4912
+ if (lines.length > JSONL_MAX_LINES) await writeFile(file, `${lines.slice(lines.length - JSONL_KEEP_LINES).join("\n")}\n`, "utf8");
4913
+ } catch {}
4914
+ }
4915
+ /** The last `limit` records (oldest last); malformed lines degrade to
4916
+ * `{raw}` instead of dropping the file. */
4917
+ async function readJsonl(file, limit) {
4918
+ try {
4919
+ return (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "").slice(-limit).map((line) => {
4920
+ try {
4921
+ return JSON.parse(line);
4922
+ } catch {
4923
+ return { raw: line };
4924
+ }
4925
+ });
4926
+ } catch {
4927
+ return [];
4928
+ }
4929
+ }
4930
+ //#endregion
4774
4931
  //#region lib/types/registry.js
4775
4932
  /**
4776
4933
  * The pet registry: a directory of per-pet control manifests, replacing the
4777
4934
  * last-writer-wins single files whose races caused repeated swap incidents
4778
4935
  * (a dying carrier's late write burying a live pet's registration). Each pet
4779
- * implementation owns its own file under pet-http.d/ and re-asserts it on a
4936
+ * implementation owns its own file under whale598/pet-http.d/ (legacy
4937
+ * pet-http.d/ still scanned) and re-asserts it on a
4780
4938
  * cadence; entries whose publisher pid is dead expire by omission. The
4781
4939
  * driver reads the whole table and routes per the multi-pet design: the
4782
4940
  * session's bound pet if alive, else the default pet (first alive entry),
4783
4941
  * plus an all-notify broadcast mode for interactions.
4784
4942
  * @module dsh-agentic-pet/registry
4785
4943
  */
4786
- const REGISTRY_DIR = "pet-http.d";
4944
+ const REGISTRY_DIR = `${WHALE_DIR}/pet-http.d`;
4945
+ const LEGACY_REGISTRY_DIR = "pet-http.d";
4787
4946
  /**
4788
4947
  * A live pet is one whose control plane ANSWERS, not merely one whose pid
4789
4948
  * exists — and not merely one whose port accepts TCP: a frozen pet (stuck
@@ -4818,27 +4977,37 @@ async function probeControlPlane(port) {
4818
4977
  * they expire by omission the next time their file is rewritten — and so are
4819
4978
  * silent entries (pid alive, control plane unresponsive). */
4820
4979
  async function readLivePets(home) {
4980
+ const candidates = /* @__PURE__ */ new Map();
4981
+ for (const dir of [join(home, REGISTRY_DIR), join(home, LEGACY_REGISTRY_DIR)]) {
4982
+ let names = [];
4983
+ try {
4984
+ names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
4985
+ } catch {
4986
+ continue;
4987
+ }
4988
+ for (const name of names) {
4989
+ const id = name.slice(0, -5);
4990
+ if (id !== "" && !candidates.has(id)) candidates.set(id, join(dir, name));
4991
+ }
4992
+ }
4821
4993
  const live = [];
4822
- try {
4823
- const files = (await readdir(join(home, REGISTRY_DIR))).filter((name) => name.endsWith(".json"));
4824
- for (const file of files) try {
4825
- const raw = JSON.parse(await readFile(join(home, REGISTRY_DIR, file), "utf8"));
4826
- const port = typeof raw.port === "number" ? raw.port : NaN;
4827
- const pid = typeof raw.pid === "number" ? raw.pid : NaN;
4828
- if (!Number.isFinite(port) || !Number.isFinite(pid)) continue;
4829
- try {
4830
- process.kill(pid, 0);
4831
- } catch {
4832
- continue;
4833
- }
4834
- if (!await probeControlPlane(port)) continue;
4835
- live.push({
4836
- id: file.slice(0, -5),
4837
- port,
4838
- pid,
4839
- since: typeof raw.since === "number" ? raw.since : 0
4840
- });
4841
- } catch {}
4994
+ for (const [id, file] of candidates) try {
4995
+ const raw = JSON.parse(await readFile(file, "utf8"));
4996
+ const port = typeof raw.port === "number" ? raw.port : NaN;
4997
+ const pid = typeof raw.pid === "number" ? raw.pid : NaN;
4998
+ if (!Number.isFinite(port) || !Number.isFinite(pid)) continue;
4999
+ try {
5000
+ process.kill(pid, 0);
5001
+ } catch {
5002
+ continue;
5003
+ }
5004
+ if (!await probeControlPlane(port)) continue;
5005
+ live.push({
5006
+ id,
5007
+ port,
5008
+ pid,
5009
+ since: typeof raw.since === "number" ? raw.since : 0
5010
+ });
4842
5011
  } catch {}
4843
5012
  return live;
4844
5013
  }
@@ -4920,15 +5089,35 @@ const EMPTY_LEDGER = {
4920
5089
  lastTouchAt: void 0,
4921
5090
  recent: []
4922
5091
  };
4923
- async function readLedger(ledgerFile) {
5092
+ const ADJUST_MAX_DELTA = 3;
5093
+ const ADJUST_COOLDOWN_MS = 5 * 6e4;
5094
+ const ADJUST_DAILY_LIMIT = 8;
5095
+ const ADJUST_DAILY_SUM = 10;
5096
+ const DECAY_START_DAYS = 3;
5097
+ const DECAY_STEP_DAYS = 3;
5098
+ async function readLedger(home, petId) {
4924
5099
  try {
4925
- const raw = JSON.parse(await readFile(ledgerFile, "utf8"));
5100
+ const raw = await readAffectionRaw(home, petId);
5101
+ if (raw === null) return {
5102
+ ...EMPTY_LEDGER,
5103
+ byPart: {},
5104
+ recent: []
5105
+ };
4926
5106
  return {
4927
5107
  affection: typeof raw.affection === "number" ? raw.affection : 0,
4928
5108
  total: typeof raw.total === "number" ? raw.total : 0,
4929
5109
  byPart: raw.byPart ?? {},
4930
5110
  lastTouchAt: raw.lastTouchAt,
4931
- recent: raw.recent ?? []
5111
+ recent: raw.recent ?? [],
5112
+ adjustments: Array.isArray(raw.adjustments) ? raw.adjustments.filter((a) => typeof a === "object" && a !== null && typeof a.delta === "number" && typeof a.reason === "string" && typeof a.at === "number") : void 0,
5113
+ quota: typeof raw.quota === "object" && raw.quota !== null ? raw.quota : void 0,
5114
+ lastDecayAt: typeof raw.lastDecayAt === "number" ? raw.lastDecayAt : void 0,
5115
+ bond: typeof raw.bond === "object" && raw.bond !== null && typeof raw.bond.lastCountedDate === "string" ? {
5116
+ value: typeof raw.bond.value === "number" ? raw.bond.value : 0,
5117
+ days: typeof raw.bond.days === "number" ? raw.bond.days : 0,
5118
+ lastCountedDate: raw.bond.lastCountedDate
5119
+ } : void 0,
5120
+ bondAdjustments: Array.isArray(raw.bondAdjustments) ? raw.bondAdjustments.filter((a) => typeof a === "object" && a !== null && typeof a.delta === "number" && typeof a.reason === "string" && typeof a.at === "number") : void 0
4932
5121
  };
4933
5122
  } catch {
4934
5123
  return {
@@ -4938,12 +5127,6 @@ async function readLedger(ledgerFile) {
4938
5127
  };
4939
5128
  }
4940
5129
  }
4941
- /** The per-pet ledger file. Legacy deployments' single book is folded under
4942
- * the electron id so pre-registry affection history survives the upgrade. */
4943
- function ledgerFileFor(home, petId) {
4944
- if (petId === "electron" && !existsSync(join(home, "pet-affection.d"))) return join(home, "pet-affection.json");
4945
- return join(home, "pet-affection.d", `${petId}.json`);
4946
- }
4947
5130
  /** Local-time log stamp: the driver log is the user's primary debugging
4948
5131
  * tool, so timestamps show the USER's wall clock, not UTC (toISOString's Z
4949
5132
  * form read 8h off and cost a confusion round). Sortable, zero-padded. */
@@ -4995,6 +5178,23 @@ const voiceLastSpokeAt = /* @__PURE__ */ new Map();
4995
5178
  const voiceLastTouchReplyAt = /* @__PURE__ */ new Map();
4996
5179
  const voiceNextWakeAt = /* @__PURE__ */ new Map();
4997
5180
  const voiceGreetedOnce = /* @__PURE__ */ new Set();
5181
+ const voiceNames = /* @__PURE__ */ new Map();
5182
+ const DEFAULT_PET_NAME = { electron: "鲸鱼娘" };
5183
+ /** B-layer bond day counting: the FIRST interaction of a calendar day
5184
+ * (touch / user chat / model adjustment) marks that day as shared. Called
5185
+ * at the interaction points, never on reads — reading the ledger does not
5186
+ * make a day meaningful. Mutates in place; the caller persists. Module-level
5187
+ * because both the driver closures AND the model tools (registerPetTools)
5188
+ * count bond days. */
5189
+ const countBondDay = (ledger) => {
5190
+ const today = (/* @__PURE__ */ new Date()).toDateString();
5191
+ if (ledger.bond?.lastCountedDate === today) return;
5192
+ ledger.bond = {
5193
+ value: ledger.bond?.value ?? 0,
5194
+ days: (ledger.bond?.days ?? 0) + 1,
5195
+ lastCountedDate: today
5196
+ };
5197
+ };
4998
5198
  const voiceLastPrompt = /* @__PURE__ */ new Map();
4999
5199
  const voiceBoundSessions = /* @__PURE__ */ new Map();
5000
5200
  const voiceWakeTimer = { timer: void 0 };
@@ -5024,6 +5224,7 @@ function installPetDriver(ctx, home, pet) {
5024
5224
  voiceLastTouchReplyAt.clear();
5025
5225
  voiceNextWakeAt.clear();
5026
5226
  voiceGreetedOnce.clear();
5227
+ voiceNames.clear();
5027
5228
  voiceLastPrompt.clear();
5028
5229
  voiceBoundSessions.clear();
5029
5230
  if (voiceWakeTimer.timer !== void 0) clearTimeout(voiceWakeTimer.timer);
@@ -5056,7 +5257,7 @@ function installPetDriver(ctx, home, pet) {
5056
5257
  if (!latestArchived.has(entry.sessionId)) continue;
5057
5258
  const generation = (voiceStore.generations[petId] ?? 0) + 1;
5058
5259
  voiceStore.generations[petId] = generation;
5059
- delete voiceStore.sessions[petId];
5260
+ Reflect.deleteProperty(voiceStore.sessions, petId);
5060
5261
  const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
5061
5262
  if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
5062
5263
  voicePendingUntil.delete(petId);
@@ -5082,13 +5283,15 @@ function installPetDriver(ctx, home, pet) {
5082
5283
  }
5083
5284
  };
5084
5285
  const attachedSessions = /* @__PURE__ */ new Set();
5286
+ const petWorkspaceDir = () => join(homedir(), "whale598");
5085
5287
  const attachVoiceSession = async (sessionId) => {
5086
5288
  if (attachedSessions.has(sessionId)) return;
5087
5289
  attachedSessions.add(sessionId);
5088
5290
  try {
5089
5291
  const registry = ctx.get("workspaceRegistry");
5090
5292
  if (registry === void 0) return;
5091
- await (await registry.create(homedir()))?.attachSession?.(sessionId);
5293
+ await mkdir(petWorkspaceDir(), { recursive: true });
5294
+ await (await registry.create(petWorkspaceDir()))?.attachSession?.(sessionId);
5092
5295
  driverLog(home, `VOICE session attached to workspace (${sessionId})`);
5093
5296
  } catch (error) {
5094
5297
  attachedSessions.delete(sessionId);
@@ -5210,7 +5413,7 @@ function installPetDriver(ctx, home, pet) {
5210
5413
  const petId = voiceOwners.get(sessionId);
5211
5414
  voiceOwners.delete(sessionId);
5212
5415
  if (petId !== void 0) {
5213
- delete voiceStore.sessions[petId];
5416
+ Reflect.deleteProperty(voiceStore.sessions, petId);
5214
5417
  voicePendingUntil.delete(petId);
5215
5418
  saveInnerVoiceStore(home, voiceStore);
5216
5419
  driverLog(home, `VOICE session removed by user; pet=${petId} will re-create on next wake`);
@@ -5222,6 +5425,22 @@ function installPetDriver(ctx, home, pet) {
5222
5425
  boundPetId = void 0;
5223
5426
  pushState("idle");
5224
5427
  });
5428
+ const refreshNames = async () => {
5429
+ voiceNames.clear();
5430
+ try {
5431
+ for (const petId of await readdir(join(home, WHALE_DIR))) {
5432
+ const name = await readPetName(home, petId);
5433
+ if (name !== void 0) voiceNames.set(petId, name);
5434
+ }
5435
+ } catch {}
5436
+ try {
5437
+ const raw = JSON.parse(await readFile(join(home, "pet-settings.json"), "utf8"));
5438
+ if (typeof raw.names === "object" && raw.names !== null) {
5439
+ for (const [petId, label] of Object.entries(raw.names)) if (petId !== "" && typeof label === "string" && label.trim() !== "" && !voiceNames.has(petId)) voiceNames.set(petId, label.trim());
5440
+ }
5441
+ } catch {}
5442
+ };
5443
+ refreshNames();
5225
5444
  const refreshBoundSessions = async () => {
5226
5445
  const config = await readInnerVoiceConfig(home);
5227
5446
  voiceBoundSessions.clear();
@@ -5229,7 +5448,31 @@ function installPetDriver(ctx, home, pet) {
5229
5448
  };
5230
5449
  refreshBoundSessions();
5231
5450
  events.on("pet-settings/changed", () => {
5232
- refreshBoundSessions();
5451
+ const prevNames = new Map(voiceNames);
5452
+ (async () => {
5453
+ await refreshNames();
5454
+ refreshBoundSessions();
5455
+ const changedPets = new Set([...prevNames.keys(), ...voiceNames.keys()]);
5456
+ for (const petId of changedPets) {
5457
+ if (prevNames.get(petId) === voiceNames.get(petId)) continue;
5458
+ const nextName = voiceNames.get(petId);
5459
+ const label = nextName ?? DEFAULT_PET_NAME[petId] ?? petId;
5460
+ const entry = voiceStore.sessions[petId];
5461
+ if (entry !== void 0) {
5462
+ const title = `${label}的内心戏`;
5463
+ try {
5464
+ await voiceController?.rename({
5465
+ sessionId: entry.sessionId,
5466
+ title
5467
+ });
5468
+ driverLog(home, `VOICE session retitled pet=${petId} → ${title}`);
5469
+ } catch (error) {
5470
+ driverLog(home, `VOICE retitle skipped pet=${petId}: ${error instanceof Error ? error.message : String(error)}`);
5471
+ }
5472
+ }
5473
+ triggerRenameNotice(petId, nextName, label);
5474
+ }
5475
+ })();
5233
5476
  if (currentState !== void 0) pushState(currentState);
5234
5477
  });
5235
5478
  const startAskPet = (pet, body, signal) => {
@@ -5403,16 +5646,19 @@ function installPetDriver(ctx, home, pet) {
5403
5646
  if (minted.hops > 0) driverLog(home, `VOICE mint skipped ${String(minted.hops)} archived id(s) pet=${petId} → ${minted.sessionId}`);
5404
5647
  const sessionId = minted.sessionId;
5405
5648
  try {
5649
+ await mkdir(petWorkspaceDir(), { recursive: true });
5406
5650
  await voiceController.create({
5407
5651
  sessionId,
5408
- agentPreset: config.preset
5652
+ agentPreset: config.preset,
5653
+ cwd: petWorkspaceDir()
5409
5654
  });
5410
5655
  } catch (firstError) {
5411
5656
  driverLog(home, `VOICE preset "${config.preset}" rejected for pet=${petId}: ${firstError instanceof Error ? firstError.message : String(firstError)}; falling back`);
5412
5657
  try {
5413
5658
  await voiceController.create({
5414
5659
  sessionId,
5415
- agentPreset: "pet-inner-voice"
5660
+ agentPreset: "whale598",
5661
+ cwd: petWorkspaceDir()
5416
5662
  });
5417
5663
  } catch (secondError) {
5418
5664
  driverLog(home, `VOICE session create failed for pet=${petId}: ${secondError instanceof Error ? secondError.message : String(secondError)}`);
@@ -5420,9 +5666,10 @@ function installPetDriver(ctx, home, pet) {
5420
5666
  }
5421
5667
  }
5422
5668
  try {
5669
+ const customName = voiceNames.get(petId);
5423
5670
  await voiceController.rename({
5424
5671
  sessionId,
5425
- title: `鲸鱼娘的内心戏`
5672
+ title: `${customName ?? DEFAULT_PET_NAME[petId] ?? petId}的内心戏`
5426
5673
  });
5427
5674
  } catch {}
5428
5675
  voiceStore.sessions[petId] = {
@@ -5447,6 +5694,9 @@ function installPetDriver(ctx, home, pet) {
5447
5694
  const promptVoice = async (petId, text, opts = {}) => {
5448
5695
  const { retried = false, userDriven = false } = opts;
5449
5696
  if (latestArchived.has(voiceStore.sessions[petId]?.sessionId ?? "")) reconcileArchivedVoiceSessions();
5697
+ const customName = voiceNames.get(petId);
5698
+ if (customName !== void 0 && !text.startsWith("[名字]")) text = `[名字] 你叫「${customName}」。
5699
+ ${text}`;
5450
5700
  const sessionId = await ensureVoiceSession(petId);
5451
5701
  if (sessionId === void 0 || voiceController === void 0) return false;
5452
5702
  try {
@@ -5460,7 +5710,24 @@ function installPetDriver(ctx, home, pet) {
5460
5710
  }]
5461
5711
  }, new AbortController().signal);
5462
5712
  } catch (error) {
5463
- driverLog(home, `VOICE prompt failed pet=${petId}: ${error instanceof Error ? error.message : String(error)}`);
5713
+ const message = error instanceof Error ? error.message : String(error);
5714
+ driverLog(home, `VOICE prompt failed pet=${petId}: ${message}`);
5715
+ if (/Unknown agent preset/i.test(message) && !retried) {
5716
+ if (voiceStore.sessions[petId] !== void 0) {
5717
+ const generation = (voiceStore.generations[petId] ?? 0) + 1;
5718
+ voiceStore.generations[petId] = generation;
5719
+ Reflect.deleteProperty(voiceStore.sessions, petId);
5720
+ const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
5721
+ if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
5722
+ voiceLastPrompt.delete(petId);
5723
+ saveInnerVoiceStore(home, voiceStore);
5724
+ driverLog(home, `VOICE session retired pet=${petId} (recorded preset no longer exists — id renamed) — fresh -g${generation} mounts the current preset`);
5725
+ return promptVoice(petId, text, {
5726
+ retried: true,
5727
+ userDriven
5728
+ });
5729
+ }
5730
+ }
5464
5731
  return false;
5465
5732
  }
5466
5733
  voicePendingUntil.set(petId, Date.now() + 25e3);
@@ -5507,11 +5774,25 @@ function installPetDriver(ctx, home, pet) {
5507
5774
  * greeting (pet offline) releases the flag for the next sweep to retry. */
5508
5775
  const triggerGreeting = async (petId) => {
5509
5776
  if (!await voiceGateOpen(petId, false, "greeting")) return;
5510
- if (await promptVoice(petId, buildGreetPrompt({ affection: (await readLedger(ledgerFileFor(home, petId))).affection }))) {
5777
+ const ledger = await readLedger(home, petId);
5778
+ if (await promptVoice(petId, buildGreetPrompt({
5779
+ affection: ledger.affection,
5780
+ bondValue: ledger.bond?.value ?? 0,
5781
+ bondDays: ledger.bond?.days ?? 0,
5782
+ bondTier: bondTier(ledger.bond?.value ?? 0)
5783
+ }))) {
5511
5784
  voiceGreetedOnce.add(petId);
5512
5785
  driverLog(home, `VOICE boot greeting pet=${petId}`);
5513
5786
  } else voiceGreetedOnce.delete(petId);
5514
5787
  };
5788
+ /** Rename notice (user request): fired when the user's custom name actually
5789
+ * changes — she acknowledges out loud, once. The per-turn short name line
5790
+ * stays as the compaction-proof invariant; a skipped notice (pet offline /
5791
+ * turn in flight) loses nothing because that line still carries the name. */
5792
+ const triggerRenameNotice = async (petId, nextName, label) => {
5793
+ if (!await voiceGateOpen(petId, false, "rename")) return;
5794
+ if (await promptVoice(petId, nextName !== void 0 ? `[改名] 用户给你改名叫「${label}」——以后自我介绍、气泡里自称,都用这个名字。` : `[改名] 用户把你改回了「${label}」。`)) driverLog(home, `VOICE rename notice pet=${petId} → ${label}`);
5795
+ };
5515
5796
  /** Random-wake trigger (docs/inner-voice.md §6.3). The gate decides whether
5516
5797
  * this wake speaks — never the model. */
5517
5798
  const triggerWake = async (petId) => {
@@ -5523,11 +5804,14 @@ function installPetDriver(ctx, home, pet) {
5523
5804
  return;
5524
5805
  }
5525
5806
  if (!await voiceGateOpen(petId, true, "wake")) return;
5526
- const ledger = await readLedger(ledgerFileFor(home, petId));
5807
+ const ledger = await readLedger(home, petId);
5527
5808
  const now = Date.now();
5528
5809
  const lastSpoke = voiceLastSpokeAt.get(petId);
5529
5810
  if (await promptVoice(petId, buildWakePrompt({
5530
5811
  affection: ledger.affection,
5812
+ bondValue: ledger.bond?.value ?? 0,
5813
+ bondDays: ledger.bond?.days ?? 0,
5814
+ bondTier: bondTier(ledger.bond?.value ?? 0),
5531
5815
  minutesSinceLastSpoke: lastSpoke === void 0 ? void 0 : Math.round((now - lastSpoke) / 6e4),
5532
5816
  minutesSinceUserActivity: voiceUserActivityAt === void 0 ? void 0 : Math.round((now - voiceUserActivityAt) / 6e4),
5533
5817
  userActive: voiceUserActivityAt !== void 0 && now - voiceUserActivityAt < 30 * 6e4
@@ -5555,7 +5839,7 @@ function installPetDriver(ctx, home, pet) {
5555
5839
  return;
5556
5840
  }
5557
5841
  if (!await voiceGateOpen(petId, false, "touch", true)) return;
5558
- const ledger = await readLedger(ledgerFileFor(home, petId));
5842
+ const ledger = await readLedger(home, petId);
5559
5843
  if (await promptVoice(petId, buildTouchPrompt({
5560
5844
  part,
5561
5845
  touchesToday: ledger.recent.filter((entry) => entry.part === part).length + 1,
@@ -5613,12 +5897,10 @@ function installPetDriver(ctx, home, pet) {
5613
5897
  wakeSweep();
5614
5898
  const lastEventSeqs = /* @__PURE__ */ new Map();
5615
5899
  const recordTouch = async (petId, part, at) => {
5616
- const ledgerFile = ledgerFileFor(home, petId);
5617
- const ledger = await readLedger(ledgerFile);
5900
+ const ledger = await readLedger(home, petId);
5901
+ countBondDay(ledger);
5618
5902
  ledger.total += 1;
5619
5903
  ledger.byPart[part] = (ledger.byPart[part] ?? 0) + 1;
5620
- const before = ledger.affection;
5621
- ledger.affection = Math.min(100, ledger.affection + (part === "head" ? 2 : 1));
5622
5904
  ledger.lastTouchAt = at;
5623
5905
  ledger.recent.unshift({
5624
5906
  part,
@@ -5626,12 +5908,11 @@ function installPetDriver(ctx, home, pet) {
5626
5908
  });
5627
5909
  ledger.recent = ledger.recent.slice(0, 10);
5628
5910
  try {
5629
- await writeFile(ledgerFile, `${JSON.stringify(ledger)}
5630
- `, "utf8");
5911
+ await writeAffectionRaw(home, petId, ledger);
5631
5912
  } catch (error) {
5632
5913
  console.error(`pet-surface driver: could not write the ledger: ${error instanceof Error ? error.message : String(error)}`);
5633
5914
  }
5634
- if (Math.floor(ledger.affection / 10) > Math.floor(before / 10)) {
5915
+ if (Math.floor(ledger.total / 10) > Math.floor((ledger.total - 1) / 10)) {
5635
5916
  const target = (await pet.list()).find((pet) => pet.id === petId);
5636
5917
  if (target !== void 0) fetch(`http://127.0.0.1:${String(target.port)}/action`, {
5637
5918
  method: "POST",
@@ -5641,17 +5922,23 @@ function installPetDriver(ctx, home, pet) {
5641
5922
  }
5642
5923
  triggerTouchReply(petId, part);
5643
5924
  };
5644
- /** Event dispatch for ONE pet event. touch → the affection ledger;
5645
- * chat → STRAIGHT to the pet's inner-voice session, bypassing the touch
5646
- * path's probability/cooldown/user-busy gates entirely (a user reaching
5647
- * out to talk is never throttled — §6 routing constraint); unknown types
5648
- * are ignored while the cursor still advances. */
5649
5925
  const handlePetEvent = async (petId, event) => {
5926
+ appendJsonl(eventsFile(home, petId), {
5927
+ seq: event.seq,
5928
+ type: event.type,
5929
+ ...event.type === "touch" ? { part: event.part } : {},
5930
+ at: event.at
5931
+ });
5650
5932
  if (event.type === "touch") {
5651
5933
  if (event.part !== void 0) await recordTouch(petId, event.part, event.at);
5652
5934
  return;
5653
5935
  }
5654
- if (event.type === "chat" && event.text !== void 0 && event.text !== "") triggerChat(petId, event.text);
5936
+ if (event.type === "chat" && event.text !== void 0 && event.text !== "") {
5937
+ const ledger = await readLedger(home, petId);
5938
+ countBondDay(ledger);
5939
+ await writeAffectionRaw(home, petId, ledger);
5940
+ triggerChat(petId, event.text);
5941
+ }
5655
5942
  };
5656
5943
  /** Per-pet long-poll loops (§6 v4): each live pet parks ONE /events
5657
5944
  * hold=30 request; the answer — an event batch or the 30s empty one —
@@ -5774,10 +6061,14 @@ function registerPetTools(ctx, home, pet) {
5774
6061
  }]
5775
6062
  },
5776
6063
  async execute(args, exec) {
6064
+ const callStartedAt = Date.now();
6065
+ let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
6066
+ let callDetail;
5777
6067
  const text = String(args.text).slice(0, 200);
5778
6068
  console.log(`pet-surface driver: pet_say ${JSON.stringify(text).slice(0, 60)}`);
5779
6069
  try {
5780
6070
  const entry = await resolveTarget(args.pet, exec?.agent?.id);
6071
+ callPet = entry.id;
5781
6072
  await pet.call(entry, "/say", {
5782
6073
  method: "POST",
5783
6074
  headers: { "content-type": "application/json" },
@@ -5785,7 +6076,17 @@ function registerPetTools(ctx, home, pet) {
5785
6076
  });
5786
6077
  if (markVoiceSpoke(entry.id)) driverLog(home, `VOICE pending released by say pet=${entry.id} (inner-voice turn complete)`);
5787
6078
  } catch (error) {
6079
+ callDetail = error instanceof Error ? error.message : String(error);
5788
6080
  throw classify(error);
6081
+ } finally {
6082
+ appendJsonl(toolCallsFile(home, callPet), {
6083
+ tool: "pet_say",
6084
+ petId: callPet,
6085
+ ok: callDetail === void 0,
6086
+ ...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
6087
+ durationMs: Date.now() - callStartedAt,
6088
+ at: Date.now()
6089
+ });
5789
6090
  }
5790
6091
  return JSON.stringify({
5791
6092
  shown: true,
@@ -5819,8 +6120,12 @@ function registerPetTools(ctx, home, pet) {
5819
6120
  }]
5820
6121
  },
5821
6122
  async execute(args, exec) {
6123
+ const callStartedAt = Date.now();
6124
+ let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
6125
+ let callDetail;
5822
6126
  try {
5823
6127
  const entry = await resolveTarget(args.pet, exec?.agent?.id);
6128
+ callPet = entry.id;
5824
6129
  const requested = String(args.action);
5825
6130
  const resolution = resolveActionRequest(requested, (await pet.capabilities(entry))?.actions ?? null);
5826
6131
  if (resolution.kind === "unknown") throw new Error(`unknown action "${requested}" for pet "${entry.id}". Available: ${formatAvailableActions(resolution.available)}`);
@@ -5863,7 +6168,17 @@ function registerPetTools(ctx, home, pet) {
5863
6168
  waitTimedOut: true
5864
6169
  });
5865
6170
  } catch (error) {
6171
+ callDetail = error instanceof Error ? error.message : String(error);
5866
6172
  throw classify(error);
6173
+ } finally {
6174
+ appendJsonl(toolCallsFile(home, callPet), {
6175
+ tool: "pet_do",
6176
+ petId: callPet,
6177
+ ok: callDetail === void 0,
6178
+ ...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
6179
+ durationMs: Date.now() - callStartedAt,
6180
+ at: Date.now()
6181
+ });
5867
6182
  }
5868
6183
  }
5869
6184
  }));
@@ -5882,8 +6197,12 @@ function registerPetTools(ctx, home, pet) {
5882
6197
  }]
5883
6198
  },
5884
6199
  async execute(args, exec) {
6200
+ const callStartedAt = Date.now();
6201
+ let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
6202
+ let callDetail;
5885
6203
  try {
5886
6204
  const entry = await resolveTarget(args.pet, exec?.agent?.id);
6205
+ callPet = entry.id;
5887
6206
  const cursor = digestCursors.get(entry.id) ?? 0;
5888
6207
  let payload = await readEvents(pet, entry, cursor, 0);
5889
6208
  if (payload.latest < cursor) payload = await readEvents(pet, entry, 0, 0);
@@ -5900,7 +6219,7 @@ function registerPetTools(ctx, home, pet) {
5900
6219
  if (event.at > (lastTouchAt ?? 0)) lastTouchAt = event.at;
5901
6220
  }
5902
6221
  digestCursors.set(entry.id, newest);
5903
- const ledger = await readLedger(ledgerFileFor(home, entry.id));
6222
+ const ledger = await readLedger(home, entry.id);
5904
6223
  const mood = moodOf(ledger.affection);
5905
6224
  return JSON.stringify({
5906
6225
  pet: entry.id,
@@ -5912,7 +6231,17 @@ function registerPetTools(ctx, home, pet) {
5912
6231
  cursor: newest
5913
6232
  });
5914
6233
  } catch (error) {
6234
+ callDetail = error instanceof Error ? error.message : String(error);
5915
6235
  throw classify(error);
6236
+ } finally {
6237
+ appendJsonl(toolCallsFile(home, callPet), {
6238
+ tool: "pet_digest",
6239
+ petId: callPet,
6240
+ ok: callDetail === void 0,
6241
+ ...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
6242
+ durationMs: Date.now() - callStartedAt,
6243
+ at: Date.now()
6244
+ });
5916
6245
  }
5917
6246
  }
5918
6247
  }));
@@ -5937,9 +6266,215 @@ function registerPetTools(ctx, home, pet) {
5937
6266
  const raw = JSON.parse(await readFile(join(home, "pet-interactions.json"), "utf8"));
5938
6267
  if (Array.isArray(raw)) list = raw;
5939
6268
  } catch {}
6269
+ appendJsonl(toolCallsFile(home, "all"), {
6270
+ tool: "pet_interactions",
6271
+ petId: "all",
6272
+ ok: true,
6273
+ durationMs: 0,
6274
+ at: Date.now()
6275
+ });
5940
6276
  return JSON.stringify({ recent: list.slice(0, limit) });
5941
6277
  }
5942
6278
  }));
6279
+ const settleDecay = (ledger, now) => {
6280
+ const silentDays = (now - Math.max(ledger.lastTouchAt ?? 0, ledger.adjustments?.[0]?.at ?? 0, ledger.lastDecayAt ?? 0)) / 864e5;
6281
+ if (silentDays <= DECAY_START_DAYS) return 0;
6282
+ return Math.min(ledger.affection, Math.floor((silentDays - DECAY_START_DAYS) / DECAY_STEP_DAYS) + 1);
6283
+ };
6284
+ scope.tools.register(defineTool({
6285
+ name: "pet_affection_get",
6286
+ description: "Read the FULL affection context before adjusting it: the current value you maintain (and its mood), the touch FACTS the system records (counts by part — cause, not feeling), your recent adjustments with their reasons, the remaining daily quota, and any pending decay. Call this whenever you are deciding whether and how to adjust.",
6287
+ parameters: { pet: {
6288
+ type: "string",
6289
+ description: "Which pet: its id from pet_state. Omit for the default pet."
6290
+ } },
6291
+ output: {
6292
+ schema: { type: "string" },
6293
+ render: (_args, value) => [{
6294
+ type: "text",
6295
+ text: value
6296
+ }]
6297
+ },
6298
+ async execute(args, exec) {
6299
+ const callStartedAt = Date.now();
6300
+ let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
6301
+ let callDetail;
6302
+ try {
6303
+ const entry = await resolveTarget(args.pet, exec?.agent?.id);
6304
+ callPet = entry.id;
6305
+ const ledger = await readLedger(home, entry.id);
6306
+ const pendingDecay = settleDecay(ledger, Date.now());
6307
+ const effective = Math.max(0, ledger.affection - pendingDecay);
6308
+ const today = (/* @__PURE__ */ new Date()).toDateString();
6309
+ const quota = ledger.quota?.date === today ? ledger.quota : {
6310
+ date: today,
6311
+ count: 0,
6312
+ sum: 0
6313
+ };
6314
+ return JSON.stringify({
6315
+ pet: entry.id,
6316
+ affection: effective,
6317
+ mood: moodOf(effective),
6318
+ touch: {
6319
+ total: ledger.total,
6320
+ byPart: ledger.byPart,
6321
+ lastTouchAt: ledger.lastTouchAt ?? null
6322
+ },
6323
+ bond: {
6324
+ value: ledger.bond?.value ?? 0,
6325
+ days: ledger.bond?.days ?? 0,
6326
+ tier: bondTier(ledger.bond?.value ?? 0)
6327
+ },
6328
+ recentBondAdjustments: (ledger.bondAdjustments ?? []).slice(0, 3),
6329
+ recentAdjustments: (ledger.adjustments ?? []).slice(0, 5),
6330
+ quota: {
6331
+ used: quota.count,
6332
+ limit: ADJUST_DAILY_LIMIT,
6333
+ sum: quota.sum,
6334
+ sumLimit: ADJUST_DAILY_SUM
6335
+ },
6336
+ pendingDecay
6337
+ });
6338
+ } catch (error) {
6339
+ callDetail = error instanceof Error ? error.message : String(error);
6340
+ throw classify(error);
6341
+ } finally {
6342
+ appendJsonl(toolCallsFile(home, callPet), {
6343
+ tool: "pet_affection_get",
6344
+ petId: callPet,
6345
+ ok: callDetail === void 0,
6346
+ ...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
6347
+ durationMs: Date.now() - callStartedAt,
6348
+ at: Date.now()
6349
+ });
6350
+ }
6351
+ }
6352
+ }));
6353
+ scope.tools.register(defineTool({
6354
+ name: "pet_affection_adjust",
6355
+ description: "Adjust the affection YOU maintain toward the user: delta (-3..+3 integer) plus a SPECIFIC reason citing what happened. Touch counts are facts the system records separately — this tool is your judgment layer, and the driver never moves the value on a touch. Limits: at least 5 minutes between adjustments, 8 per day, |daily sum| <= 10 — violations are REJECTED with the remaining quota, not clamped. Judge honestly: this number is your read of the relationship, not a score to farm.",
6356
+ parameters: {
6357
+ delta: {
6358
+ type: "number",
6359
+ required: true,
6360
+ description: "The change: a NON-ZERO integer between -3 and +3."
6361
+ },
6362
+ reason: {
6363
+ type: "string",
6364
+ required: true,
6365
+ description: "WHY this delta — cite the concrete interaction. Empty or generic reasons are rejected."
6366
+ },
6367
+ pet: {
6368
+ type: "string",
6369
+ description: "Which pet: its id from pet_state. Omit for the default pet."
6370
+ },
6371
+ bondDelta: {
6372
+ type: "number",
6373
+ description: "OPTIONAL change to the long-term bond: a NON-ZERO integer between -1 and +1. The slowest variable — the smallest steps."
6374
+ },
6375
+ bondReason: {
6376
+ type: "string",
6377
+ description: "Required when bondDelta is given: the concrete event that moved the bond."
6378
+ }
6379
+ },
6380
+ output: {
6381
+ schema: { type: "string" },
6382
+ render: (_args, value) => [{
6383
+ type: "text",
6384
+ text: value
6385
+ }]
6386
+ },
6387
+ async execute(args, exec) {
6388
+ const callStartedAt = Date.now();
6389
+ let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
6390
+ let callDetail;
6391
+ try {
6392
+ const entry = await resolveTarget(args.pet, exec?.agent?.id);
6393
+ callPet = entry.id;
6394
+ const ledger = await readLedger(home, entry.id);
6395
+ const now = Date.now();
6396
+ const pendingDecay = settleDecay(ledger, now);
6397
+ if (pendingDecay > 0) {
6398
+ ledger.affection = Math.max(0, ledger.affection - pendingDecay);
6399
+ ledger.adjustments = [{
6400
+ delta: -pendingDecay,
6401
+ reason: `system: decay(${String(pendingDecay)} 天未互动)`,
6402
+ at: now,
6403
+ system: true
6404
+ }, ...ledger.adjustments ?? []];
6405
+ ledger.lastDecayAt = now;
6406
+ }
6407
+ const delta = typeof args.delta === "number" && Number.isInteger(args.delta) ? args.delta : NaN;
6408
+ const reason = typeof args.reason === "string" ? args.reason.trim() : "";
6409
+ if (Number.isNaN(delta) || delta === 0 || Math.abs(delta) > ADJUST_MAX_DELTA) throw new Error(`delta 必须是 -${String(ADJUST_MAX_DELTA)}..+${String(ADJUST_MAX_DELTA)} 的非零整数(收到 ${String(args.delta)})`);
6410
+ if (reason === "") throw new Error("reason 必填:没有理由的调整不予记录");
6411
+ const adjustments = ledger.adjustments ?? [];
6412
+ const cooldownLeft = (adjustments.find((a) => !a.system)?.at ?? 0) + ADJUST_COOLDOWN_MS - now;
6413
+ if (cooldownLeft > 0) throw new Error(`距上次调整还有 ${String(Math.ceil(cooldownLeft / 1e3))} 秒冷却`);
6414
+ const today = (/* @__PURE__ */ new Date()).toDateString();
6415
+ const quota = ledger.quota?.date === today ? ledger.quota : {
6416
+ date: today,
6417
+ count: 0,
6418
+ sum: 0
6419
+ };
6420
+ if (quota.count >= ADJUST_DAILY_LIMIT) throw new Error(`今日调整次数已用完(${String(ADJUST_DAILY_LIMIT)})——明天再判断,或者相信当前的关系状态`);
6421
+ if (Math.abs(quota.sum + delta) > ADJUST_DAILY_SUM) throw new Error(`今日净变动将超过 ±${String(ADJUST_DAILY_SUM)}(当前 ${String(quota.sum)})——剩下的留给明天`);
6422
+ countBondDay(ledger);
6423
+ const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
6424
+ const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
6425
+ if (bondDelta !== 0) {
6426
+ if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
6427
+ if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
6428
+ }
6429
+ ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
6430
+ quota.count += 1;
6431
+ quota.sum += delta;
6432
+ ledger.quota = quota;
6433
+ ledger.adjustments = [{
6434
+ delta,
6435
+ reason,
6436
+ at: now
6437
+ }, ...adjustments].slice(0, 20);
6438
+ if (bondDelta !== 0) {
6439
+ ledger.bond = {
6440
+ value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
6441
+ days: ledger.bond?.days ?? 0,
6442
+ lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
6443
+ };
6444
+ ledger.bondAdjustments = [{
6445
+ delta: bondDelta,
6446
+ reason: bondReason,
6447
+ at: now
6448
+ }, ...ledger.bondAdjustments ?? []].slice(0, 10);
6449
+ }
6450
+ await writeAffectionRaw(home, entry.id, ledger);
6451
+ return JSON.stringify({
6452
+ pet: entry.id,
6453
+ affection: ledger.affection,
6454
+ mood: moodOf(ledger.affection),
6455
+ applied: delta,
6456
+ quota: {
6457
+ used: quota.count,
6458
+ limit: ADJUST_DAILY_LIMIT,
6459
+ sum: quota.sum,
6460
+ sumLimit: ADJUST_DAILY_SUM
6461
+ }
6462
+ });
6463
+ } catch (error) {
6464
+ callDetail = error instanceof Error ? error.message : String(error);
6465
+ throw classify(error);
6466
+ } finally {
6467
+ appendJsonl(toolCallsFile(home, callPet), {
6468
+ tool: "pet_affection_adjust",
6469
+ petId: callPet,
6470
+ ok: callDetail === void 0,
6471
+ ...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
6472
+ durationMs: Date.now() - callStartedAt,
6473
+ at: Date.now()
6474
+ });
6475
+ }
6476
+ }
6477
+ }));
5943
6478
  scope.tools.register(defineTool({
5944
6479
  name: "pet_state",
5945
6480
  description: "Read the desktop pets' current state: every running pet with its id, what it is doing, which animation is playing, where it sits on screen (location), where it may roam (world, when the pet supports one), its visibility, plus each pet's affection book. Use it when the user refers to a pet, to pick a target for pet_say/pet_do, or to react to a pet.",
@@ -5969,20 +6504,35 @@ function registerPetTools(ctx, home, pet) {
5969
6504
  ]);
5970
6505
  const ledgers = {};
5971
6506
  for (const id of knownIds) {
5972
- const ledger = await readLedger(ledgerFileFor(home, id));
6507
+ const ledger = await readLedger(home, id);
5973
6508
  const mood = moodOf(ledger.affection);
5974
6509
  ledgers[id] = {
5975
6510
  ...ledger,
5976
- mood
6511
+ mood,
6512
+ bond: {
6513
+ value: ledger.bond?.value ?? 0,
6514
+ days: ledger.bond?.days ?? 0,
6515
+ tier: bondTier(ledger.bond?.value ?? 0)
6516
+ }
5977
6517
  };
5978
6518
  }
5979
6519
  const petsOut = states.map((state) => {
5980
6520
  const actions = actionsById.get(state.id);
5981
- return actions === void 0 ? state : {
5982
- ...state,
5983
- actions
6521
+ return {
6522
+ ...actions === void 0 ? state : {
6523
+ ...state,
6524
+ actions
6525
+ },
6526
+ name: voiceNames.get(state.id) ?? DEFAULT_PET_NAME[state.id] ?? state.id
5984
6527
  };
5985
6528
  });
6529
+ appendJsonl(toolCallsFile(home, "all"), {
6530
+ tool: "pet_state",
6531
+ petId: "all",
6532
+ ok: true,
6533
+ durationMs: 0,
6534
+ at: Date.now()
6535
+ });
5986
6536
  return JSON.stringify({
5987
6537
  defaultPet: pets[0]?.id ?? null,
5988
6538
  pets: petsOut,
@@ -6255,4 +6805,4 @@ function installPetService(ctx, home) {
6255
6805
  return new PetHttpService(ctx, home);
6256
6806
  }
6257
6807
  //#endregion
6258
- export { logStamp as a, installPetDriver as i, installPetService as n, registerPetTools as o, petHome as r, createUnregisteredPetHttp as t };
6808
+ export { logStamp as a, readAffectionRaw as c, toolCallsFile as d, writeAffectionRaw as f, installPetDriver as i, readJsonl as l, installPetService as n, registerPetTools as o, writePetName as p, petHome as r, eventsFile as s, createUnregisteredPetHttp as t, readPetName as u };