@agfpd/voicetalk-runtime 0.4.0 → 0.4.2
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/CHANGELOG.md +19 -0
- package/docs/README.md +2 -1
- package/docs/ru/README.md +2 -1
- package/package.json +1 -1
- package/src/nativeVoice.ts +14 -6
- package/src/voiceSession.ts +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.2] - 2026-10-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `iapeer voice open` now carries `--mic-released` as the LAST argv token — the
|
|
15
|
+
hand-off token iapeer ≥0.4.151 requires while a voicetalk runtime is alive (no second
|
|
16
|
+
listener can be opened next to ours). Sent only after the capture exit is confirmed;
|
|
17
|
+
harmless on older cores.
|
|
18
|
+
- A cold peer (`status.live === false`) is woken by open itself (up to ~2 min); open
|
|
19
|
+
then gets a 150 s budget (`VOICETALK_NATIVE_OPEN_COLD_MS`) instead of 20 s.
|
|
20
|
+
|
|
21
|
+
## [0.4.1] - 2026-10-26
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `native.ready {target, state}` on start when the `iapeer voice status` preflight
|
|
26
|
+
answers and the target stays in native mode — a positive receipt, so a healthy start
|
|
27
|
+
is distinguishable from one that never ran the preflight.
|
|
28
|
+
|
|
10
29
|
## [0.4.0] - 2026-10-26
|
|
11
30
|
|
|
12
31
|
### Added
|
package/docs/README.md
CHANGED
|
@@ -60,7 +60,8 @@ live session, which iapeer opens and closes:
|
|
|
60
60
|
what an accepted activation does, never what counts as one.
|
|
61
61
|
2. voicetalk kills its capture process and waits until it has actually exited (no
|
|
62
62
|
CoreAudio client left), plays the short "heard you" earcon while nobody is
|
|
63
|
-
listening, then runs `iapeer voice open <peer> --json
|
|
63
|
+
listening, then runs `iapeer voice open <peer> --json --mic-released` (150 s budget when the
|
|
64
|
+
peer has no live session — open wakes it first).
|
|
64
65
|
3. While the peer holds the room voicetalk stays silent: no earcons, and any agent
|
|
65
66
|
answer that arrives is spoken only after the hand-off ends.
|
|
66
67
|
4. voicetalk polls `iapeer voice status <peer> --json` (1 s) and takes the microphone
|
package/docs/ru/README.md
CHANGED
|
@@ -60,7 +60,8 @@ prompt: такой контекст способен сам породить и
|
|
|
60
60
|
принятая активация, но не то, что ею считается.
|
|
61
61
|
2. voicetalk гасит процесс захвата и ждёт его фактического выхода (CoreAudio-клиента
|
|
62
62
|
нет), играет короткий earcon «услышал», пока никто не слушает, и вызывает
|
|
63
|
-
`iapeer voice open <peer> --json
|
|
63
|
+
`iapeer voice open <peer> --json --mic-released` (для пира без живой сессии бюджет
|
|
64
|
+
150 с — open сначала будит его).
|
|
64
65
|
3. Пока комната у пира, voicetalk молчит: ни earcon'ов, а пришедший ответ агента
|
|
65
66
|
озвучивается только после возврата микрофона.
|
|
66
67
|
4. voicetalk опрашивает `iapeer voice status <peer> --json` (1 с) и забирает микрофон
|
package/package.json
CHANGED
package/src/nativeVoice.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// session belongs to iapeer (the pty supervisor), so voicetalk only REQUESTS and OBSERVES
|
|
6
6
|
// over a small CLI contract agreed with the iapeer Implementer (topic codex-voice-wake):
|
|
7
7
|
//
|
|
8
|
-
// iapeer voice open <peer> --json → {"ok":true,"state":"open","session":"…","already":bool}
|
|
9
|
-
// iapeer voice status <peer> --json → {"state":"open|opening|closing|closed","session":…,"reason"
|
|
8
|
+
// iapeer voice open <peer> --json --mic-released → {"ok":true,"state":"open","session":"…","already":bool}
|
|
9
|
+
// iapeer voice status <peer> --json → {"state":"open|opening|closing|closed","session":…,"reason"?:…,"live"?:bool}
|
|
10
10
|
// iapeer voice close <peer> --json → {"ok":true,"state":"closed"}
|
|
11
11
|
//
|
|
12
12
|
// open returns only once the realtime session is confirmed started, close once it is
|
|
@@ -24,13 +24,16 @@ export type NativeVoiceReceipt = {
|
|
|
24
24
|
session?: string | null
|
|
25
25
|
already?: boolean
|
|
26
26
|
reason?: string
|
|
27
|
+
/** status only: is the peer's session alive? A cold peer is woken by open first. */
|
|
28
|
+
live?: boolean
|
|
27
29
|
error?: string
|
|
28
30
|
/** Raw stdout/stderr tail for the log when the answer did not parse. */
|
|
29
31
|
raw?: string
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export type NativeVoiceControl = {
|
|
33
|
-
|
|
35
|
+
/** Called ONLY after our capture process has provably exited. */
|
|
36
|
+
open(peer: string, opts?: { timeoutMs?: number }): Promise<NativeVoiceReceipt>
|
|
34
37
|
status(peer: string): Promise<NativeVoiceReceipt>
|
|
35
38
|
close(peer: string): Promise<NativeVoiceReceipt>
|
|
36
39
|
}
|
|
@@ -52,6 +55,7 @@ export function parseVoiceReceipt(stdout: string, stderr: string, exitCode: numb
|
|
|
52
55
|
session: typeof obj.session === 'string' ? obj.session : null,
|
|
53
56
|
already: obj.already === true,
|
|
54
57
|
reason: typeof obj.reason === 'string' ? obj.reason : undefined,
|
|
58
|
+
...(typeof obj.live === 'boolean' ? { live: obj.live } : {}),
|
|
55
59
|
error: exitCode === 0 && obj.ok !== false ? undefined : (errorText ?? `exit ${exitCode}`),
|
|
56
60
|
}
|
|
57
61
|
} catch {}
|
|
@@ -69,7 +73,7 @@ export function cliNativeVoice(opts: {
|
|
|
69
73
|
statusTimeoutMs?: number
|
|
70
74
|
closeTimeoutMs?: number
|
|
71
75
|
}): NativeVoiceControl {
|
|
72
|
-
const run = (verb: string, peer: string, timeoutMs: number) =>
|
|
76
|
+
const run = (verb: string, peer: string, timeoutMs: number, extra: string[] = []) =>
|
|
73
77
|
new Promise<NativeVoiceReceipt>(resolve => {
|
|
74
78
|
let stdout = ''
|
|
75
79
|
let stderr = ''
|
|
@@ -82,7 +86,7 @@ export function cliNativeVoice(opts: {
|
|
|
82
86
|
}
|
|
83
87
|
let child: ReturnType<typeof spawn>
|
|
84
88
|
try {
|
|
85
|
-
child = spawn(opts.bin, ['voice', verb, peer, '--json'], {
|
|
89
|
+
child = spawn(opts.bin, ['voice', verb, peer, '--json', ...extra], {
|
|
86
90
|
cwd: opts.cwd,
|
|
87
91
|
env: opts.env ?? process.env,
|
|
88
92
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -103,7 +107,11 @@ export function cliNativeVoice(opts: {
|
|
|
103
107
|
child.on('close', code => finish(parseVoiceReceipt(stdout, stderr, code)))
|
|
104
108
|
})
|
|
105
109
|
return {
|
|
106
|
-
|
|
110
|
+
// `--mic-released` is the hand-off token (iapeer ≥0.4.151): while a voicetalk runtime
|
|
111
|
+
// is alive, iapeer accepts open ONLY with it, so nobody can open a second listener
|
|
112
|
+
// next to ours. It goes LAST in argv — an older parser that does not know the flag
|
|
113
|
+
// would otherwise swallow the next token as its value.
|
|
114
|
+
open: (peer, o) => run('open', peer, o?.timeoutMs ?? opts.openTimeoutMs ?? 20_000, ['--mic-released']),
|
|
107
115
|
status: peer => run('status', peer, opts.statusTimeoutMs ?? 5_000),
|
|
108
116
|
close: peer => run('close', peer, opts.closeTimeoutMs ?? 15_000),
|
|
109
117
|
}
|
package/src/voiceSession.ts
CHANGED
|
@@ -110,6 +110,9 @@ const WAKE_CAPTURE_MAX = Math.min(100, num(process.env.VOICETALK_WAKE_CAPTURE_MA
|
|
|
110
110
|
* to iapeer; the ceiling only bounds a hand-off whose closing was never confirmed. */
|
|
111
111
|
const NATIVE_POLL_MS = num(process.env.VOICETALK_NATIVE_POLL_MS, 1_000)
|
|
112
112
|
const NATIVE_MAX_MS = num(process.env.VOICETALK_NATIVE_MAX_MS, 30 * 60_000)
|
|
113
|
+
/** open on a peer with no live session first wakes it (one short turn, up to ~2 min per
|
|
114
|
+
* iapeer); killing that open at the warm 20 s budget would only guarantee a failure. */
|
|
115
|
+
const NATIVE_OPEN_COLD_MS = num(process.env.VOICETALK_NATIVE_OPEN_COLD_MS, 150_000)
|
|
113
116
|
|
|
114
117
|
export type DeliverFn = (target: string, text: string) => Promise<{ ok: boolean; detail?: string }>
|
|
115
118
|
export type LogFn = (evt: string, fields: Record<string, unknown>) => void
|
|
@@ -653,9 +656,12 @@ export class VoiceSession {
|
|
|
653
656
|
await this.earcon('end')
|
|
654
657
|
if (this.stopped) return
|
|
655
658
|
this.native!.phase = 'opening'
|
|
656
|
-
|
|
659
|
+
// A cold peer (no live session) is woken by open itself: give it the long budget.
|
|
660
|
+
const pre = await control.status(target)
|
|
661
|
+
const cold = pre.live === false
|
|
662
|
+
this.opts.log('native.open-requested', { target, cold })
|
|
657
663
|
const started = Date.now()
|
|
658
|
-
const receipt = await control.open(target)
|
|
664
|
+
const receipt = await control.open(target, cold ? { timeoutMs: NATIVE_OPEN_COLD_MS } : undefined)
|
|
659
665
|
if (!receipt.ok || receipt.state !== 'open') {
|
|
660
666
|
// A failed open is NOT evidence that the peer is off the microphone: the realtime
|
|
661
667
|
// session may still start late, and a timeout or an unreadable answer says nothing
|
|
@@ -750,6 +756,10 @@ export class VoiceSession {
|
|
|
750
756
|
void this.holdUntilClosed(target)
|
|
751
757
|
return true
|
|
752
758
|
}
|
|
759
|
+
// Positive preflight receipt: the contract answered and the target is live in
|
|
760
|
+
// native mode. Without this line a healthy start is indistinguishable from one that
|
|
761
|
+
// never ran the preflight.
|
|
762
|
+
this.opts.log('native.ready', { target, state: st.state })
|
|
753
763
|
}
|
|
754
764
|
return false
|
|
755
765
|
}
|