@pellux/goodvibes-sdk 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/contracts/artifacts/operator-contract.json +1416 -88
  2. package/dist/platform/companion/companion-chat-manager.d.ts +6 -12
  3. package/dist/platform/companion/companion-chat-manager.d.ts.map +1 -1
  4. package/dist/platform/companion/companion-chat-manager.js +8 -2
  5. package/dist/platform/config/index.d.ts +2 -1
  6. package/dist/platform/config/index.d.ts.map +1 -1
  7. package/dist/platform/config/index.js +1 -0
  8. package/dist/platform/config/manager.d.ts +35 -0
  9. package/dist/platform/config/manager.d.ts.map +1 -1
  10. package/dist/platform/config/manager.js +98 -2
  11. package/dist/platform/config/shared-config-tier.d.ts +51 -0
  12. package/dist/platform/config/shared-config-tier.d.ts.map +1 -0
  13. package/dist/platform/config/shared-config-tier.js +127 -0
  14. package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
  15. package/dist/platform/control-plane/method-catalog-runtime.js +95 -1
  16. package/dist/platform/control-plane/operator-contract-schemas-knowledge.d.ts.map +1 -1
  17. package/dist/platform/control-plane/operator-contract-schemas-knowledge.js +6 -1
  18. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts +7 -0
  19. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts.map +1 -1
  20. package/dist/platform/control-plane/operator-contract-schemas-runtime.js +40 -1
  21. package/dist/platform/daemon/facade-composition.d.ts.map +1 -1
  22. package/dist/platform/daemon/facade-composition.js +1 -0
  23. package/dist/platform/daemon/homeassistant-chat.d.ts +4 -1
  24. package/dist/platform/daemon/homeassistant-chat.d.ts.map +1 -1
  25. package/dist/platform/daemon/homeassistant-chat.js +1 -1
  26. package/dist/platform/daemon/http/homeassistant-routes.d.ts.map +1 -1
  27. package/dist/platform/daemon/http/homeassistant-routes.js +28 -2
  28. package/dist/platform/integrations/webhooks.d.ts +15 -0
  29. package/dist/platform/integrations/webhooks.d.ts.map +1 -1
  30. package/dist/platform/integrations/webhooks.js +18 -0
  31. package/dist/platform/knowledge/extraction-policy.d.ts +12 -1
  32. package/dist/platform/knowledge/extraction-policy.d.ts.map +1 -1
  33. package/dist/platform/knowledge/extraction-policy.js +19 -1
  34. package/dist/platform/knowledge/graphql-schema.d.ts +1 -1
  35. package/dist/platform/knowledge/graphql-schema.d.ts.map +1 -1
  36. package/dist/platform/knowledge/graphql-schema.js +5 -0
  37. package/dist/platform/knowledge/home-graph/index.d.ts.map +1 -1
  38. package/dist/platform/knowledge/home-graph/index.js +4 -0
  39. package/dist/platform/knowledge/home-graph/link.d.ts +2 -2
  40. package/dist/platform/knowledge/home-graph/link.d.ts.map +1 -1
  41. package/dist/platform/knowledge/home-graph/link.js +60 -9
  42. package/dist/platform/knowledge/home-graph/refinement.d.ts +14 -9
  43. package/dist/platform/knowledge/home-graph/refinement.d.ts.map +1 -1
  44. package/dist/platform/knowledge/home-graph/refinement.js +18 -3
  45. package/dist/platform/knowledge/home-graph/reset.js +21 -4
  46. package/dist/platform/knowledge/home-graph/service.d.ts +10 -11
  47. package/dist/platform/knowledge/home-graph/service.d.ts.map +1 -1
  48. package/dist/platform/knowledge/home-graph/triage.d.ts +103 -0
  49. package/dist/platform/knowledge/home-graph/triage.d.ts.map +1 -0
  50. package/dist/platform/knowledge/home-graph/triage.js +362 -0
  51. package/dist/platform/knowledge/home-graph/types.d.ts +15 -0
  52. package/dist/platform/knowledge/home-graph/types.d.ts.map +1 -1
  53. package/dist/platform/knowledge/knowledge-api.d.ts +6 -0
  54. package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
  55. package/dist/platform/knowledge/knowledge-api.js +5 -0
  56. package/dist/platform/knowledge/knowledge-history-types.d.ts +56 -0
  57. package/dist/platform/knowledge/knowledge-history-types.d.ts.map +1 -0
  58. package/dist/platform/knowledge/knowledge-history-types.js +1 -0
  59. package/dist/platform/knowledge/packet.d.ts.map +1 -1
  60. package/dist/platform/knowledge/packet.js +27 -3
  61. package/dist/platform/knowledge/scope-records.js +4 -2
  62. package/dist/platform/knowledge/semantic/answer-gaps.d.ts +8 -0
  63. package/dist/platform/knowledge/semantic/answer-gaps.d.ts.map +1 -1
  64. package/dist/platform/knowledge/semantic/answer-gaps.js +26 -6
  65. package/dist/platform/knowledge/semantic/enrichment.d.ts.map +1 -1
  66. package/dist/platform/knowledge/semantic/enrichment.js +15 -13
  67. package/dist/platform/knowledge/semantic/graph-index.d.ts.map +1 -1
  68. package/dist/platform/knowledge/semantic/graph-index.js +3 -1
  69. package/dist/platform/knowledge/semantic/service.d.ts +7 -0
  70. package/dist/platform/knowledge/semantic/service.d.ts.map +1 -1
  71. package/dist/platform/knowledge/semantic/service.js +10 -1
  72. package/dist/platform/knowledge/service-node-admin.d.ts +46 -0
  73. package/dist/platform/knowledge/service-node-admin.d.ts.map +1 -0
  74. package/dist/platform/knowledge/service-node-admin.js +72 -0
  75. package/dist/platform/knowledge/service.d.ts +24 -20
  76. package/dist/platform/knowledge/service.d.ts.map +1 -1
  77. package/dist/platform/knowledge/service.js +23 -47
  78. package/dist/platform/knowledge/store-config.d.ts +34 -0
  79. package/dist/platform/knowledge/store-config.d.ts.map +1 -1
  80. package/dist/platform/knowledge/store-config.js +23 -0
  81. package/dist/platform/knowledge/store-load.d.ts +3 -1
  82. package/dist/platform/knowledge/store-load.d.ts.map +1 -1
  83. package/dist/platform/knowledge/store-load.js +3 -1
  84. package/dist/platform/knowledge/store-node-history.d.ts +49 -0
  85. package/dist/platform/knowledge/store-node-history.d.ts.map +1 -0
  86. package/dist/platform/knowledge/store-node-history.js +331 -0
  87. package/dist/platform/knowledge/store-record-delete.d.ts +25 -0
  88. package/dist/platform/knowledge/store-record-delete.d.ts.map +1 -0
  89. package/dist/platform/knowledge/store-record-delete.js +84 -0
  90. package/dist/platform/knowledge/store-schema.d.ts +3 -1
  91. package/dist/platform/knowledge/store-schema.d.ts.map +1 -1
  92. package/dist/platform/knowledge/store-schema.js +66 -0
  93. package/dist/platform/knowledge/store.d.ts +19 -1
  94. package/dist/platform/knowledge/store.d.ts.map +1 -1
  95. package/dist/platform/knowledge/store.js +69 -108
  96. package/dist/platform/knowledge/types.d.ts +18 -0
  97. package/dist/platform/knowledge/types.d.ts.map +1 -1
  98. package/dist/platform/providers/registry.d.ts +18 -0
  99. package/dist/platform/providers/registry.d.ts.map +1 -1
  100. package/dist/platform/providers/registry.js +49 -3
  101. package/dist/platform/providers/runtime-snapshot.d.ts.map +1 -1
  102. package/dist/platform/providers/runtime-snapshot.js +14 -2
  103. package/dist/platform/runtime/memory-spine/client.d.ts +137 -12
  104. package/dist/platform/runtime/memory-spine/client.d.ts.map +1 -1
  105. package/dist/platform/runtime/memory-spine/client.js +119 -1
  106. package/dist/platform/runtime/memory-spine/index.d.ts +8 -1
  107. package/dist/platform/runtime/memory-spine/index.d.ts.map +1 -1
  108. package/dist/platform/runtime/memory-spine/index.js +7 -0
  109. package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts +74 -0
  110. package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts.map +1 -0
  111. package/dist/platform/runtime/memory-spine/recall-snapshot.js +87 -0
  112. package/dist/platform/runtime/memory-spine/wire-verb-availability.d.ts +59 -0
  113. package/dist/platform/runtime/memory-spine/wire-verb-availability.d.ts.map +1 -0
  114. package/dist/platform/runtime/memory-spine/wire-verb-availability.js +114 -0
  115. package/dist/platform/runtime/services.d.ts.map +1 -1
  116. package/dist/platform/runtime/services.js +3 -12
  117. package/dist/platform/state/memory-recall-contract.d.ts +7 -0
  118. package/dist/platform/state/memory-recall-contract.d.ts.map +1 -1
  119. package/dist/platform/state/memory-recall-contract.js +2 -0
  120. package/dist/platform/utils/notify.d.ts +25 -1
  121. package/dist/platform/utils/notify.d.ts.map +1 -1
  122. package/dist/platform/utils/notify.js +29 -1
  123. package/dist/platform/version.js +1 -1
  124. package/dist/platform/voice/index.d.ts +2 -0
  125. package/dist/platform/voice/index.d.ts.map +1 -1
  126. package/dist/platform/voice/index.js +3 -0
  127. package/dist/platform/voice/spoken-turn/audio-sink.d.ts +111 -0
  128. package/dist/platform/voice/spoken-turn/audio-sink.d.ts.map +1 -0
  129. package/dist/platform/voice/spoken-turn/audio-sink.js +1 -0
  130. package/dist/platform/voice/spoken-turn/controller.d.ts +117 -0
  131. package/dist/platform/voice/spoken-turn/controller.d.ts.map +1 -0
  132. package/dist/platform/voice/spoken-turn/controller.js +448 -0
  133. package/dist/platform/voice/spoken-turn/index.d.ts +16 -0
  134. package/dist/platform/voice/spoken-turn/index.d.ts.map +1 -0
  135. package/dist/platform/voice/spoken-turn/index.js +12 -0
  136. package/dist/platform/voice/spoken-turn/text-chunker.d.ts +33 -0
  137. package/dist/platform/voice/spoken-turn/text-chunker.d.ts.map +1 -0
  138. package/dist/platform/voice/spoken-turn/text-chunker.js +97 -0
  139. package/package.json +9 -9
