@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
@@ -1,46 +1,44 @@
1
1
  /**
2
- * Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
3
- * (maps core rc.6 session events turn/step/tool boundaries — and the
4
- * session lifecycle onto the pet phases), the affinity ledger, and the
5
- * persisted display config. The API gateway maps this service's methods onto
6
- * `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig`
7
- * for browser consumers.
2
+ * Pet host service — the `pet.*` RPC domain. A composition facade: it wires
3
+ * the pure event projection (`event-projection`) onto the state machine,
4
+ * delegates the affinity economy to the ledger (`ledger`), and routes
5
+ * persistence through `persist`. The API gateway maps these methods onto
6
+ * `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig` for
7
+ * browser consumers.
8
8
  * @module @linxin666/dsh-pet/service
9
9
  */
10
10
  import { Service } from '@deepseek-ai/cordis';
11
- import { applyInteraction, applyTurnReward, defaultAffinityConfig, rankOf, } from "./affinity.js";
12
- import { loadPetPersist, petHomeDir, savePetPersist, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, DISPLAY_INSET_MAX, PET_NAME_MAX_LENGTH, } from "./persist.js";
13
- import { defaultTreatConfig, settleTreatGrants, consumeTreat, } from "./treats.js";
11
+ import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
12
+ import { PetLedger } from "./ledger.js";
13
+ import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
14
14
  import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
15
15
  /** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
16
16
  export const PET_SETTINGS_NAMESPACE = 'pet';
17
17
  /**
18
18
  * Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
19
- * written until a query or interaction arrives; event listeners update only
20
- * in-memory state, and persistence happens on interaction/config changes
21
- * plus every completed turn.
19
+ * written until an economic event or interaction arrives; event listeners
20
+ * update only in-memory state, and persistence happens on economic changes
21
+ * (turn rewards, feeds, config/name changes) — never on a read.
22
22
  */
