@linxin666/dsh-pet 0.1.13 → 0.1.15

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 (57) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +11 -7
  3. package/README.zh.md +11 -7
  4. package/lib/client.js +38 -17
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +393 -150
  7. package/lib/invariant.js +1 -1
  8. package/lib/{state-C9EyycwI.js → state-CFyJv0sQ.js} +22 -7
  9. package/lib/types/affinity.d.ts +15 -0
  10. package/lib/types/affinity.d.ts.map +1 -1
  11. package/lib/types/affinity.js +14 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts +26 -11
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PluginSettingsCard.js +27 -7
  15. package/lib/types/client/WhalePet.d.ts.map +1 -1
  16. package/lib/types/client/WhalePet.js +2 -1
  17. package/lib/types/client/index.d.ts +1 -1
  18. package/lib/types/client/index.js +3 -3
  19. package/lib/types/client/settings-form.d.ts +14 -5
  20. package/lib/types/client/settings-form.d.ts.map +1 -1
  21. package/lib/types/client/settings-form.js +38 -10
  22. package/lib/types/dsh-home.d.ts +17 -0
  23. package/lib/types/dsh-home.d.ts.map +1 -0
  24. package/lib/types/dsh-home.js +34 -0
  25. package/lib/types/event-projection.d.ts +36 -0
  26. package/lib/types/event-projection.d.ts.map +1 -0
  27. package/lib/types/event-projection.js +98 -0
  28. package/lib/types/ledger.d.ts +77 -0
  29. package/lib/types/ledger.d.ts.map +1 -0
  30. package/lib/types/ledger.js +139 -0
  31. package/lib/types/persist.d.ts +5 -1
  32. package/lib/types/persist.d.ts.map +1 -1
  33. package/lib/types/persist.js +7 -3
  34. package/lib/types/service.d.ts +24 -44
  35. package/lib/types/service.d.ts.map +1 -1
  36. package/lib/types/service.js +98 -131
  37. package/lib/types/state.d.ts +10 -12
  38. package/lib/types/state.d.ts.map +1 -1
  39. package/lib/types/state.js +14 -10
  40. package/package.json +1 -1
  41. package/src/affinity.ts +33 -0
  42. package/src/client/PluginSettingsCard.tsx +83 -17
  43. package/src/client/WhalePet.test.tsx +25 -1
  44. package/src/client/WhalePet.tsx +6 -0
  45. package/src/client/index.ts +3 -3
  46. package/src/client/pet.module.css +9 -0
  47. package/src/client/settings-card.module.css +6 -0
  48. package/src/client/settings-form.ts +41 -9
  49. package/src/dsh-home.test.ts +35 -0
  50. package/src/dsh-home.ts +36 -0
  51. package/src/event-projection.ts +128 -0
  52. package/src/ledger.test.ts +67 -0
  53. package/src/ledger.ts +183 -0
  54. package/src/persist.ts +7 -3
  55. package/src/service.ts +110 -171
  56. package/src/state.test.ts +4 -1
  57. package/src/state.ts +17 -13
