switchroom 0.19.15 → 0.19.16
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.
- package/dist/cli/switchroom.js +1 -1
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +30 -1
- package/telegram-plugin/dist/gateway/gateway.js +693 -433
- package/telegram-plugin/dist/server.js +30 -1
- package/telegram-plugin/gateway/background-shell-liveness.ts +65 -0
- package/telegram-plugin/gateway/gateway.ts +7 -58
- package/telegram-plugin/gateway/outbound-send-path.ts +25 -23
- package/telegram-plugin/gateway/outbox-listen-markup.ts +67 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +92 -18
- package/telegram-plugin/gateway/rich-message-handler.ts +10 -4
- package/telegram-plugin/gateway/silence-poke-session-event.ts +89 -0
- package/telegram-plugin/session-tail.ts +88 -1
- package/telegram-plugin/silence-poke.ts +118 -1
- package/telegram-plugin/tests/background-shell-liveness.test.ts +72 -0
- package/telegram-plugin/tests/feed-survival.test.ts +7 -1
- package/telegram-plugin/tests/fixtures/bg-shell-liveness-3519.jsonl +3 -0
- package/telegram-plugin/tests/forwarded-rich-message-coalesce.test.ts +290 -0
- package/telegram-plugin/tests/outbox-sweep-listen-button.test.ts +253 -0
- package/telegram-plugin/tests/session-tail.test.ts +91 -1
- package/telegram-plugin/tests/silence-poke.test.ts +280 -0
- package/telegram-plugin/tests/tts-normalize.test.ts +66 -0
- package/telegram-plugin/tests/voice-normalize-text.test.ts +82 -1
- package/telegram-plugin/tts-normalize.ts +12 -0
- package/telegram-plugin/voice-normalize-text.ts +100 -0
- package/telegram-plugin/voice-ondemand.ts +71 -0
|
@@ -311,3 +311,74 @@ export function mayInjectListenButton(
|
|
|
311
311
|
if (rawKeyboard == null) return true
|
|
312
312
|
return !rawKeyboard.some((row) => Array.isArray(row) && row.length > 0)
|
|
313
313
|
}
|
|
314
|
+
|
|
315
|
+
/** The voice-out plan fields the Listen-button decision depends on. A subset of
|
|
316
|
+
* the gateway's full `VoiceOutPlan` so this pure helper carries no gateway
|
|
317
|
+
* import. */
|
|
318
|
+
export interface ListenButtonVoiceOutPlan {
|
|
319
|
+
engine: 'kokoro' | 'openai'
|
|
320
|
+
voice?: string
|
|
321
|
+
speed: number
|
|
322
|
+
replyMode: 'voice+text' | 'voice-only' | 'on-demand'
|
|
323
|
+
ttsChunks: string[]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The decision to inject a 🔊 Listen button on a message, plus the cache
|
|
327
|
+
* payload the caller must persist so a tap can resynthesize. */
|
|
328
|
+
export interface ListenButtonPlan {
|
|
329
|
+
/** The single-row Listen keyboard to attach as `reply_markup`. */
|
|
330
|
+
replyMarkup: {
|
|
331
|
+
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>
|
|
332
|
+
}
|
|
333
|
+
/** The freshly minted token keying the keyboard callback + cache entry. */
|
|
334
|
+
token: string
|
|
335
|
+
/** The payload the caller stores under `token` in the VoiceOnDemandCache
|
|
336
|
+
* (and enqueues for eager pre-synth). */
|
|
337
|
+
payload: VoiceOnDemandPayload
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The SINGLE source of truth for "should this delivered message carry a 🔊
|
|
342
|
+
* Listen button, and if so which token/keyboard/cache payload?" — shared by
|
|
343
|
+
* BOTH the normal reply path (`sendReply` in outbound-send-path.ts) and the
|
|
344
|
+
* durable-outbox safety-net delivery (`outbox-sweep.ts` wiring), so a
|
|
345
|
+
* net-delivered final answer is indistinguishable from a normally-delivered
|
|
346
|
+
* one (switchroom #3502 regression: the sweep dropped the button).
|
|
347
|
+
*
|
|
348
|
+
* Returns null — no button — when any gate fails:
|
|
349
|
+
* - no voice-out plan, or the plan is not kokoro on-demand (openai on-demand
|
|
350
|
+
* synthesizes immediately and its taps would dead-end on the local sidecar);
|
|
351
|
+
* - the TTS text is empty (nothing to speak — the empty-TTS guard);
|
|
352
|
+
* - the agent supplied its own inline keyboard (single_use collision gate —
|
|
353
|
+
* see mayInjectListenButton).
|
|
354
|
+
*
|
|
355
|
+
* Pure: it mints a token and builds the keyboard/payload but performs NO side
|
|
356
|
+
* effects (no cache write, no eager enqueue). The caller owns those so the
|
|
357
|
+
* token is only persisted when it decides to actually attach the button.
|
|
358
|
+
*/
|
|
359
|
+
export function planListenButton(params: {
|
|
360
|
+
voiceOutPlan: ListenButtonVoiceOutPlan | null
|
|
361
|
+
rawKeyboard: unknown[][] | undefined | null
|
|
362
|
+
}): ListenButtonPlan | null {
|
|
363
|
+
const plan = params.voiceOutPlan
|
|
364
|
+
// on-demand Listen button is a LOCAL-engine (kokoro) feature only.
|
|
365
|
+
const useOnDemandButton =
|
|
366
|
+
plan != null && plan.replyMode === 'on-demand' && plan.engine === 'kokoro'
|
|
367
|
+
if (!useOnDemandButton) return null
|
|
368
|
+
// Empty-TTS guard: nothing to speak → no button.
|
|
369
|
+
if (!(plan.ttsChunks.length > 0 && plan.ttsChunks[0]!.length > 0)) return null
|
|
370
|
+
// Collision gate: agent supplied its own keyboard → skip.
|
|
371
|
+
if (!mayInjectListenButton(params.rawKeyboard)) return null
|
|
372
|
+
|
|
373
|
+
const token = mintVoiceOnDemandToken()
|
|
374
|
+
return {
|
|
375
|
+
replyMarkup: buildListenKeyboard(token),
|
|
376
|
+
token,
|
|
377
|
+
payload: {
|
|
378
|
+
// ttsChunks[0] is already normalizeForSpeech(reply) (kokoro path).
|
|
379
|
+
text: plan.ttsChunks[0]!,
|
|
380
|
+
...(plan.voice != null ? { voice: plan.voice } : {}),
|
|
381
|
+
speed: plan.speed,
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
}
|