23
23
  export class PetService extends Service {
24
24
  static inject = [];
25
25
  machine;
26
- affinityConfig;
27
- treatConfig;
26
+ ledger;
28
27
  persistDir;
29
- persist;
30
- /** Completed turns already rewarded, per session (turn numbers are per-session). */
31
- rewardedTurns = new Map();
32
28
  enabled;
33
29
  disposeActivity;
30
+ /** Session whose most recent meaningful event currently drives the global pet. */
31
+ displaySession;
32
+ sessionActivity = new WeakMap();
34
33
  constructor(ctx, config = {}) {
35
34
  super(ctx, 'pet');
36
35
  this.persistDir = config.persistDir ?? petHomeDir();
37
- this.affinityConfig = { ...defaultAffinityConfig, ...(config.affinity ?? {}) };
38
- this.treatConfig = { ...defaultTreatConfig, ...(config.treats ?? {}) };
36
+ const ledgerConfig = { affinity: config.affinity, treats: config.treats };
37
+ this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
39
38
  this.machine = new PetStateMachine({
40
39
  ...defaultPetStateConfig,
41
40
  ...(config.state ?? {}),
42
41
  });
43
- this.persist = loadPetPersist(this.persistDir);
44
42
  this.enabled = config.enabled ?? true;
45
43
  this.syncActivity();
46
44
  }
@@ -54,11 +52,11 @@ export class PetService extends Service {
54
52
  }
55
53
  /** Current persisted display config (read-only view). */
56
54
  display() {
57
- return { ...this.persist.display };
55
+ return { ...this.ledger.snapshot.display };
58
56
  }
59
57
  /** Current persisted pet name (read-only view). */
60
58
  petName() {
61
- return this.persist.name;
59
+ return this.ledger.snapshot.name;
62
60
  }
63
61
  /** Start or stop the session-activity listeners that drive the pet. */
64
62
  setEnabled(enabled) {
@@ -75,38 +73,40 @@ export class PetService extends Service {
75
73
  this.disposeActivity = (() => {
76
74
  const disposers = [
77
75
  this.ctx.on('session/event', (session, event) => {
78
- // rc.6 publishes no 'activity/status' event (the working-activity
79
- // tracker is gone), so the pet derives its phases from the core
80
- // session vocabulary instead.
81
- switch (event.type) {
82
- case 'turn/start':
83
- this.machine.onSessionActive();
84
- break;
85
- case 'step/start':
86
- this.machine.onSessionActive();
87
- this.machine.onActivityStatus({ phase: 'thinking' });
88
- break;
89
- case 'tool/call':
90
- this.machine.onSessionActive();
91
- this.machine.onActivityStatus({ phase: 'tool', line: 'tool: ' + event.data.name });
92
- break;
93
- case 'turn/end':
94
- this.machine.onSessionActive();
95
- if (event.data.reason.kind === 'completed') {
96
- this.machine.onActivityStatus({ phase: 'done' });
97
- this.rewardTurn(String(session.id), event.data.turn);
98
- }
99
- else {
100
- // Aborted / failed turns clear the working pose instead of
101
- // freezing the pet on its last phase.
102
- this.machine.onActivityStatus({ phase: 'idle' });
103
- }
104
- break;
105
- default:
106
- break;
76
+ const runtime = this.activityRuntime(session);
77
+ // `activity/status` is an optional compatibility input. It is not
78
+ // declared as a durable event type by this package because current
79
+ // Harness installations publish the official session vocabulary.
80
+ if (event.type === 'activity/status') {
81
+ const payload = (event.data ?? {});
82
+ if (typeof payload.phase !== 'string' || !isActivityPhase(payload.phase))
83
+ return;
84
+ this.applyActivity(session, {
85
+ phase: payload.phase,
86
+ ...(typeof payload.line === 'string' ? { line: payload.line } : {}),
87
+ ...(typeof payload.phrase === 'string' ? { phrase: payload.phrase } : {}),
88
+ });
89
+ // On a legacy-only stream the compatibility event owns turn
90
+ // rewards. Once any official activity is observed, turn/end owns
91
+ // them and a derived legacy `done` cannot double-count.
92
+ if (payload.phase === 'done' && !runtime.officialEventsSeen) {
93
+ this.rewardLegacyTurn();
94
+ }
95
+ return;
96
+ }
97
+ const transition = projectOfficialEvent(event, runtime);
98
+ if (transition === undefined)
99
+ return;
100
+ runtime.officialEventsSeen = true;
101
+ this.applyActivity(session, transition.input);
102
+ if (transition.completedTurn !== undefined) {
103
+ this.rewardTurn(String(session.id), transition.completedTurn);
107
104
  }
108
105
  }),
109
- this.ctx.on('session/disposed', () => {
106
+ this.ctx.on('session/disposed', (session) => {
107
+ if (session !== this.displaySession)
108
+ return;
109
+ this.displaySession = undefined;
110
110
  this.machine.onSessionDisposed();
111
111
  }),
112
112
  ];
@@ -114,55 +114,46 @@ export class PetService extends Service {
114
114
  dispose(); };
115
115
  })();
116
116
  }
117
+ /** Return the projection state associated with one live session. */
118
+ activityRuntime(session) {
119
+ let runtime = this.sessionActivity.get(session);
120
+ if (runtime === undefined) {
121
+ runtime = emptyProjectionRuntime();
122
+ this.sessionActivity.set(session, runtime);
123
+ }
124
+ return runtime;
125
+ }
126
+ /** Commit one activity as the host-global pet's most recent display state. */
127
+ applyActivity(session, input) {
128
+ this.displaySession = session;
129
+ this.machine.onActivityStatus(input);
130
+ this.machine.onSessionActive();
131
+ }
117
132
  /** RPC: pet or feed the pet. */
118
133
  async interact(kind) {
119
134
  const nowMs = Date.now();
120
- // Feeding consumes a treat: settle the economy first (work + time
121
- // output since the last settlement), then gate on the feed cooldown
122
- // BEFORE spending stock — a feed inside the cooldown must not burn a
123
- // treat for nothing.
124
- if (kind === 'feed')
125
- this.settleTreats(nowMs);
126
- const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig);
127
- if (kind === 'feed' && !outcome.accepted) {
128
- return { reaction: outcome.reaction, delta: 0, affinity: this.affinityView(this.persist.affinity) };
129
- }
130
- if (kind === 'feed') {
131
- const consume = consumeTreat(this.persist.treats);
132
- if (!consume.ok) {
133
- const affinity = this.affinityView(this.persist.affinity);
134
- return {
135
- reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
136
- delta: 0,
137
- affinity,
138
- };
139
- }
140
- this.persist = { ...this.persist, treats: consume.ledger };
141
- }
142
- if (outcome.accepted) {
143
- this.persist = { ...this.persist, affinity: outcome.affinity };
135
+ const result = this.ledger.interact(kind, nowMs);
136
+ if (this.ledger.takeDirty())
144
137
  this.flush();
145
- }
146
- const affinity = this.affinityView(outcome.affinity);
147
- return { reaction: outcome.reaction, delta: outcome.delta, affinity };
138
+ return result;
148
139
  }
149
140
  /** RPC: show or hide the pet. */
150
141
  async setVisible(visible) {
151
- this.persist = { ...this.persist, display: { ...this.persist.display, visible } };
142
+ this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
152
143
  this.flush();
153
144
  this.syncSettingsFromPet();
154
- return { ok: true, display: this.persist.display };
145
+ return { ok: true, display: this.ledger.snapshot.display };
155
146
  }
156
147
  /** RPC: update display config (size / position). Values are clamped to whole pixels. */
157
148
  async setConfig(patch) {
158
- const next = { ...this.persist.display, ...patch };
149
+ const next = { ...this.ledger.snapshot.display, ...patch };
159
150
  next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)));
160
151
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
161
152
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
162
- this.persist = { ...this.persist, display: next };
153
+ this.ledger.setDisplay(next);
163
154
  this.flush();
164
155
  this.syncSettingsFromPet();
165
- return { ok: true, display: this.persist.display };
156
+ return { ok: true, display: this.ledger.snapshot.display };
166
157
  }
167
158
  /** RPC: rename the pet (trimmed, 1–20 chars). */
168
159
  async setName(name) {
@@ -171,7 +162,7 @@ export class PetService extends Service {
171
162
  return { ok: false, error: 'name-empty' };
172
163
  if (trimmed.length > PET_NAME_MAX_LENGTH)
173
164
  return { ok: false, error: 'name-too-long' };
174
- this.persist = { ...this.persist, name: trimmed };
165
+ this.ledger.setName(trimmed);
175
166
  this.flush();
176
167
  this.syncSettingsFromPet();
177
168
  return { ok: true, name: trimmed };
@@ -183,12 +174,13 @@ export class PetService extends Service {
183
174
  * @param section - the resolved settings section.
184
175
  */
185
176
  applySettingsSection(section) {
186
- const next = { ...this.persist.display };
177
+ const next = { ...this.ledger.snapshot.display };
187
178
  next.visible = section.visible && (section.enabled ?? true);
188
179
  next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)));
189
180
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
190
181
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
191
- this.persist = { ...this.persist, display: next, name: section.name.trim() };
182
+ this.ledger.setDisplay(next);
183
+ this.ledger.setName(section.name.trim());
192
184
  this.flush();
193
185
  }
194
186
  /** Mirror the persisted display config into the settings document (best-effort). */
@@ -196,73 +188,48 @@ export class PetService extends Service {
196
188
  const settings = this.ctx.get('settings', false);
197
189
  if (settings === undefined)
198
190
  return;
191
+ const snapshot = this.ledger.snapshot;
199
192
  void settings.update(PET_SETTINGS_NAMESPACE, {
200
- visible: this.persist.display.visible,
201
- size: this.persist.display.size,
202
- right: this.persist.display.right,
203
- bottom: this.persist.display.bottom,
204
- name: this.persist.name,
193
+ visible: snapshot.display.visible,
194
+ size: snapshot.display.size,
195
+ right: snapshot.display.right,
196
+ bottom: snapshot.display.bottom,
197
+ name: snapshot.name,
205
198
  }).catch(() => {
206
199
  // A settings write failure must not break the pet's own persistence.
207
200
  });
208
201
  }
209
202
  /** Award the turn reward once per completed turn (idempotent per session + turn). */
210
203
  rewardTurn(sessionId, turn) {
211
- const last = this.rewardedTurns.get(sessionId) ?? 0;
212
- if (turn <= last)
213
- return;
214
- this.rewardedTurns.set(sessionId, turn);
215
- this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) };
216
- this.flush();
204
+ if (this.ledger.rewardTurn(sessionId, turn, Date.now()))
205
+ this.flush();
217
206
  }