@@ -139,6 +139,7 @@ export function runHonestMemorySearch(store, filter = {}, options = {}) {
139
139
  excludedFlaggedCount: 0,
140
140
  excludedBelowFloorCount: 0,
141
141
  totalBeforeRecallFilter,
142
+ recallFloor: MIN_PROMPT_MEMORY_CONFIDENCE,
142
143
  };
143
144
  }
144
145
  const kept = [];
@@ -166,5 +167,6 @@ export function runHonestMemorySearch(store, filter = {}, options = {}) {
166
167
  excludedFlaggedCount,
167
168
  excludedBelowFloorCount,
168
169
  totalBeforeRecallFilter,
170
+ recallFloor: MIN_PROMPT_MEMORY_CONFIDENCE,
169
171
  };
170
172
  }
@@ -6,8 +6,32 @@
6
6
  * @param title - Notification title
7
7
  * @param message - Notification body
8
8
  * @param durationMs - Turn duration in milliseconds
9
+ * @param options - Optional overrides (see {@link NotifyOptions}).
9
10
  */
10
11
  /** Escape a string for safe interpolation into an AppleScript string literal. */
11
12
  export declare function escapeAppleScript(s: string): string;
12
- export declare function notifyCompletion(title: string, message: string, durationMs: number): void;
13
+ export interface NotifyOptions {
14
+ /**
15
+ * Bypass test suppression for this call. Intended only for tests that
16
+ * exercise the notification shell-out layer itself (e.g. asserting the
17
+ * notify-send/osascript invocation or the terminal bell byte). Every
18
+ * other caller should leave this unset.
19
+ */
20
+ force?: boolean;
21
+ }
22
+ /**
23
+ * True when this call must be a no-op: real desktop notifications (and the
24
+ * terminal bell) must never fire from an automated test run and spam
25
+ * whoever's desktop the tests happen to execute on.
26
+ *
27
+ * Suppressed when either:
28
+ * - `NODE_ENV === 'test'` (set automatically by `bun test`), or
29
+ * - `GOODVIBES_SUPPRESS_NOTIFY` is set to a truthy value (explicit
30
+ * override for harnesses that don't run under NODE_ENV=test).
31
+ *
32
+ * `options.force: true` bypasses suppression entirely — the one sanctioned
33
+ * escape hatch, for tests that specifically exercise this shell-out layer.
34
+ */
35
+ export declare function isNotifySuppressed(force?: boolean): boolean;
36
+ export declare function notifyCompletion(title: string, message: string, durationMs: number, options?: NotifyOptions): void;
13
37
  //# sourceMappingURL=notify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/platform/utils/notify.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAKrF;;;;;;;GAOG;AACH,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AAiCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAmBzF"}
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/platform/utils/notify.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAKrF;;;;;;;;GAQG;AACH,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAM3D;AAiCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAwBlH"}
@@ -8,11 +8,35 @@ import { logger } from './logger.js';
8
8
  * @param title - Notification title
