diff --git a/CHANGELOG.md b/CHANGELOG.md index 53faf16..5d6ec67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). Versioning foll ## [Unreleased] +## [0.15.3] - 2026-10-08 + +### Added — `connection.reconnectWindowSec` (15–30 s) for `projects voice-advanced`. + ## [0.15.2] - 2026-10-07 ### Added diff --git a/README.md b/README.md index d102dda..0a519ba 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,14 @@ voicethere projects session-settings set error_message "Sorry, something went wr Boolean values for `set`: `true` / `false` / `1` / `0` / `yes` / `no`. +## Reconnect window + +`connection.reconnectWindowSec` sets how long a dropped voice conversation is held for the client to reconnect: a whole number of seconds from 15 to 30 (default 15). A recovered conversation is billed including the interruption. An unrecovered one stops billing at the drop but holds an agent runner slot for the window. It applies on the next **`voicethere deploy --wait`**. + +```bash +voicethere projects voice-advanced set connection.reconnectWindowSec 30 +``` + ## Language switch settings Spoken-language auto-switch and its wait behavior are advanced voice settings. They apply on the next **`voicethere deploy --wait`**. When the caller switches to a language whose models are not running yet, the runner starts them on demand and can play a short wait message in the language being left. diff --git a/package-lock.json b/package-lock.json index 6021b4e..08e29b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@voicethere/cli", - "version": "0.15.2", + "version": "0.15.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@voicethere/cli", - "version": "0.15.2", + "version": "0.15.3", "license": "MIT", "dependencies": { "@node-webrtc-rust/voice-catalog": "0.9.16", diff --git a/package.json b/package.json index b1aab46..cc2f8d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@voicethere/cli", - "version": "0.15.2", + "version": "0.15.3", "description": "VoiceThere cloud CLI — login, projects, and agent bundle upload", "type": "module", "bin": { diff --git a/src/commands/projects/voice-advanced/commands.test.ts b/src/commands/projects/voice-advanced/commands.test.ts index 0a42cf0..0035d4d 100644 --- a/src/commands/projects/voice-advanced/commands.test.ts +++ b/src/commands/projects/voice-advanced/commands.test.ts @@ -56,7 +56,10 @@ describe("projects voice-advanced commands", () => { ); expect(VOICE_ADVANCED_SETTING_KEYS).toContain("voice.profilesByLanguage"); expect(VOICE_ADVANCED_SETTING_KEYS).toContain("tts.phraseCache"); - expect(VOICE_ADVANCED_SETTING_KEYS).toHaveLength(40); + expect(VOICE_ADVANCED_SETTING_KEYS).toContain( + "connection.reconnectWindowSec", + ); + expect(VOICE_ADVANCED_SETTING_KEYS).toHaveLength(41); expect(VOICE_ADVANCED_SETTING_KEYS).not.toContain( "voice.expectedLanguages", ); @@ -187,6 +190,74 @@ describe("projects voice-advanced commands", () => { ).rejects.toThrow(/Invalid boolean/); }); + describe("connection.reconnectWindowSec", () => { + it("is a number 15-30 that defaults to 15", () => { + expect( + VOICE_ADVANCED_SETTING_DEFS["connection.reconnectWindowSec"], + ).toMatchObject({ type: "number", min: 15, max: 30, default: 15 }); + }); + + it("appears in the list output with default 15", async () => { + listProjectVoiceAdvancedSettings.mockResolvedValue({ + project_id: "proj-1", + settings: { connection: { reconnectWindowSec: 15 } }, + }); + + await runProjectsVoiceAdvancedList({}); + + expect(console.log).toHaveBeenCalledWith( + "connection.reconnectWindowSec=15", + ); + }); + + it("sets connection.reconnectWindowSec to 30", async () => { + setProjectVoiceAdvancedSetting.mockResolvedValue({ + project_id: "proj-1", + settings: { connection: { reconnectWindowSec: 30 } }, + }); + + await runProjectsVoiceAdvancedSet({ + name: "connection.reconnectWindowSec", + value: "30", + }); + + expect(setProjectVoiceAdvancedSetting).toHaveBeenCalledWith( + "proj-1", + "connection.reconnectWindowSec", + 30, + ); + expect(console.log).toHaveBeenCalledWith( + "connection.reconnectWindowSec=30", + ); + }); + + it.each(["14", "31", "15.5", "abc"])("rejects %s", async (value) => { + await expect( + runProjectsVoiceAdvancedSet({ + name: "connection.reconnectWindowSec", + value, + }), + ).rejects.toThrow(); + expect(setProjectVoiceAdvancedSetting).not.toHaveBeenCalled(); + }); + + it("accepts 15", async () => { + setProjectVoiceAdvancedSetting.mockResolvedValue({ + project_id: "proj-1", + settings: {}, + }); + await runProjectsVoiceAdvancedSet({ + name: "connection.reconnectWindowSec", + value: "15", + }); + expect(setProjectVoiceAdvancedSetting).toHaveBeenCalledWith( + "proj-1", + "connection.reconnectWindowSec", + 15, + ); + }); + }); + it("rejects tts.speed outside 0.2–2.0", async () => { await expect( runProjectsVoiceAdvancedSet({ name: "tts.speed", value: "0.1" }), diff --git a/src/commands/projects/voice-advanced/defs.ts b/src/commands/projects/voice-advanced/defs.ts index 33042cd..a362e6c 100644 --- a/src/commands/projects/voice-advanced/defs.ts +++ b/src/commands/projects/voice-advanced/defs.ts @@ -38,6 +38,7 @@ export const VOICE_ADVANCED_SETTING_KEYS = [ "tts.speed", "tts.postUtteranceSilenceMs", "tts.phraseCache", + "connection.reconnectWindowSec", "noiseSuppression.enabled", "languageId.enabled", "languageId.minSpeechMs", @@ -205,6 +206,14 @@ export const VOICE_ADVANCED_SETTING_DEFS: Record< description: "Runner TTS phrase cache: repeated replies are served from memory on the runner instead of being synthesised again. On by default.", }, + "connection.reconnectWindowSec": { + type: "number", + default: 15, + min: 15, + max: 30, + description: + "Reconnect window in whole seconds (15-30): how long a dropped voice conversation is held for the client to reconnect. A recovered conversation is billed including the interruption. An unrecovered one stops billing at the drop but holds an agent runner slot for the window.", + }, "noiseSuppression.enabled": { type: "boolean", default: true, @@ -482,6 +491,9 @@ export function parseVoiceAdvancedSettingValue( if (!Number.isFinite(n)) { throw new Error(`Invalid number for ${key}`); } + if (key === "connection.reconnectWindowSec" && !Number.isInteger(n)) { + throw new Error(`${key} must be an integer`); + } const min = def.min ?? 0; const max = def.max ?? Number.MAX_SAFE_INTEGER; if (n < min || n > max) { diff --git a/src/lib/api.ts b/src/lib/api.ts index 0462dcc..a9aeb6c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -346,6 +346,7 @@ export type VoiceAdvancedSettingKey = | "tts.speed" | "tts.postUtteranceSilenceMs" | "tts.phraseCache" + | "connection.reconnectWindowSec" | "events.mode" | "noiseSuppression.enabled" | "languageId.enabled"