218
- /**
219
- * Settle the treat economy (work + time output since the last settlement)
220
- * and persist whenever the ledger changed. A zero-gain first settlement
221
- * still starts the time clock (anchor write), which is what lets the
222
- * 30-minute time output ever accrue.
223
- */
224
- settleTreats(nowMs) {
225
- const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
226
- if (settlement.ledger !== this.persist.treats) {
227
- this.persist = { ...this.persist, treats: settlement.ledger };
207
+ /** Preserve turn rewards for installations that only emit legacy activity. */
208
+ rewardLegacyTurn() {
209
+ if (this.ledger.rewardLegacyTurn(Date.now()))
228
210
  this.flush();
229
- }
230
211
  }
231
212
  view() {
232
213
  const snapshot = this.machine.render();
233
- // Time-output treats accrue while the host is idle too; settle on read.
234
- this.settleTreats(Date.now());
214
+ // Read-only: the ledger settles on economic events only, never on a read,
215
+ // so polling the state cannot trigger pet.json writes.
235
216
  return {
236
217
  animation: snapshot.animation,
237
218
  ...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
238
219
  phase: snapshot.phase,
239
220
  sessionActive: snapshot.sessionActive,
240
- affinity: this.affinityView(this.persist.affinity),
241
- display: { ...this.persist.display },
242
- name: this.persist.name,
221
+ affinity: this.ledger.affinityView(Date.now()),
222
+ display: { ...this.ledger.snapshot.display },
223
+ name: this.ledger.snapshot.name,
243
224
  treats: {
244
- stocked: this.persist.treats.treats,
245
- max: this.treatConfig.maxTreats,
225
+ stocked: this.ledger.snapshot.treats.treats,
226
+ max: this.ledger.treatMax,
246
227
  },
247
228
  };
248
229
  }
249
- affinityView(affinity) {
250
- const nowMs = Date.now();
251
- const rank = rankOf(affinity.points);
252
- return {
253
- points: affinity.points,
254
- rank: rank.name,
255
- rankEmoji: rank.emoji,
256
- pets: affinity.pets,
257
- feeds: affinity.feeds,
258
- turns: affinity.turns,
259
- petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
260
- feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs,
261
- };
262
- }
263
230
  flush() {
264
231
  try {
265
- savePetPersist(this.persist, this.persistDir);
232
+ savePetPersist(this.ledger.snapshot, this.persistDir);
266
233
  }
267
234
  catch {
268
235
  // Persistence is best-effort; the in-memory ledger keeps working.
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Pet state machine — pure, clock-injected. Maps the pet's working-phase
3
- * vocabulary (the service derives it from core session events) onto the
4
- * 9-state Codex pet animation contract, plus the session lifecycle
5
- * transitions the web UI exposes (turn end celebration, no-session idle).
2
+ * Pet state machine — pure, clock-injected. Maps official DSH session activity
3
+ * and the legacy `activity/status` vocabulary onto the 9-state Codex pet
4
+ * animation contract, plus turn-end celebration and no-session idle.
6
5
  *
7
6
  * The machine is deliberately dumb: it holds the last input phase, the
8
7
  * animation decision, and a one-shot "celebration" window after `done` so the
@@ -10,13 +9,13 @@
10
9
  * function of (input, nowMs); persistence and RPC live in the service.
11
10
  * @module @linxin666/dsh-pet/state
12
11
  */
13
- /** The pet's working-phase vocabulary (derived from core session events by the service). */
14
- export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done';
12
+ /** Activity phases understood by the pet host. */
13
+ export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'review' | 'done' | 'failed';
15
14
  /** The Codex-compatible 9-state animation contract (spritesheet rows). */
16
15
  export type PetAnimation = 'idle' | 'running-right' | 'running-left' | 'waving' | 'jumping' | 'failed' | 'waiting' | 'running' | 'review';
17
16
  /** One input snapshot consumed by the machine. */
18
17
  export interface PetStateInput {
19
- /** Current working phase of the active session. */
18
+ /** Current activity phase of the active session. */
20
19
  phase: ActivityPhase;
21
20
  /** Human-readable status line (plain text). */
22
21
  line?: string;
@@ -44,13 +43,12 @@ export interface PetStateConfig {
44
43
  export declare const defaultPetStateConfig: PetStateConfig;
45
44
  /**
46
45
  * Map one activity phase onto the animation contract.
47
- * - thinking / tool → `running` (focused work), with `running-right` as the
48
- * side-alternating variant the client may use for tool activity.
46
+ * - thinking → `running` and tool `running-right` (focused work).
47
+ * - review `review` while answer text is streaming.
49
48
  * - waiting → `waiting` (expectant pose, needs user input).
50
49
  * - done → `jumping` (celebration), then back to `idle` after the window.
50
+ * - failed → `failed` until another activity event arrives.
51
51
  * - idle → `idle` (calm breathing loop).
52
- * `failed` has no DSH phase source yet; the machine keeps the mapping table
53
- * so a future error event can light it up.
54
52
  */
55
53
  export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
56
54
  /** The spritesheet row index for one animation track. */
@@ -68,7 +66,7 @@ export declare class PetStateMachine {
68
66
  private sessionActive;
69
67
  private doneAt;
70
68
  constructor(config?: PetStateConfig, now?: () => number);
71
- /** Consume one phase snapshot (fed by the service from session events). */
69
+ /** Consume one projected activity update. */
72
70
  onActivityStatus(input: PetStateInput): void;
73
71
  /** A session became the active one (or a fresh session started). */
74
72
  onSessionActive(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;AAE7E,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,eAAe,GACf,cAAc,GACd,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,SAAS,EAAE,YAAY,CAAA;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAA;IACpB,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAsC,CAAA;AAE1E;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAQpE;AAED,yDAAyD;AACzD,wBAAgB,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAarD;AAED;;;GAGG;AACH,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;gBAGf,MAAM,GAAE,cAAsC,EAC9C,GAAG,GAAE,MAAM,MAAiB;IAG/C,2EAA2E;IAC3E,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAO5C,oEAAoE;IACpE,eAAe,IAAI,IAAI;IAIvB,sDAAsD;IACtD,iBAAiB,IAAI,IAAI;IAQzB,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB;CAoB3B"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEnG,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,eAAe,GACf,cAAc,GACd,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,SAAS,EAAE,YAAY,CAAA;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAA;IACpB,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAsC,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAUpE;AAED,yDAAyD;AACzD,wBAAgB,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAarD;AAED;;;GAGG;AACH,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;gBAGf,MAAM,GAAE,cAAsC,EAC9C,GAAG,GAAE,MAAM,MAAiB;IAG/C,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAO5C,oEAAoE;IACpE,eAAe,IAAI,IAAI;IAIvB,sDAAsD;IACtD,iBAAiB,IAAI,IAAI;IAQzB,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB;CAwB3B"}
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Pet state machine — pure, clock-injected. Maps the pet's working-phase
3
- * vocabulary (the service derives it from core session events) onto the
4
- * 9-state Codex pet animation contract, plus the session lifecycle
5
- * transitions the web UI exposes (turn end celebration, no-session idle).
2
+ * Pet state machine — pure, clock-injected. Maps official DSH session activity
3
+ * and the legacy `activity/status` vocabulary onto the 9-state Codex pet
4
+ * animation contract, plus turn-end celebration and no-session idle.
6
5
  *
7
6
  * The machine is deliberately dumb: it holds the last input phase, the
8
7
  * animation decision, and a one-shot "celebration" window after `done` so the
@@ -13,20 +12,21 @@
13
12
  export const defaultPetStateConfig = { celebrateMs: 2400 };
14
13
  /**
15
14
  * Map one activity phase onto the animation contract.
16
- * - thinking / tool → `running` (focused work), with `running-right` as the
17
- * side-alternating variant the client may use for tool activity.
15
+ * - thinking → `running` and tool `running-right` (focused work).
16
+ * - review `review` while answer text is streaming.
18
17
  * - waiting → `waiting` (expectant pose, needs user input).
19
18
  * - done → `jumping` (celebration), then back to `idle` after the window.
19
+ * - failed → `failed` until another activity event arrives.
20
20
  * - idle → `idle` (calm breathing loop).
21
- * `failed` has no DSH phase source yet; the machine keeps the mapping table
22
- * so a future error event can light it up.
23
21
  */
24
22
  export function animationForPhase(phase) {
25
23
  switch (phase) {
26
24
  case 'thinking': return 'running';
27
25
  case 'tool': return 'running-right';
26
+ case 'review': return 'review';
28
27
  case 'waiting': return 'waiting';
29
28
  case 'done': return 'jumping';
29
+ case 'failed': return 'failed';
30
30
  case 'idle': return 'idle';
31
31
  }
32
32
  }
@@ -61,7 +61,7 @@ export class PetStateMachine {
61
61
  this.config = config;
62
62
  this.now = now;
63
63
  }
64
- /** Consume one phase snapshot (fed by the service from session events). */
64
+ /** Consume one projected activity update. */
65
65
  onActivityStatus(input) {
66
66
  this.phase = input.phase;
67
67
  this.line = input.line;
@@ -94,7 +94,11 @@ export class PetStateMachine {
94
94
  animation = 'idle';
95
95
  }
96
96
  }
97
- const bubble = this.phrase ?? this.line;
97
+ const bubble = this.phase === 'done'
98
+ && this.doneAt !== undefined
99
+ && nowMs - this.doneAt >= this.config.celebrateMs
100
+ ? undefined
101
+ : this.phrase ?? this.line;
98
102
  return {
99
103
  animation,
100
104
  ...(bubble === undefined ? {} : { bubble }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
4
- "version": "0.1.13",
4
+ "version": "0.1.15",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
package/src/affinity.ts CHANGED
@@ -84,6 +84,39 @@ export function rankOf(points: number): (typeof AFFINITY_RANKS)[number] {
84
84
  return rank
85
85
  }
86
86
 
87
+ /** Read-only affinity snapshot suited for the RPC view shape. */
88
+ export interface PetAffinityView {
89
+ points: number
90
+ rank: string
91
+ rankEmoji: string
92
+ pets: number
93
+ feeds: number
94
+ turns: number
95
+ /** True while the pet interaction is inside its cooldown. */
96
+ petCooldown: boolean
97
+ /** True while the feed is inside its cooldown. */
98
+ feedCooldown: boolean
99
+ }
100
+
101
+ /** Derive the read-only view of one affinity state at a wall-clock instant. */
102
+ export function affinityViewOf(
103
+ state: AffinityState,
104
+ nowMs: number,
105
+ config: AffinityConfig = defaultAffinityConfig,
106
+ ): PetAffinityView {
107
+ const rank = rankOf(state.points)
108
+ return {
109
+ points: state.points,
110
+ rank: rank.name,
111
+ rankEmoji: rank.emoji,
112
+ pets: state.pets,
113
+ feeds: state.feeds,
114
+ turns: state.turns,
115
+ petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
116
+ feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs,
117
+ }
118
+ }
119
+
87
120
  function clamp(points: number): number {
88
121
  return Math.min(AFFINITY_MAX, Math.max(0, points))
89
122
  }