@linxin666/dsh-pet 0.1.20 → 0.2.1

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 (85) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +18 -11
  3. package/README.zh.md +18 -11
  4. package/assets/whale/pet.json +9 -0
  5. package/lib/client.js +394 -58
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +1007 -43
  8. package/lib/invariant.js +1 -1
  9. package/lib/{state-Diiq8vjk.js → state-DrMX22GL.js} +86 -28
  10. package/lib/types/affinity.d.ts +4 -0
  11. package/lib/types/affinity.d.ts.map +1 -1
  12. package/lib/types/affinity.js +32 -6
  13. package/lib/types/chatter.d.ts +111 -0
  14. package/lib/types/chatter.d.ts.map +1 -0
  15. package/lib/types/chatter.js +756 -0
  16. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  17. package/lib/types/client/PetDockEntry.js +1 -1
  18. package/lib/types/client/PetSprite.d.ts.map +1 -1
  19. package/lib/types/client/PetSprite.js +133 -16
  20. package/lib/types/client/PluginSettingsCard.d.ts +28 -0
  21. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  22. package/lib/types/client/PluginSettingsCard.js +147 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +1 -0
  25. package/lib/types/client/locales.d.ts +4 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +4 -0
  28. package/lib/types/client/sequences.d.ts +10 -0
  29. package/lib/types/client/sequences.d.ts.map +1 -0
  30. package/lib/types/client/sequences.js +20 -0
  31. package/lib/types/event-projection.d.ts +14 -1
  32. package/lib/types/event-projection.d.ts.map +1 -1
  33. package/lib/types/event-projection.js +38 -17
  34. package/lib/types/ledger.d.ts.map +1 -1
  35. package/lib/types/ledger.js +15 -7
  36. package/lib/types/loopback.d.ts +25 -0
  37. package/lib/types/loopback.d.ts.map +1 -0
  38. package/lib/types/loopback.js +68 -0
  39. package/lib/types/persist.d.ts.map +1 -1
  40. package/lib/types/persist.js +3 -1
  41. package/lib/types/registry.d.ts +7 -1
  42. package/lib/types/registry.d.ts.map +1 -1
  43. package/lib/types/registry.js +47 -3
  44. package/lib/types/remarks.d.ts +2 -0
  45. package/lib/types/remarks.d.ts.map +1 -1
  46. package/lib/types/remarks.js +35 -0
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +14 -0
  49. package/lib/types/service.d.ts +6 -0
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +15 -2
  52. package/lib/types/state.d.ts +7 -4
  53. package/lib/types/state.d.ts.map +1 -1
  54. package/lib/types/state.js +20 -20
  55. package/package.json +9 -9
  56. package/src/affinity.test.ts +4 -2
  57. package/src/affinity.ts +37 -6
  58. package/src/chatter.test.ts +154 -0
  59. package/src/chatter.ts +787 -0
  60. package/src/client/PetDockEntry.test.tsx +93 -0
  61. package/src/client/PetDockEntry.tsx +1 -0
  62. package/src/client/PetSprite.test.tsx +253 -3
  63. package/src/client/PetSprite.tsx +192 -37
  64. package/src/client/PluginSettingsCard.tsx +229 -18
  65. package/src/client/index.test.tsx +95 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/locales.ts +4 -0
  68. package/src/client/pet-css.test.ts +54 -0
  69. package/src/client/pet.module.css +215 -57
  70. package/src/client/sequences.test.ts +33 -0
  71. package/src/client/sequences.ts +33 -0
  72. package/src/client/settings-card.module.css +90 -1
  73. package/src/event-projection.ts +49 -16
  74. package/src/ledger.test.ts +42 -1
  75. package/src/ledger.ts +15 -7
  76. package/src/loopback.ts +63 -0
  77. package/src/persist.test.ts +18 -1
  78. package/src/persist.ts +3 -1
  79. package/src/registry.test.ts +79 -4
  80. package/src/registry.ts +58 -6
  81. package/src/remarks.ts +36 -0
  82. package/src/routes.ts +11 -0
  83. package/src/service.ts +27 -2
  84. package/src/state.test.ts +13 -0
  85. package/src/state.ts +24 -18
@@ -12,6 +12,7 @@ import { existsSync } from 'node:fs';
12
12
  import { readFile } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