9
9
  * @param message - Notification body
10
10
  * @param durationMs - Turn duration in milliseconds
11
+ * @param options - Optional overrides (see {@link NotifyOptions}).
11
12
  */
12
13
  /** Escape a string for safe interpolation into an AppleScript string literal. */
13
14
  export function escapeAppleScript(s) {
14
15
  return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
15
16
  }
17
+ /**
18
+ * True when this call must be a no-op: real desktop notifications (and the
19
+ * terminal bell) must never fire from an automated test run and spam
20
+ * whoever's desktop the tests happen to execute on.
21
+ *
22
+ * Suppressed when either:
23
+ * - `NODE_ENV === 'test'` (set automatically by `bun test`), or
24
+ * - `GOODVIBES_SUPPRESS_NOTIFY` is set to a truthy value (explicit
25
+ * override for harnesses that don't run under NODE_ENV=test).
26
+ *
27
+ * `options.force: true` bypasses suppression entirely — the one sanctioned
28
+ * escape hatch, for tests that specifically exercise this shell-out layer.
29
+ */
30
+ export function isNotifySuppressed(force) {
31
+ if (force)
32
+ return false;
33
+ if (process.env.NODE_ENV === 'test')
34
+ return true;
35
+ const override = process.env.GOODVIBES_SUPPRESS_NOTIFY;
36
+ if (override && override !== '0' && override.toLowerCase() !== 'false')
37
+ return true;
38
+ return false;
39
+ }
16
40
  function trimNotificationOutput(value) {
17
41
  const trimmed = value.trim();
18
42
  return trimmed.length > 0 ? trimmed.slice(0, 500) : undefined;
@@ -43,7 +67,11 @@ function spawnNotification(command) {
43
67
  });