package/lib/index.js CHANGED
@@ -1,12 +1,132 @@
1
- import { a as AFFINITY_MAX, c as applyTurnReward, d as rankOf, i as rowOf, l as defaultAffinityConfig, n as animationForPhase, o as AFFINITY_RANKS, r as defaultPetStateConfig, s as applyInteraction, t as PetStateMachine, u as emptyAffinity } from "./state-C9EyycwI.js";
1
+ import { a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, i as rowOf, l as applyTurnReward, n as animationForPhase, o as AFFINITY_RANKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig } from "./state-CFyJv0sQ.js";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import z from "schemastery";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
6
+ import { isAbsolute, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { fileURLToPath } from "node:url";
10
+ //#region src/event-projection.ts
11
+ /** Fresh projection runtime for a newly seen session. */
12
+ function emptyProjectionRuntime() {
13
+ return {
14
+ activeTools: /* @__PURE__ */ new Set(),
15
+ officialEventsSeen: false,
16
+ stepHadFailure: false
17
+ };
18
+ }
19
+ /** Keep tool names readable inside the compact status bubble. */
20
+ function displayToolName(name) {
21
+ const compact = name.replace(/\s+/g, " ").trim() || "工具";
22
+ return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
23
+ }
24
+ /** Whether a legacy phase is part of the pet's supported vocabulary. */
25
+ function isActivityPhase(phase) {
26
+ return [
27
+ "idle",
28
+ "waiting",
29
+ "thinking",
30
+ "tool",
31
+ "review",
32
+ "done",
33
+ "failed"
34
+ ].includes(phase);
35
+ }
36
+ /**
37
+ * Project the durable DSH session vocabulary into the pet's visual phases.
38
+ * Unknown and log-only events do not disturb the last meaningful activity.
39
+ */
40
+ function projectOfficialEvent(event, runtime) {
41
+ switch (event.type) {
42
+ case "turn/start":
43
+ runtime.activeTools.clear();
44
+ runtime.stepHadFailure = false;
45
+ return { input: {
46
+ phase: "waiting",
47
+ line: "准备开始"
48
+ } };
49
+ case "step/start":
50
+ runtime.activeTools.clear();
51
+ runtime.stepHadFailure = false;
52
+ return { input: {
53
+ phase: "waiting",
54
+ line: "等待模型响应"
55
+ } };
56
+ case "assistant/chunk": {
57
+ const { chunk } = event.data;
58
+ if (chunk.type === "reasoning-delta" && chunk.text.length > 0) return { input: {
59
+ phase: "thinking",
60
+ line: "正在思考"
61
+ } };
62
+ if (chunk.type === "text-delta" && chunk.text.length > 0) return { input: {
63
+ phase: "review",
64
+ line: "整理回复中"
65
+ } };
66
+ return;
67
+ }
68
+ case "assistant/message": return { input: {
69
+ phase: "review",
70
+ line: "整理回复中"
71
+ } };
72
+ case "tool/call":
73
+ runtime.activeTools.add(String(event.data.callId));
74
+ return { input: {
75
+ phase: "tool",
76
+ line: `正在使用 ${displayToolName(event.data.name)}`
77
+ } };
78
+ case "tool/result": {
79
+ const block = event.data.message.content[0];
80
+ runtime.activeTools.delete(String(event.data.message.source.callId));
81
+ runtime.stepHadFailure ||= event.data.error !== void 0 || block.isError === true;
82
+ if (runtime.activeTools.size > 0) return { input: {
83
+ phase: "tool",
84
+ line: `还有 ${runtime.activeTools.size} 个工具运行中`
85
+ } };
86
+ return runtime.stepHadFailure ? { input: {
87
+ phase: "failed",
88
+ line: "工具执行失败"
89
+ } } : { input: {
90
+ phase: "thinking",
91
+ line: "处理工具结果"
92
+ } };
93
+ }
94
+ case "turn/end":
95
+ runtime.activeTools.clear();
96
+ switch (event.data.reason.kind) {
97
+ case "completed": return {
98
+ input: {
99
+ phase: "done",
100
+ line: "完成啦"
101
+ },
102
+ completedTurn: event.data.turn
103
+ };
104
+ case "error": return { input: {
105
+ phase: "failed",
106
+ line: "执行失败"
107
+ } };
108
+ case "max-tokens": return { input: {
109
+ phase: "failed",
110
+ line: "达到输出上限"
111
+ } };
112
+ case "interrupted": return { input: {
113
+ phase: "failed",
114
+ line: "执行意外中断"
115
+ } };
116
+ case "blocked": return { input: {
117
+ phase: "waiting",
118
+ line: "等待继续"
119
+ } };
120
+ case "aborted": return { input: {
121
+ phase: "idle",
122
+ line: "已停止"
123
+ } };
124
+ default: return { input: { phase: "idle" } };
125
+ }
126
+ default: return;
127
+ }
128
+ }
129
+ //#endregion
10
130
  //#region src/treats.ts
11
131
  const defaultTreatConfig = {
12
132
  turnsPerTreat: 3,
@@ -80,6 +200,193 @@ function consumeTreat(ledger) {
80
200
  };
81
201
  }
82
202
  //#endregion
203
+ //#region src/ledger.ts
204
+ /**
205
+ * Pet affinity economy (ledger) — composes the pure affinity and treats
206
+ * modules with the cooldown/dedup bookkeeping and emits updated persistence
207
+ * snapshots, marking dirty so the owning facade decides when to flush. Read
208
+ * paths (view) no longer settle the economy; settlements happen on explicit
209
+ * economic events: completed-turn rewards (official or legacy) and feeds.
210
+ * @module @linxin666/dsh-pet/ledger
211
+ */
212
+ /**
213
+ * Holds the current persistence snapshot and all economy bookkeeping. Every
214
+ * mutating call flags takeDirty so the facade persists exactly once per
215
+ * batch of changes; read methods (snapshot, affinityView) never write.
216
+ */
217
+ var PetLedger = class {
218
+ affinityConfig;
219
+ treatConfig;
220
+ current;
221
+ /** Completed turns already rewarded, per session (turn numbers are per-session). */
222
+ rewardedTurns = /* @__PURE__ */ new Map();
223
+ lastLegacyTurnRewardAt = 0;
224
+ dirty = false;
225
+ constructor(persist, config = {}) {
226
+ this.affinityConfig = {
227
+ ...defaultAffinityConfig,
228
+ ...config.affinity ?? {}
229
+ };
230
+ this.treatConfig = {
231
+ ...defaultTreatConfig,
232
+ ...config.treats ?? {}
233
+ };
234
+ this.current = persist;
235
+ }
236
+ /** Affinity cooldown/rank tuning (read-only). */
237
+ get affinity() {
238
+ return this.affinityConfig;
239
+ }
240
+ /** The current persistence snapshot (trade a copy when mutating). */
241
+ get snapshot() {
242
+ return this.current;
243
+ }
244
+ /** Stock cap reported to clients. */
245
+ get treatMax() {
246
+ return this.treatConfig.maxTreats;
247
+ }
248
+ /** Consume the pending-write flag if any mutation occurred. */
249
+ takeDirty() {
250
+ const was = this.dirty;
251
+ this.dirty = false;
252
+ return was;
253
+ }
254
+ /** Replace the display block (clamping stays a caller concern). */
255
+ setDisplay(display) {
256
+ this.current = {
257
+ ...this.current,
258
+ display
259
+ };
260
+ this.dirty = true;
261
+ }
262
+ /** Replace the pet display name (validation stays a caller concern). */
263
+ setName(name) {
264
+ this.current = {
265
+ ...this.current,
266
+ name
267
+ };
268
+ this.dirty = true;
269
+ }
270
+ /**
271
+ * Settle the treat economy (work + time output since the last settlement).
272
+ * A zero-gain first settlement still starts the time clock (anchor write),
273
+ * which is how the 30-minute time output can ever accrue. Returns true when
274
+ * the in-memory ledger changed and should be persisted.
275
+ */
276
+ settleTreats(nowMs) {
277
+ const settlement = settleTreatGrants(this.current.treats, this.current.affinity.turns, nowMs, this.treatConfig);
278
+ if (settlement.ledger === this.current.treats) return false;
279
+ this.current = {
280
+ ...this.current,
281
+ treats: settlement.ledger
282
+ };
283
+ this.dirty = true;
284
+ return true;
285
+ }
286
+ /**
287
+ * Award the completed-turn reward once per session+turn (idempotent) and
288
+ * run the treat settlement that work output feeds. Returns true when the
289
+ * snapshot changed.
290
+ */
291
+ rewardTurn(sessionId, turn, nowMs) {
292
+ if (turn <= (this.rewardedTurns.get(sessionId) ?? 0)) return false;
293
+ this.rewardedTurns.set(sessionId, turn);
294
+ let changed = this.applyTurnReward();
295
+ if (this.settleTreats(nowMs)) changed = true;
296
+ return changed;
297
+ }
298
+ /** Preserve turn rewards for installations that only emit legacy activity. */
299
+ rewardLegacyTurn(nowMs) {
300
+ if (nowMs - this.lastLegacyTurnRewardAt < 5e3) return false;
301
+ this.lastLegacyTurnRewardAt = nowMs;
302
+ let changed = this.applyTurnReward();
303
+ if (this.settleTreats(nowMs)) changed = true;
304
+ return changed;
305
+ }
306
+ applyTurnReward() {
307
+ this.current = {
308
+ ...this.current,
309
+ affinity: applyTurnReward(this.current.affinity, this.affinityConfig)
310
+ };
311
+ this.dirty = true;
312
+ return true;
313
+ }
314
+ /**
315
+ * Pet or feed the pet. Feeding settles first, then gates on the feed
316
+ * cooldown before spending stock — a feed inside the cooldown must not burn
317
+ * a treat for nothing.
318
+ */
319
+ interact(kind, nowMs) {
320
+ if (kind === "feed") this.settleTreats(nowMs);
321
+ const outcome = applyInteraction(this.current.affinity, kind, nowMs, this.affinityConfig);
322
+ if (kind === "feed" && !outcome.accepted) return {
323
+ reaction: outcome.reaction,
324
+ delta: 0,
325
+ affinity: this.affinityView(nowMs)
326
+ };
327
+ if (kind === "feed") {
328
+ const consume = consumeTreat(this.current.treats);
329
+ if (!consume.ok) return {
330
+ reaction: "没有小鱼干了,多陪鲸鱼娘工作一会儿吧~",
331
+ delta: 0,
332
+ affinity: this.affinityView(nowMs)
333
+ };
334
+ this.current = {
335
+ ...this.current,
336
+ treats: consume.ledger
337
+ };
338
+ this.dirty = true;
339
+ }
340
+ if (outcome.accepted) {
341
+ this.current = {
342
+ ...this.current,
343
+ affinity: outcome.affinity
344
+ };
345
+ this.dirty = true;
346
+ }
347
+ return {
348
+ reaction: outcome.reaction,
349
+ delta: outcome.delta,
350
+ affinity: this.affinityView(nowMs)
351
+ };
352
+ }
353
+ /** Current affinity view for the RPC snapshot. */
354
+ affinityView(nowMs) {
355
+ return affinityViewOf(this.current.affinity, nowMs, this.affinityConfig);
356
+ }
357
+ };
358
+ //#endregion
359
+ //#region src/dsh-home.ts
360
+ /**
361
+ * DSH_HOME resolution shared by the plugin family's Host halves: the
362
+ * environment override wins, the platform home fallback follows. Mirrors
363
+ * what dsh-pet and dsh-liangshen each used to implement locally.
364
+ */
365
+ /** Expand a leading ~ (or ~user) in a path, platform-style. */
366
+ function expandHome(path, home = homedir()) {
367
+ if (path === "~") return home;
368
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
369
+ return path;
370
+ }
371
+ /**
372
+ * Resolve the DSH home directory.
373
+ * @param env - process environment to read DSH_HOME from.
374
+ * @param home - platform home directory fallback (test seam).
375
+ * @returns the absolute DSH home path.
376
+ */
377
+ function resolveDshHome(env = process.env, home = homedir()) {
378
+ const raw = env.DSH_HOME;
379
+ if (raw !== void 0 && raw.trim() !== "") {
380
+ const expanded = expandHome(raw.trim(), home);
381
+ return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
382
+ }
383
+ return join(home, ".dsh");
384
+ }
385
+ /** Resolve the DSH home directory from the live environment. */
386
+ function dshHome() {
387
+ return resolveDshHome();
388
+ }
389
+ //#endregion
83
390
  //#region src/persist.ts
84
391
  /**
85
392
  * Pet persistence — tiny JSON store for affinity + display config, written
@@ -104,9 +411,13 @@ function emptyPersist() {
104
411
  display: { ...defaultDisplayConfig }
105
412
  };
106
413
  }
107
- /** Resolve the persistence directory ($DSH_HOME or ~/.dsh). */
414
+ /**
415
+ * Resolve the persistence directory ($DSH_HOME or ~/.dsh). Delegates to the
416
+ * shared {@link dshHome} resolution so the plugin family keeps one DSH_HOME
417
+ * definition (env override, ~ expansion, cwd-joined relative values).
418
+ */
108
419
  function petHomeDir() {
109
- return process.env.DSH_HOME ?? join(homedir(), ".dsh");
420
+ return dshHome();
110
421
  }
111
422
  /** Numeric field guard: finite numbers only, else the fallback. */
112
423
  function finiteNum(value, fallback) {
@@ -164,37 +475,32 @@ function savePetPersist(data, dir = petHomeDir()) {
164
475
  }
165
476
  /**
166
477
  * Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
167
- * written until a query or interaction arrives; event listeners update only
168
- * in-memory state, and persistence happens on interaction/config changes
169
- * plus every completed turn.
478
+ * written until an economic event or interaction arrives; event listeners
479
+ * update only in-memory state, and persistence happens on economic changes
480
+ * (turn rewards, feeds, config/name changes) — never on a read.
170
481
  */
171
482
  var PetService = class extends Service {
172
483
  static inject = [];
173
484
  machine;
174
- affinityConfig;
175
- treatConfig;
485
+ ledger;
176
486
  persistDir;
177
- persist;
178
- /** Completed turns already rewarded, per session (turn numbers are per-session). */
179
- rewardedTurns = /* @__PURE__ */ new Map();
180
487
  enabled;
181
488
  disposeActivity;
489
+ /** Session whose most recent meaningful event currently drives the global pet. */
490
+ displaySession;
491
+ sessionActivity = /* @__PURE__ */ new WeakMap();
182
492
  constructor(ctx, config = {}) {
183
493
  super(ctx, "pet");
184
494
  this.persistDir = config.persistDir ?? petHomeDir();
185
- this.affinityConfig = {
186
- ...defaultAffinityConfig,
187
- ...config.affinity ?? {}
188
- };
189
- this.treatConfig = {
190
- ...defaultTreatConfig,
191
- ...config.treats ?? {}
495
+ const ledgerConfig = {
496
+ affinity: config.affinity,
497
+ treats: config.treats
192
498
  };
499
+ this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
193
500
  this.machine = new PetStateMachine({
194
501
  ...defaultPetStateConfig,
195
502
  ...config.state ?? {}
196
503
  });
197
- this.persist = loadPetPersist(this.persistDir);
198
504
  this.enabled = config.enabled ?? true;
199
505
  this.syncActivity();
200
506
  }
@@ -208,11 +514,11 @@ var PetService = class extends Service {
208
514
  }
209
515
  /** Current persisted display config (read-only view). */
210
516
  display() {
211
- return { ...this.persist.display };
517
+ return { ...this.ledger.snapshot.display };
212
518
  }
213
519
  /** Current persisted pet name (read-only view). */
214
520
  petName() {
215
- return this.persist.name;
521
+ return this.ledger.snapshot.name;
216
522
  }
217
523
  /** Start or stop the session-activity listeners that drive the pet. */
218
524
  setEnabled(enabled) {
@@ -227,31 +533,26 @@ var PetService = class extends Service {
227
533
  if (!this.enabled) return;
228
534
  this.disposeActivity = (() => {
229
535
  const disposers = [this.ctx.on("session/event", (session, event) => {
230
- switch (event.type) {
231
- case "turn/start":
232
- this.machine.onSessionActive();
233
- break;
234
- case "step/start":
235
- this.machine.onSessionActive();
236
- this.machine.onActivityStatus({ phase: "thinking" });
237
- break;
238
- case "tool/call":
239
- this.machine.onSessionActive();
240
- this.machine.onActivityStatus({
241
- phase: "tool",
242
- line: "tool: " + event.data.name
243
- });
244
- break;
245
- case "turn/end":
246
- this.machine.onSessionActive();
247
- if (event.data.reason.kind === "completed") {
248
- this.machine.onActivityStatus({ phase: "done" });
249
- this.rewardTurn(String(session.id), event.data.turn);
250
- } else this.machine.onActivityStatus({ phase: "idle" });
251
- break;
252
- default: break;
536
+ const runtime = this.activityRuntime(session);
537
+ if (event.type === "activity/status") {
538
+ const payload = event.data ?? {};
539
+ if (typeof payload.phase !== "string" || !isActivityPhase(payload.phase)) return;
540
+ this.applyActivity(session, {
541
+ phase: payload.phase,
542
+ ...typeof payload.line === "string" ? { line: payload.line } : {},
543
+ ...typeof payload.phrase === "string" ? { phrase: payload.phrase } : {}
544
+ });
545
+ if (payload.phase === "done" && !runtime.officialEventsSeen) this.rewardLegacyTurn();
546
+ return;
253
547
  }
254
- }), this.ctx.on("session/disposed", () => {
548
+ const transition = projectOfficialEvent(event, runtime);
549
+ if (transition === void 0) return;
550
+ runtime.officialEventsSeen = true;
551
+ this.applyActivity(session, transition.input);
552
+ if (transition.completedTurn !== void 0) this.rewardTurn(String(session.id), transition.completedTurn);
553
+ }), this.ctx.on("session/disposed", (session) => {
554
+ if (session !== this.displaySession) return;
555
+ this.displaySession = void 0;
255
556
  this.machine.onSessionDisposed();
256
557
  })];
257
558
  return () => {
@@ -259,76 +560,56 @@ var PetService = class extends Service {
259
560
  };
260
561
  })();
261
562
  }
563
+ /** Return the projection state associated with one live session. */
564
+ activityRuntime(session) {
565
+ let runtime = this.sessionActivity.get(session);
566
+ if (runtime === void 0) {
567
+ runtime = emptyProjectionRuntime();
568
+ this.sessionActivity.set(session, runtime);
569
+ }
570
+ return runtime;
571
+ }
572
+ /** Commit one activity as the host-global pet's most recent display state. */
573
+ applyActivity(session, input) {
574
+ this.displaySession = session;
575
+ this.machine.onActivityStatus(input);
576
+ this.machine.onSessionActive();
577
+ }
262
578
  /** RPC: pet or feed the pet. */
263
579
  async interact(kind) {
264
580
  const nowMs = Date.now();
265
- if (kind === "feed") this.settleTreats(nowMs);
266
- const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig);
267
- if (kind === "feed" && !outcome.accepted) return {
268
- reaction: outcome.reaction,
269
- delta: 0,
270
- affinity: this.affinityView(this.persist.affinity)
271
- };
272
- if (kind === "feed") {
273
- const consume = consumeTreat(this.persist.treats);
274
- if (!consume.ok) return {
275
- reaction: "没有小鱼干了,多陪鲸鱼娘工作一会儿吧~",
276
- delta: 0,
277
- affinity: this.affinityView(this.persist.affinity)
278
- };
279
- this.persist = {
280
- ...this.persist,
281
- treats: consume.ledger
282
- };
283
- }
284
- if (outcome.accepted) {
285
- this.persist = {
286
- ...this.persist,
287
- affinity: outcome.affinity
288
- };
289
- this.flush();
290
- }
291
- const affinity = this.affinityView(outcome.affinity);
292
- return {
293
- reaction: outcome.reaction,
294
- delta: outcome.delta,
295
- affinity
296
- };
581
+ const result = this.ledger.interact(kind, nowMs);
582
+ if (this.ledger.takeDirty()) this.flush();
583
+ return result;
297
584
  }
298
585
  /** RPC: show or hide the pet. */
299
586
  async setVisible(visible) {
300
- this.persist = {
301
- ...this.persist,
302
- display: {
303
- ...this.persist.display,
304
- visible
305
- }
306
- };
587
+ this.ledger.setDisplay({
588
+ ...this.ledger.snapshot.display,
589
+ visible
590
+ });
307
591
  this.flush();
308
592
  this.syncSettingsFromPet();
309
593
  return {
310
594
  ok: true,
311
- display: this.persist.display
595
+ display: this.ledger.snapshot.display
312
596
  };
313
597
  }
314
598
  /** RPC: update display config (size / position). Values are clamped to whole pixels. */
315
599
  async setConfig(patch) {
316
600
  const next = {
317
- ...this.persist.display,
601
+ ...this.ledger.snapshot.display,
318
602
  ...patch
319
603
  };
320
604
  next.size = Math.round(Math.min(512, Math.max(32, next.size)));
321
605
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
322
606
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
323
- this.persist = {
324
- ...this.persist,
325
- display: next
326
- };
607
+ this.ledger.setDisplay(next);
327
608
  this.flush();
328
609
  this.syncSettingsFromPet();
329
610
  return {
330
611
  ok: true,
331
- display: this.persist.display
612
+ display: this.ledger.snapshot.display
332
613
  };
333
614
  }
334
615
  /** RPC: rename the pet (trimmed, 1–20 chars). */
@@ -342,10 +623,7 @@ var PetService = class extends Service {
342
623
  ok: false,
343
624
  error: "name-too-long"
344
625
  };
345
- this.persist = {
346
- ...this.persist,
347
- name: trimmed
348
- };
626
+ this.ledger.setName(trimmed);
349
627
  this.flush();
350
628
  this.syncSettingsFromPet();
351
629
  return {
@@ -360,90 +638,55 @@ var PetService = class extends Service {
360
638
  * @param section - the resolved settings section.
361
639
  */
362
640
  applySettingsSection(section) {
363
- const next = { ...this.persist.display };
641
+ const next = { ...this.ledger.snapshot.display };
364
642
  next.visible = section.visible && (section.enabled ?? true);
365
643
  next.size = Math.round(Math.min(512, Math.max(32, section.size)));
366
644
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
367
645
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
368
- this.persist = {
369
- ...this.persist,
370
- display: next,
371
- name: section.name.trim()
372
- };
646
+ this.ledger.setDisplay(next);
647
+ this.ledger.setName(section.name.trim());
373
648
  this.flush();
374
649
  }
375
650
  /** Mirror the persisted display config into the settings document (best-effort). */
376
651
  syncSettingsFromPet() {
377
652
  const settings = this.ctx.get("settings", false);
378
653
  if (settings === void 0) return;
654
+ const snapshot = this.ledger.snapshot;
379
655
  settings.update("pet", {
380
- visible: this.persist.display.visible,
381
- size: this.persist.display.size,
382
- right: this.persist.display.right,
383
- bottom: this.persist.display.bottom,
384
- name: this.persist.name
656
+ visible: snapshot.display.visible,
657
+ size: snapshot.display.size,
658
+ right: snapshot.display.right,
659
+ bottom: snapshot.display.bottom,
660
+ name: snapshot.name
385
661
  }).catch(() => {});
386
662
  }
387
663
  /** Award the turn reward once per completed turn (idempotent per session + turn). */
388
664
  rewardTurn(sessionId, turn) {
389
- if (turn <= (this.rewardedTurns.get(sessionId) ?? 0)) return;
390
- this.rewardedTurns.set(sessionId, turn);
391
- this.persist = {
392
- ...this.persist,
393
- affinity: applyTurnReward(this.persist.affinity, this.affinityConfig)
394
- };
395
- this.flush();
665
+ if (this.ledger.rewardTurn(sessionId, turn, Date.now())) this.flush();
396
666
  }
397
- /**
398
- * Settle the treat economy (work + time output since the last settlement)
399
- * and persist whenever the ledger changed. A zero-gain first settlement
400
- * still starts the time clock (anchor write), which is what lets the
401
- * 30-minute time output ever accrue.
402
- */
403
- settleTreats(nowMs) {
404
- const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
405
- if (settlement.ledger !== this.persist.treats) {
406
- this.persist = {
407
- ...this.persist,
408
- treats: settlement.ledger
409
- };
410
- this.flush();
411
- }
667
+ /** Preserve turn rewards for installations that only emit legacy activity. */
668
+ rewardLegacyTurn() {
669
+ if (this.ledger.rewardLegacyTurn(Date.now())) this.flush();
412
670
  }
413
671
  view() {
414
672
  const snapshot = this.machine.render();
415
- this.settleTreats(Date.now());
416
673
  return {
417
674
  animation: snapshot.animation,
418
675
  ...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
419
676
  phase: snapshot.phase,
420
677
  sessionActive: snapshot.sessionActive,
421
- affinity: this.affinityView(this.persist.affinity),
422
- display: { ...this.persist.display },
423
- name: this.persist.name,
678
+ affinity: this.ledger.affinityView(Date.now()),
679
+ display: { ...this.ledger.snapshot.display },
680
+ name: this.ledger.snapshot.name,
424
681
  treats: {
425
- stocked: this.persist.treats.treats,
426
- max: this.treatConfig.maxTreats
682
+ stocked: this.ledger.snapshot.treats.treats,
683
+ max: this.ledger.treatMax
427
684
  }
428
685
  };
429
686
  }
430
- affinityView(affinity) {
431
- const nowMs = Date.now();
432
- const rank = rankOf(affinity.points);
433
- return {
434
- points: affinity.points,
435
- rank: rank.name,
436
- rankEmoji: rank.emoji,
437
- pets: affinity.pets,
438
- feeds: affinity.feeds,
439
- turns: affinity.turns,
440
- petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
441
- feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs
442
- };
443
- }
444
687
  flush() {
445
688
  try {
446
- savePetPersist(this.persist, this.persistDir);
689
+ savePetPersist(this.ledger.snapshot, this.persistDir);
447
690
  } catch {}
448
691
  }
449
692
  };
package/lib/invariant.js CHANGED
@@ -1,4 +1,4 @@
1
- import { l as defaultAffinityConfig, n as animationForPhase, o as AFFINITY_RANKS } from "./state-C9EyycwI.js";
1
+ import { n as animationForPhase, o as AFFINITY_RANKS, u as defaultAffinityConfig } from "./state-CFyJv0sQ.js";
2
2
  //#region src/invariant.ts
3
3
  /**
4
4
  * Package invariants — cheap structural checks run at import time on the