14
  import { petEntryView } from './registry.js';
15
+ import { isLoopbackRequest } from './loopback.js';
15
16
  /** Browser-facing base path of the pet API. */
16
17
  export const PET_API_PREFIX = '/api/pet';
17
18
  /** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
@@ -75,12 +76,21 @@ function readJsonBody(req) {
75
76
  req.on('error', reject);
76
77
  });
77
78
  }
79
+ /** Shared route fence: the browser UI is a loopback client; LAN hosts stay out. */
80
+ function guard(req, res) {
81
+ if (isLoopbackRequest(req))
82
+ return true;
83
+ json(res, 403, { ok: false, error: 'forbidden: loopback-only' });
84
+ return false;
85
+ }
78
86
  /** Wrap one async service call as a GET JSON route. */
79
87
  function getRoute(path, run) {
80
88
  return {
81
89
  kind: 'exact',
82
90
  path,
83
91
  handler: (req, res) => {
92
+ if (!guard(req, res))
93
+ return;
84
94
  if (!requireMethod(req, res, 'GET'))
85
95
  return;
86
96
  run().then((value) => json(res, 200, value), (error) => {
@@ -95,6 +105,8 @@ function postRoute(path, run) {
95
105
  kind: 'exact',
96
106
  path,
97
107
  handler: (req, res) => {
108
+ if (!guard(req, res))
109
+ return Promise.resolve();
98
110
  if (!requireMethod(req, res, 'POST'))
99
111
  return Promise.resolve();
100
112
  return readJsonBody(req).then((body) => {
@@ -127,6 +139,8 @@ function dirAliases(registry) {
127
139
  function assetHandler(registry) {
128
140
  const aliases = dirAliases(registry);
129
141
  return (req, res) => {
142
+ if (!guard(req, res))
143
+ return;
130
144
  if (req.method !== 'GET' && req.method !== 'HEAD') {
131
145
  res.writeHead(405);
132
146
  res.end();
@@ -113,6 +113,12 @@ export interface PetStateView {
113
113
  /** Stock cap. */
114
114
  max: number;
115
115
  };
116
+ /**
117
+ * The display session's fresh inner whisper (碎碎念), when one is within
118
+ * its TTL — short inner-voice copy woken by the model's own output,
119
+ * rendered by the client as a distinct whisper bubble.
120
+ */
121
+ whisper?: string;
116
122
  }
117
123
  /** Result of `pet.interact`. */
118
124
  export type PetInteractResult = LedgerInteractionResult;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,EAAgC,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EASL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;AAEnB,4BAA4B;AAC5B,MAAM,WAAW,SAAS;IACxB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAC/B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,oEAAoE;IACpE,IAAI,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,2HAA2H;AAC3H,eAAO,MAAM,sBAAsB,QAAQ,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACxC,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;CACjC;AAED,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChC,aAAa,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,EAAE,eAAe,CAAA;IACzB,6BAA6B;IAC7B,OAAO,EAAE,gBAAgB,CAAA;IACzB,4CAA4C;IAC5C,GAAG,EAAE;QACH,mBAAmB;QACnB,EAAE,EAAE,MAAM,CAAA;QACV,iDAAiD;QACjD,WAAW,EAAE,MAAM,CAAA;QACnB,4BAA4B;QAC5B,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,MAAM,EAAE;QACN,0BAA0B;QAC1B,OAAO,EAAE,MAAM,CAAA;QACf,iBAAiB;QACjB,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,gCAAgC;AAChC,MAAM,MAAM,iBAAiB,GAAG,uBAAuB,CAAA;AAEvD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,GAAG,EAAE,UAAU,CAAA;KAChB;CACF;AAiBD;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,OAAO;IACrC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAK;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAA0B;IACjD,kFAAkF;IAClF,OAAO,CAAC,cAAc,CAAqB;IAC3C;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IAEtE,YAAY,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,EA6B/C;IAED,uEAAuE;IACvE,SAAS,IAAI,OAAO,CAEnB;IAED,uCAAuC;IACjC,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,CAEnC;IAED,yDAAyD;IACzD,OAAO,IAAI,gBAAgB,CAE1B;IAED,2EAA2E;IACrE,IAAI,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAErC;IAED,gEAAgE;IAChE,gBAAgB,IAAI,WAAW,CAE9B;IAED,yCAAyC;IACzC,WAAW,IAAI,WAAW,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAEzD;IAED,6CAA6C;IAC7C,aAAa,IAAI,MAAM,CAEtB;IAED,yEAAyE;IACzE,OAAO,CAAC,KAAK,GAAE,MAA6B,GAAG,MAAM,CAIpD;IAED,4EAA4E;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAQjG;IAED,uEAAuE;IACvE,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,OAAO,CAAC,YAAY;IA4DpB,0EAA0E;IAC1E,OAAO,CAAC,UAAU;IAYlB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAqBrB,gCAAgC;IAC1B,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAK/D;IAED,iCAAiC;IAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAKnF;IAED,wFAAwF;IAClF,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC,CASlG;IAED,2EAA2E;IACrE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAO9F;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAiBtD;IAED,oFAAoF;IACpF,OAAO,CAAC,mBAAmB;IAe3B,qFAAqF;IACrF,OAAO,CAAC,UAAU;IAIlB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,IAAI;IAuDZ,OAAO,CAAC,KAAK;CAOd"}
@@ -16,6 +16,7 @@ import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from '
16
16
  import { PetLedger } from './ledger.js';
17
17
  import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from './persist.js';
18
18
  import { loadPetRegistry, petEntryView, petPackageRoot, } from './registry.js';
19
+ import { WHISPER_TTL_MS } from './chatter.js';
19
20
  import { defaultPetStateConfig, PetStateMachine, } from './state.js';
20
21
  /** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
21
22
  export const PET_SETTINGS_NAMESPACE = 'pet';
@@ -159,7 +160,7 @@ export class PetService extends Service {
159
160
  if (transition === undefined)
160
161
  return;
161
162
  runtime.officialEventsSeen = true;
162
- this.applyActivity(session, transition.input);
163
+ this.applyActivity(session, transition.input, transition.whisper);
163
164
  if (transition.completedTurn !== undefined) {
164
165
  this.rewardTurn(String(session.id), transition.completedTurn);
165
166
  }
@@ -206,9 +207,11 @@ export class PetService extends Service {
206
207
  * the session becomes the host-global display session (most recent
207
208
  * meaningful event wins the sprite animation).
208
209
  */
209
- applyActivity(session, input) {
210
+ applyActivity(session, input, whisper) {
210
211
  const activity = this.activityOf(session);
211
212
  activity.lastInput = input;
213
+ if (whisper !== undefined)
214
+ activity.whisper = { text: whisper, at: Date.now() };
212
215
  activity.machine.onActivityStatus(input);
213
216
  activity.machine.onSessionActive();
214
217
  // Move to the tail so map order reads most-recent-last, then trim the
@@ -339,6 +342,15 @@ export class PetService extends Service {
339
342
  phase: perSession.phase,
340
343
  });
341
344
  }
345
+ // The display session's inner whisper rides the global view while fresh;
346
+ // an expired whisper simply stops appearing (the client's 2s poll drops it).
347
+ const displayActivity = this.displaySession === undefined
348
+ ? undefined
349
+ : this.sessionActivity.get(this.displaySession);
350
+ const whisper = displayActivity?.whisper;
351
+ const freshWhisper = whisper !== undefined && Date.now() - whisper.at < WHISPER_TTL_MS
352
+ ? whisper.text
353
+ : undefined;
342
354
  // Read-only: the ledger settles on economic events only, never on a read,
343
355
  // so polling the state cannot trigger pet.json writes.
344
356
  return {
@@ -347,6 +359,7 @@ export class PetService extends Service {
347
359
  phase: snapshot.phase,
348
360
  sessionActive: snapshot.sessionActive,
349
361
  sessions,
362
+ ...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
350
363
  affinity: this.ledger.affinityView(Date.now()),
351
364
  display: { ...this.ledger.snapshot.display },
352
365
  pet: {
@@ -39,6 +39,8 @@ export interface PetStateSnapshot {
39
39
  export interface PetStateConfig {
40
40
  /** Celebration window after `done` before settling to idle, ms (default 2400). */
41
41
  celebrateMs: number;
42
+ /** Failure display window before settling to idle, ms (default 2400). */
43
+ failureMs: number;
42
44
  }
43
45
  export declare const defaultPetStateConfig: PetStateConfig;
44
46
  /**
@@ -47,7 +49,7 @@ export declare const defaultPetStateConfig: PetStateConfig;
47
49
  * - review → `review` while answer text is streaming.
48
50
  * - waiting → `waiting` (expectant pose, needs user input).
49
51
  * - done → `jumping` (celebration), then back to `idle` after the window.
50
- * - failed → `failed` until another activity event arrives.
52
+ * - failed → `failed` briefly, then back to `idle`.
51
53
  * - idle → `idle` (calm breathing loop).
52
54
  */
53
55
  export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
@@ -55,17 +57,18 @@ export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
55
57
  export declare function rowOf(animation: PetAnimation): number;
56
58
  /**
57
59
  * PetStateMachine — one instance per host process. Holds only the latest
58
- * input snapshot and the celebration timing; no storage, no side effects.
60
+ * input snapshot and terminal-state timing; no storage, no side effects.
59
61
  */
60
62
  export declare class PetStateMachine {
61
- private readonly config;
62
63
  private readonly now;
63
64
  private phase;
64
65
  private line;
65
66
  private phrase;
66
67
  private sessionActive;
67
68
  private doneAt;
68
- constructor(config?: PetStateConfig, now?: () => number);
69
+ private failedAt;
70
+ private readonly config;
71
+ constructor(config?: Partial<PetStateConfig>, now?: () => number);
69
72
  /** Consume one projected activity update. */
70
73
  onActivityStatus(input: PetStateInput): void;
71
74
  /** A session became the active one (or a fresh session started). */
@@ -1 +1 @@
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;IAElC,YACmB,MAAM,GAAE,cAAsC,EAC9C,GAAG,GAAE,MAAM,MAAiB,EAC3C;IAEJ,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAK3C;IAED,oEAAoE;IACpE,eAAe,IAAI,IAAI,CAEtB;IAED,sDAAsD;IACtD,iBAAiB,IAAI,IAAI,CAMxB;IAED,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB,CAuBzB;CACF"}
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;IACnB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAuD,CAAA;AAE3F;;;;;;;;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;IAWxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,YACE,MAAM,GAAE,OAAO,CAAC,cAAc,CAAyB,EACtC,GAAG,GAAE,MAAM,MAAiB,EAG9C;IAED,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAM3C;IAED,oEAAoE;IACpE,eAAe,IAAI,IAAI,CAEtB;IAED,sDAAsD;IACtD,iBAAiB,IAAI,IAAI,CAOxB;IAED,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB,CAqBzB;CACF"}
@@ -9,14 +9,14 @@
9
9
  * function of (input, nowMs); persistence and RPC live in the service.
10
10
  * @module @linxin666/dsh-pet/state
11
11
  */
12
- export const defaultPetStateConfig = { celebrateMs: 2400 };
12
+ export const defaultPetStateConfig = { celebrateMs: 2400, failureMs: 2400 };
13
13
  /**
14
14
  * Map one activity phase onto the animation contract.
15
15
  * - thinking → `running` and tool → `running-right` (focused work).
16
16
  * - review → `review` while answer text is streaming.
17
17
  * - waiting → `waiting` (expectant pose, needs user input).
18
18
  * - done → `jumping` (celebration), then back to `idle` after the window.
19
- * - failed → `failed` until another activity event arrives.
19
+ * - failed → `failed` briefly, then back to `idle`.
20
20
  * - idle → `idle` (calm breathing loop).
21
21
  */
22
22
  export function animationForPhase(phase) {
@@ -47,27 +47,28 @@ export function rowOf(animation) {
47
47
  }
48
48
  /**
49
49
  * PetStateMachine — one instance per host process. Holds only the latest
50
- * input snapshot and the celebration timing; no storage, no side effects.
50
+ * input snapshot and terminal-state timing; no storage, no side effects.
51
51
  */
52
52
  export class PetStateMachine {
53
- config;
54
53
  now;
55
54
  phase = 'idle';
56
55
  line;
57
56
  phrase;
58
57
  sessionActive = false;
59
58
  doneAt;
59
+ failedAt;
60
+ config;
60
61
  constructor(config = defaultPetStateConfig, now = Date.now) {
61
- this.config = config;
62
62
  this.now = now;
63
+ this.config = { ...defaultPetStateConfig, ...config };
63
64
  }
64
65
  /** Consume one projected activity update. */
65
66
  onActivityStatus(input) {
66
67
  this.phase = input.phase;
67
68
  this.line = input.line;
68
69
  this.phrase = input.phrase;
69
- if (input.phase === 'done')
70
- this.doneAt = this.now();
70
+ this.doneAt = input.phase === 'done' ? this.now() : undefined;
71
+ this.failedAt = input.phase === 'failed' ? this.now() : undefined;
71
72
  }
72
73
  /** A session became the active one (or a fresh session started). */
73
74
  onSessionActive() {
@@ -80,24 +81,23 @@ export class PetStateMachine {
80
81
  this.line = undefined;
81
82
  this.phrase = undefined;
82
83
  this.doneAt = undefined;
84
+ this.failedAt = undefined;
83
85
  }
84
86
  /** Render the current animation decision. */
85
87
  render() {
86
88
  const nowMs = this.now();
87
89
  let animation = animationForPhase(this.phase);
88
- // Celebration window: after `done`, jump for celebrateMs then settle idle.
89
- if (this.phase === 'done' && this.doneAt !== undefined) {
90
- if (nowMs - this.doneAt < this.config.celebrateMs) {
91
- animation = 'jumping';
92
- }
93
- else {
94
- animation = 'idle';
95
- }
96
- }
97
- // Settled sessions never bubble: idle (e.g. an aborted/stopped turn)
98
- // and done past its celebration window both fall silent.
99
- const settled = this.phase === 'idle'
100
- || (this.phase === 'done' && this.doneAt !== undefined && nowMs - this.doneAt >= this.config.celebrateMs);
90
+ const doneSettled = this.phase === 'done'
91
+ && this.doneAt !== undefined
92
+ && nowMs - this.doneAt >= this.config.celebrateMs;
93
+ const failedSettled = this.phase === 'failed'
94
+ && this.failedAt !== undefined
95
+ && nowMs - this.failedAt >= this.config.failureMs;
96
+ if (doneSettled || failedSettled)
97
+ animation = 'idle';
98
+ // Settled sessions never bubble: idle (e.g. an aborted/stopped turn),
99
+ // completed celebration expiry, and failed display expiry all fall silent.
100
+ const settled = this.phase === 'idle' || doneSettled || failedSettled;
101
101
  const bubble = settled ? undefined : this.phrase ?? this.line;
102
102
  return {
103
103
  animation,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
- "version": "0.1.20",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -58,14 +58,14 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@deepseek-ai/cordis": "^4.0.1",
61
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
62
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
63
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
64
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
65
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
62
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
63
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
64
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
65
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
66
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
67
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
68
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
69
69
  "@testing-library/dom": "^10.4.1",
70
70
  "@testing-library/react": "^16.3.2",
71
71
  "@types/node": "^22.20.0",
@@ -26,8 +26,9 @@ describe('applyInteraction', () => {
26
26
  const second = applyInteraction(first.affinity, 'pet', now + defaultAffinityConfig.petCooldownMs - 1)
27
27
  expect(second.accepted).toBe(false)
28
28
  expect(second.delta).toBe(0)
29
- expect(second.affinity).toBe(first.affinity) // same reference: no mutation
29
+ expect(second.affinity).not.toBe(first.affinity)
30
30
  expect(second.affinity.pets).toBe(1)
31
+ expect(second.affinity.petRejects).toBe(1)
31
32
  })
32
33
 
33
34
  it('accepts a pet again after the cooldown elapsed', () => {
@@ -44,8 +45,9 @@ describe('applyInteraction', () => {
44
45
  const second = applyInteraction(first.affinity, 'feed', now + defaultAffinityConfig.feedCooldownMs - 1)
45
46
  expect(second.accepted).toBe(false)
46
47
  expect(second.delta).toBe(0)
47
- expect(second.affinity).toBe(first.affinity)
48
+ expect(second.affinity).not.toBe(first.affinity)
48
49
  expect(second.affinity.feeds).toBe(1)
50
+ expect(second.affinity.feedRejects).toBe(1)
49
51
  })
50
52
 
51
53
  it('clamps points at AFFINITY_MAX', () => {
package/src/affinity.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * @module @linxin666/dsh-pet/affinity
9
9
  */
10
10
 
11
- import { builtinRemark } from './remarks.ts'
11
+ import { BUILTIN_REMARKS, builtinRemark } from './remarks.ts'
12
12
 
13
13
  /** One interaction the user can perform on the pet. */
14
14
  export type PetInteraction = 'pet' | 'feed'
@@ -25,6 +25,10 @@ export interface AffinityState {
25
25
  pets: number
26
26
  /** Total feed count (lifetime). */
27
27
  feeds: number
28
+ /** Total pet attempts rejected by cooldown (lifetime). */
29
+ petRejects: number
30
+ /** Total feed attempts rejected by cooldown (lifetime). */
31
+ feedRejects: number
28
32
  /** Total completed turns witnessed (lifetime). */
29
33
  turns: number
30
34
  }
@@ -75,7 +79,16 @@ export const defaultAffinityConfig: AffinityConfig = {
75
79
  }
76
80
 
77
81
  export function emptyAffinity(): AffinityState {
78
- return { points: 0, lastPetAt: 0, lastFeedAt: 0, pets: 0, feeds: 0, turns: 0 }
82
+ return {
83
+ points: 0,
84
+ lastPetAt: 0,
85
+ lastFeedAt: 0,
86
+ pets: 0,
87
+ feeds: 0,
88
+ petRejects: 0,
89
+ feedRejects: 0,
90
+ turns: 0,
91
+ }
79
92
  }
80
93
 
81
94
  /** Outcome of one interaction. */
@@ -136,6 +149,12 @@ function clamp(points: number): number {
136
149
  return Math.min(AFFINITY_MAX, Math.max(0, points))
137
150
  }
138
151
 
152
+ /** Built-in reaction selected deterministically from a persisted counter. */
153
+ function countedRemark(kind: 'pet' | 'petCooldown' | 'feed' | 'feedCooldown', count: number): string {
154
+ const pool = BUILTIN_REMARKS[kind]
155
+ return pool[Math.max(0, Math.floor(count)) % pool.length] ?? builtinRemark(kind)
156
+ }
157
+
139
158
  /**
140
159
  * Apply one interaction to a copy of the state (immutable style: returns a
141
160
  * new object; the caller replaces the persisted state). Cooldowns only
@@ -151,7 +170,13 @@ export function applyInteraction(
151
170
  const next = { ...state }
152
171
  if (kind === 'pet') {
153
172
  if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) {
154
- return { affinity: state, delta: 0, reaction: builtinRemark('petCooldown'), accepted: false }
173
+ next.petRejects += 1
174
+ return {
175
+ affinity: next,
176
+ delta: 0,
177
+ reaction: countedRemark('petCooldown', state.petRejects),
178
+ accepted: false,
179
+ }
155
180
  }
156
181
  next.lastPetAt = nowMs
157
182
  next.pets += 1
@@ -159,13 +184,19 @@ export function applyInteraction(
159
184
  return {
160
185
  affinity: next,
161
186
  delta: config.petReward,
162
- reaction: builtinRemark('pet'),
187
+ reaction: countedRemark('pet', state.pets),
163
188
  accepted: true,
164
189
  }
165
190
  }
166
191
  if (kind === 'feed') {
167
192
  if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) {
168
- return { affinity: state, delta: 0, reaction: builtinRemark('feedCooldown'), accepted: false }
193
+ next.feedRejects += 1
194
+ return {
195
+ affinity: next,
196
+ delta: 0,
197
+ reaction: countedRemark('feedCooldown', state.feedRejects),
198
+ accepted: false,
199
+ }
169
200
  }
170
201
  next.lastFeedAt = nowMs
171
202
  next.feeds += 1
@@ -173,7 +204,7 @@ export function applyInteraction(
173
204
  return {
174
205
  affinity: next,
175
206
  delta: config.feedReward,
176
- reaction: builtinRemark('feed'),
207
+ reaction: countedRemark('feed', state.feeds),
177
208
  accepted: true,
178
209
  }
179
210
  }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Chatter unit tests: the status voice (scene-stable lines, cadence rotation,
3
+ * per-tool families, argument hints) and the murmur engine (keyword moods,
4
+ * cooldown, output-volume budget). Everything is deterministic round-robin,
5
+ * so exact lines are asserted.
6
+ */
7
+ import { describe, expect, it } from 'vitest'
8
+ import {
9
+ STATUS_POOLS,
10
+ StatusVoice,
11
+ TOOL_POOLS,
12
+ toolArgHint,
13
+ toolCategory,
14
+ WHISPER_GENERIC_POOL,
15
+ WHISPER_RULES,
16
+ WhisperEngine,
17
+ } from './chatter.ts'
18
+
19
+ describe('StatusVoice', () => {
20
+ it('opens every scene with its legacy fixed line', () => {
21
+ // The first pool line is the wording the plugin has always shown.
22
+ expect(STATUS_POOLS.prepare[0]).toBe('准备开始')
23
+ expect(STATUS_POOLS.waiting[0]).toBe('等待模型响应')
24
+ expect(STATUS_POOLS.thinking[0]).toBe('正在思考')
25
+ expect(STATUS_POOLS.review[0]).toBe('整理回复中')
26
+ expect(STATUS_POOLS.toolResult[0]).toBe('处理工具结果')
27
+ expect(STATUS_POOLS.done[0]).toBe('完成啦')
28
+ expect(STATUS_POOLS.failed[0]).toBe('执行失败')
29
+ expect(STATUS_POOLS.toolFailed[0]).toBe('工具执行失败')
30
+ })
31
+
32
+ it('keeps the line stable while a scene repeats within the rotation window', () => {
33
+ const voice = new StatusVoice()
34
+ // Streamed chunks re-emit the same scene many times a second: the copy
35
+ // must not flicker per chunk.
36
+ expect(voice.scene('thinking', 1000)).toBe('正在思考')
37
+ expect(voice.scene('thinking', 2000)).toBe('正在思考')
38
+ expect(voice.scene('thinking', 4999)).toBe('正在思考')
39
+ })
40
+
41
+ it('advances the line once the scene persists past the rotation cadence', () => {
42
+ const voice = new StatusVoice()
43
+ expect(voice.scene('thinking', 1000)).toBe('正在思考')
44
+ expect(voice.scene('thinking', 5001)).toBe(STATUS_POOLS.thinking[1])
45
+ expect(voice.scene('thinking', 9002)).toBe(STATUS_POOLS.thinking[2])
46
+ })
47
+
48
+ it('rotates round-robin when a scene is revisited after another scene', () => {
49
+ const voice = new StatusVoice()
50
+ expect(voice.scene('thinking', 0)).toBe(STATUS_POOLS.thinking[0])
51
+ expect(voice.scene('review', 100)).toBe(STATUS_POOLS.review[0])
52
+ expect(voice.scene('thinking', 200)).toBe(STATUS_POOLS.thinking[1])
53
+ })
54
+
55
+ it('interpolates the tool name and argument hint into tool lines', () => {
56
+ const voice = new StatusVoice()
57
+ expect(voice.tool('bash', 'bash', 'npm test', 0)).toBe('正在使用 bash')
58
+ // Same family within the window: the pool line repeats but the CURRENT
59
+ // call's name/hint are interpolated, so copy never goes stale.
60
+ expect(voice.tool('bash', 'bash', 'pnpm build', 100)).toBe('正在使用 bash')
61
+ // Past the cadence the shell family rotates to a hint-carrying line.
62
+ const rotated = voice.tool('bash', 'bash', 'npm test', 5000)
63
+ expect(rotated).toBe(TOOL_POOLS.shell[1]!.replace('{hint}', 'npm test'))
64
+ })
65
+
66
+ it('falls back to the tool name when no argument hint is available', () => {
67
+ const voice = new StatusVoice()
68
+ const line = voice.tool('grep', 'grep', undefined, 5000)
69
+ expect(line).not.toContain('{hint}')
70
+ expect(line).not.toContain('{tool}')
71
+ })
72
+
73
+ it('interpolates the running count into the remaining-tools line', () => {
74
+ const voice = new StatusVoice()
75
+ expect(voice.toolRemaining(1, 0)).toBe('还有 1 个工具运行中')
76
+ expect(voice.toolRemaining(3, 5000)).toContain('3')
77
+ })
78
+ })
79
+
80
+ describe('toolCategory', () => {
81
+ it('maps the common tool vocabulary onto copy families', () => {
82
+ expect(toolCategory('read')).toBe('read')
83
+ expect(toolCategory('write')).toBe('write')
84
+ expect(toolCategory('edit')).toBe('edit')
85
+ expect(toolCategory('str_replace_editor')).toBe('edit')
86
+ expect(toolCategory('run_code')).toBe('shell')
87
+ expect(toolCategory('bash')).toBe('shell')
88
+ expect(toolCategory('grep')).toBe('grep')
89
+ expect(toolCategory('glob')).toBe('find')
90
+ expect(toolCategory('web_search')).toBe('webSearch')
91
+ expect(toolCategory('browserFetch')).toBe('webFetch')
92
+ expect(toolCategory('mcp__mem0__search_memories')).toBe('memory')
93
+ expect(toolCategory('subagent')).toBe('subagent')
94
+ expect(toolCategory('todo_write')).toBe('todo')
95
+ expect(toolCategory('some_future_tool')).toBe('generic')
96
+ })
97
+ })
98
+
99
+ describe('toolArgHint', () => {
100
+ it('extracts the shell command', () => {
101
+ expect(toolArgHint('bash', '{"command":"npm test"}')).toBe('npm test')
102
+ expect(toolArgHint('run_code', '{"code":"console.log(1)"}')).toBe('console.log(1)')
103
+ })
104
+
105
+ it('reduces file paths to their basename', () => {
106
+ expect(toolArgHint('read', '{"file_path":"/repo/src/client/PetSprite.tsx"}')).toBe('PetSprite.tsx')
107
+ })
108
+
109
+ it('extracts search patterns and web queries', () => {
110
+ expect(toolArgHint('grep', '{"pattern":"bubbleStack"}')).toBe('bubbleStack')
111
+ expect(toolArgHint('web_search', '{"query":"dsh 插件"}')).toBe('dsh 插件')
112
+ })
113
+
114
+ it('stays hintless for unparseable or shapeless arguments', () => {
115
+ expect(toolArgHint('bash', 'not json')).toBeUndefined()
116
+ expect(toolArgHint('bash', '[]')).toBeUndefined()
117
+ expect(toolArgHint('bash', '{"timeout":1000}')).toBeUndefined()
118
+ })
119
+
120
+ it('caps long hints so the bubble stays compact', () => {
121
+ const hint = toolArgHint('bash', JSON.stringify({ command: 'x'.repeat(60) }))
122
+ expect(hint).toBeDefined()
123
+ expect(hint!.length).toBeLessThanOrEqual(28)
124
+ expect(hint!.endsWith('...')).toBe(true)
125
+ })
126
+ })
127
+
128
+ describe('WhisperEngine', () => {
129
+ it('wakes a themed whisper from a keyword in the model output', () => {
130
+ const engine = new WhisperEngine()
131
+ expect(engine.feed('这里有个错误需要处理', 0)).toBe(WHISPER_RULES[1]!.pool[0])
132
+ })
133
+
134
+ it('suppresses whispers during the cooldown, then rotates the pool', () => {
135
+ const engine = new WhisperEngine()
136
+ expect(engine.feed('出现一个错误', 0)).toBe(WHISPER_RULES[1]!.pool[0])
137
+ expect(engine.feed('又一个错误', 1000)).toBeUndefined()
138
+ expect(engine.feed('还有一个错误', 9000)).toBe(WHISPER_RULES[1]!.pool[1])
139
+ })
140
+
141
+ it('earns an ambient whisper from output volume alone', () => {
142
+ const engine = new WhisperEngine(0, 10)
143
+ expect(engine.feed('aaaaaaaa', 0)).toBeUndefined()
144
+ expect(engine.feed('bbbb', 1)).toBe(WHISPER_GENERIC_POOL[0])
145
+ // The counter resets after speaking: another budget must accumulate.
146
+ expect(engine.feed('cccc', 2)).toBeUndefined()
147
+ expect(engine.feed('dddddddddd', 3)).toBe(WHISPER_GENERIC_POOL[1])
148
+ })
149
+
150
+ it('ignores empty chunks', () => {
151
+ const engine = new WhisperEngine(0, 0)
152
+ expect(engine.feed('', 0)).toBeUndefined()
153
+ })
154
+ })