Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
73 changes: 72 additions & 1 deletion src/commands/projects/voice-advanced/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand Down Expand Up @@ -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" }),
Expand Down
12 changes: 12 additions & 0 deletions src/commands/projects/voice-advanced/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const VOICE_ADVANCED_SETTING_KEYS = [
"tts.speed",
"tts.postUtteranceSilenceMs",
"tts.phraseCache",
"connection.reconnectWindowSec",
"noiseSuppression.enabled",
"languageId.enabled",
"languageId.minSpeechMs",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export type VoiceAdvancedSettingKey =
| "tts.speed"
| "tts.postUtteranceSilenceMs"
| "tts.phraseCache"
| "connection.reconnectWindowSec"
| "events.mode"
| "noiseSuppression.enabled"
| "languageId.enabled"
Expand Down
Loading