44
68
  }
45
69
  }
46
- export function notifyCompletion(title, message, durationMs) {
70
+ export function notifyCompletion(title, message, durationMs, options) {
71
+ if (isNotifySuppressed(options?.force)) {
72
+ logger.debug('Completion notification suppressed under test', { title, durationMs });
73
+ return;
74
+ }
47
75
  // Terminal bell for responses > 5s.
48
76
  // Surface-specific: this writes directly to stdout and is only meaningful
49
77
  // in a terminal context. Host surfaces that manage their own output stream
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- let version = '1.1.0';
3
+ let version = '1.3.0';
4
4
  try {
5
5
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf-8'));
6
6
  version = pkg.version ?? version;
@@ -4,4 +4,6 @@ export type { VoiceProviderDescriptor } from './provider-registry.js';
4
4
  export { VoiceService } from './service.js';
5
5
  export type { VoiceServiceStatus } from './service.js';
6
6
  export { ensureBuiltinVoiceProviders } from './builtin-providers.js';
7
+ export { SpokenTurnController, TtsTextChunker, normalizeSpeechText } from './spoken-turn/index.js';
8
+ export type { SpokenTurnControllerOptions, TtsTextChunkerOptions, AudioSink, AudioSinkPlaybackOptions, } from './spoken-turn/index.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/platform/voice/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,2BAA2B,EAC3B,qBAAqB,EACrB,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/platform/voice/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,2BAA2B,EAC3B,qBAAqB,EACrB,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAIrE,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACnG,YAAY,EACV,2BAA2B,EAC3B,qBAAqB,EACrB,SAAS,EACT,wBAAwB,GACzB,MAAM,wBAAwB,CAAC"}
@@ -1,3 +1,6 @@
1
1
  export { VoiceProviderRegistry } from './provider-registry.js';
2
2
  export { VoiceService } from './service.js';
3
3
  export { ensureBuiltinVoiceProviders } from './builtin-providers.js';
4
+ // Spoken-turn (live TTS) policy engine — shared behavioral contract; consumers
5
+ // inject an AudioSink for I/O. See ./spoken-turn.
6
+ export { SpokenTurnController, TtsTextChunker, normalizeSpeechText } from './spoken-turn/index.js';
@@ -0,0 +1,111 @@
1
+ import type { VoiceAudioChunk } from '../types.js';
2
+ /**
3
+ * AudioSink — the injectable I/O boundary the spoken-turn policy engine plays
4
+ * through. The SDK owns POLICY (chunking, the bounded synthesis window, merge
5
+ * coalescing, retry/backoff, the turn state machine); a sink owns I/O (turning
6
+ * an ordered stream of audio bytes into sound and reporting when it has stopped
7
+ * making sound). The controller never spawns a process, opens a device, or
8
+ * touches Web Audio — it only talks to this interface, so the same policy runs
9
+ * unchanged behind a terminal subprocess player, a browser Web Audio sink, or a
10
+ * test fake.
11
+ *
12
+ * ## Behavioral contract
13
+ *
14
+ * The controller depends on these guarantees, not on any implementation detail:
15
+ *
16
+ * 1. **Readiness / head survival.** `play()` must not drop the leading bytes of
17
+ * the stream while the underlying output is still coming up. A subprocess
18
+ * sink holds the first byte until the player has actually exec'd (its spawn
19
+ * event); a browser sink holds until the MediaSource `sourceopen` /
20
+ * SourceBuffer is ready. The controller writes the whole stream and trusts
21
+ * the sink to have played the head — it does not re-send or pre-buffer.
22
+ * `available` is the coarse, synchronous readiness signal (is any output
23
+ * device usable at all); per-call readiness is internal to `play()`.
24
+ *
25
+ * 2. **Natural drain plays everything.** When the input stream ends without an
26
+ * abort, `play()` resolves only after the last buffered sample has been
27
+ * heard — never at end-of-input. Truncating the tail of a response is the
28
+ * bug this contract exists to prevent.
29
+ *
30
+ * 3. **Abort cuts immediately.** When `options.signal` aborts (a deliberate
31
+ * interrupt — new turn, Ctrl+C, /tts stop, turn cancel), `play()` stops
32
+ * emitting sound and resolves promptly; it must NOT wait on a graceful
33
+ * drain. `stop()` is the same instant cut driven imperatively rather than by
34
+ * a signal.
35
+ *
36
+ * 4. **Bounded exit drain.** `waitForDrain(timeoutMs)` resolves when the sound
37
+ * currently playing finishes naturally OR after `timeoutMs`, whichever comes
38
+ * first, and resolves immediately when nothing is playing. The exit path
39
+ * uses this to let the final audio of a finished response play out inside a
40
+ * short window instead of being cut mid-word.
41
+ *
42
+ * ## Web Audio / browser sink mapping (proof the interface fits a browser)
43
+ *
44
+ * A browser sink for the webui voice build implements this same interface over
45
+ * streaming MP3 into MediaSource; no interface change is required:
46
+ *
47
+ * - `available` -> `('MediaSource' in window) && MediaSource.isTypeSupported('audio/mpeg')`
48
+ * (fall back to Web Audio `decodeAudioData` where MSE is absent).
49
+ * - `play(chunks, { format, signal })` ->
50
+ * - create a `MediaSource`, attach it to an `HTMLAudioElement` via
51
+ * `URL.createObjectURL`, and `await` its `sourceopen` event — this is the
52
+ * readiness gate (contract 1);
53
+ * - `addSourceBuffer(mimeFor(format))` where `mimeFor('mp3') === 'audio/mpeg'`
54
+ * (the same `format` string the controller forwards from the synthesis
55
+ * result), then `await audioEl.play()`;
56
+ * - `for await (const chunk of chunks)` -> `sourceBuffer.appendBuffer(chunk.data)`,
57
+ * awaiting each `updateend`, honoring `signal.aborted` between chunks;
58
+ * - on input end without abort: `mediaSource.endOfStream()` and resolve on
59
+ * the audio element's `ended` event -> natural drain (contract 2);
60
+ * - on `signal` abort: `audioEl.pause()`, `sourceBuffer.abort()`,
61
+ * `mediaSource.endOfStream()`, revoke the object URL, resolve now
62
+ * (contract 3).
63
+ * - `stop()` -> the same teardown as the abort branch, driven imperatively.
64
+ * - `waitForDrain(timeoutMs)` -> resolve on the audio element's `ended` event or
65
+ * a `setTimeout(timeoutMs)`, whichever fires first (contract 4).
66
+ *
67
+ * `VoiceAudioChunk.data` is a `Uint8Array`, which `appendBuffer` accepts
68
+ * directly, and the `format` string maps 1:1 to a MediaSource MIME type, so the
69
+ * byte-and-format shape the controller emits is exactly what a browser sink
70
+ * consumes. (The subprocess sink — mpv/ffplay over stdin — stays consumer-side
71
+ * and is not part of the SDK.)
72
+ */
73
+ export interface AudioSink {
74
+ /** Human-readable sink name for status lines (e.g. 'mpv', 'web-audio'). */
75
+ readonly label: string;
76
+ /**
77
+ * Coarse, synchronous readiness: is any output device usable at all. When
78
+ * false the controller arms nothing and reports the graceful-degradation
79
+ * notice instead of attempting playback.
80
+ */
81
+ readonly available: boolean;
82
+ /**
83
+ * Play an ordered stream of audio chunks to completion. Resolves after the
84
+ * final sample is heard on a natural end, or promptly on abort. See the
85
+ * interface contract above for readiness, drain, and abort obligations.
86
+ */
87
+ play(chunks: AsyncIterable<VoiceAudioChunk>, options: AudioSinkPlaybackOptions): Promise<void>;
88
+ /** Instant cut: stop emitting sound now and release the output. Idempotent; a no-op when idle. */
89
+ stop(): void;
90
+ /**
91
+ * Resolves once the currently playing sound has finished naturally or after
92
+ * `timeoutMs`, whichever comes first; resolves immediately when nothing is
93
+ * playing. The bounded exit drain — see contract 4.
94
+ */
95
+ waitForDrain(timeoutMs: number): Promise<void>;
96
+ }
97
+ export interface AudioSinkPlaybackOptions {
98
+ /**
99
+ * Container/codec hint for the byte stream (e.g. 'mp3'), forwarded verbatim
100
+ * from the synthesis result's `format`. A subprocess sink maps it to a
101
+ * demuxer flag; a browser sink maps it to a MediaSource MIME type. Omitted or
102
+ * a value containing '/' (already a MIME type) leaves the sink on its default.
103
+ */
104
+ readonly format?: string | undefined;
105
+ /**
106
+ * Deliberate-interrupt signal. When it aborts, playback cuts immediately (no
107
+ * graceful drain) and `play()` resolves. Absence means "play to natural end".
108
+ */
109
+ readonly signal?: AbortSignal | undefined;
110
+ }
111
+ //# sourceMappingURL=audio-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-sink.d.ts","sourceRoot":"","sources":["../../../../src/platform/voice/spoken-turn/audio-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/F,kGAAkG;IAClG,IAAI,IAAI,IAAI,CAAC;IACb;;;;OAIG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import type { ConfigManager } from '../../config/index.js';
2
+ import type { TurnEvent } from '../../../events/turn.js';
3
+ import type { VoiceService } from '../service.js';
4
+ import type { AudioSink } from './audio-sink.js';
5
+ export interface SpokenTurnControllerOptions {
6
+ readonly voiceService: Pick<VoiceService, 'synthesizeStream'>;
7
+ readonly configManager: Pick<ConfigManager, 'get'>;
8
+ /** The injectable I/O boundary. See {@link AudioSink}. */
9
+ readonly sink: AudioSink;
10
+ readonly notify?: ((message: string) => void) | undefined;
11
+ /**
12
+ * Attribution label recorded in each synthesis request's metadata.source
13
+ * (e.g. 'goodvibes-tui', 'goodvibes-agent', 'goodvibes-webui'). Defaults to
14
+ * 'goodvibes-sdk'.
15
+ */
16
+ readonly source?: string | undefined;
17
+ readonly now?: (() => number) | undefined;
18
+ readonly setInterval?: typeof setInterval | undefined;
19
+ readonly clearInterval?: typeof clearInterval | undefined;
20
+ /** Injectable clock for retry backoff timers (tests use fakes). */
21
+ readonly setTimeout?: typeof setTimeout | undefined;
22
+ readonly clearTimeout?: typeof clearTimeout | undefined;
23
+ }
24
+ /**
25
+ * SpokenTurnController — the shared spoken-output policy engine. It watches a
26
+ * turn's lifecycle events, chunks the streamed answer into speech-sized pieces,
27
+ * merges and dispatches synthesis requests through a bounded 2-slot window with
28
+ * retry/backoff, and drives an injected {@link AudioSink} with honest
29
+ * drain-vs-interrupt semantics. All I/O is the sink's; this class is pure
30
+ * policy and runs unchanged in the terminal, the agent, and the browser.
31
+ */
32
+ export declare class SpokenTurnController {
33
+ private pendingPrompt;
34
+ private activeTurnId;
35
+ private chunker;
36
+ private chunkSequence;
37
+ private playbackChain;
38
+ private readonly abortControllers;
39
+ private timer;
40
+ private errorReportedForTurn;
41
+ private noSinkNoticed;
42
+ /** Chunker output waiting to be merged into a synthesis request. */
43
+ private pendingTexts;
44
+ /** Requests currently in the pipeline (synthesizing / waiting / playing). */
45
+ private pipelineDepth;
46
+ /** Bumped on every teardown so stale pipeline releases are ignored. */
47
+ private pipelineGeneration;
48
+ private pumpScheduled;
49
+ /** Set when TURN_COMPLETED arrives; the turn releases once the pipeline drains. */
50
+ private completedTurnId;
51
+ private readonly voiceService;
52
+ private readonly configManager;
53
+ private readonly sink;
54
+ private readonly notify?;
55
+ private readonly source;
56
+ private readonly now;
57
+ private readonly setIntervalImpl;
58
+ private readonly clearIntervalImpl;
59
+ private readonly setTimeoutImpl;
60
+ private readonly clearTimeoutImpl;
61
+ constructor(options: SpokenTurnControllerOptions);
62
+ submitNextTurn(prompt: string): boolean;
63
+ /**
64
+ * Returns whether speech was actually ACTIVE when stopped. The notice only
65
+ * prints in that case — stop() on an idle controller used to notify anyway,
66
+ * spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay
67
+ * fix); callers use the return to decide whether the press "did a
68
+ * job" (see handleCtrlC's consume-on-speech-stop).
69
+ */
70
+ stop(message?: string): boolean;
71
+ /**
72
+ * Exit-path teardown: drops everything not yet audible (pending arm,
73
+ * buffered text, queued chunks) but lets the audio the user is already
74
+ * hearing finish naturally, capped at `drainTimeoutMs`, before the hard
75
+ * stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their
76
+ * instant path through stop(); this is only for exiting the app while the
77
+ * final audio of a completed response is still draining.
78
+ */
79
+ stopForExit(drainTimeoutMs?: number): Promise<void>;
80
+ handleTurnEvent(event: TurnEvent): void;
81
+ private maybeStartTurn;
82
+ private finishTurn;
83
+ private resetPipeline;
84
+ private startTimer;
85
+ private stopTimer;
86
+ /**
87
+ * Chunker output does NOT map 1:1 to synthesis requests. Text queues here
88
+ * and the pump merges everything pending into one request whenever a
89
+ * pipeline slot is free — so the request count tracks how often the model
90
+ * out-paces the audio, not how many sentences it wrote. A short answer that
91
+ * arrives before the first pump tick is exactly one request.
92
+ */
93
+ private queueTexts;
94
+ /**
95
+ * Deferred one tick so text delivered in the same synchronous burst (fast
96
+ * deltas, or a turn that completes instantly) coalesces into a single
97
+ * request instead of firing per sentence boundary.
98
+ */
99
+ private schedulePump;
100
+ private pump;
101
+ /** Merge everything pending into one request, capped at the per-request text limit. */
102
+ private takeMergedText;
103
+ private dispatchChunk;
104
+ private releasePipelineSlot;
105
+ private maybeReleaseTurn;
106
+ private synthesizeWithRetry;
107
+ /** Abortable backoff sleep — an abort clears the timer and rejects, so a stop mid-backoff leaves nothing running. */
108
+ private delay;
109
+ private synthesize;
110
+ /**
111
+ * One synthesis request failed after its retries. Report once per turn and
112
+ * keep going — the rest of the response still plays.
113
+ */
114
+ private reportSkippedChunk;
115
+ private reportError;
116
+ }
117
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/platform/voice/spoken-turn/controller.ts"],"names":[],"mappings":"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"}