@goodandready/dsh-voice 0.9.17 → 0.9.18

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.
@@ -6,6 +6,7 @@ export function createLocalDaemons({ live, ctx }) {
6
6
  let child = null
7
7
  let whisperError = null
8
8
  let startingWhisper = false
9
+ let disposed = false
9
10
 
10
11
  async function whisperAlive() {
11
12
  const rawUrl = String(live().whisperUrl || '').trim()
@@ -20,7 +21,7 @@ export function createLocalDaemons({ live, ctx }) {
20
21
  }
21
22
 
22
23
  async function startWhisper() {
23
- if (startingWhisper) return false
24
+ if (startingWhisper || disposed) return false
24
25
  startingWhisper = true
25
26
  try {
26
27
  const cfg = live()
@@ -36,15 +37,21 @@ export function createLocalDaemons({ live, ctx }) {
36
37
  }
37
38
  const command = `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
38
39
  + ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`
39
- const specOpts = { command, timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
40
+ const specOpts = { command, onExpiry: 'none', timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
40
41
  const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
41
42
 
42
43
  if (typeof ctx.shell?.execute === 'function') {
43
44
  child = await ctx.shell.execute(spec)
44
45
  } else if (typeof ctx.shell?.start === 'function') {
45
46
  child = await ctx.shell.start(spec)
46
- } else {
47
- child = null
47
+ }
48
+
49
+ if (disposed) {
50
+ if (child && typeof child.kill === 'function') {
51
+ try { child.kill() } catch { /* ignore */ }
52
+ child = null
53
+ }
54
+ return false
48
55
  }
49
56
 
50
57
  if (child && typeof child.on === 'function') {
@@ -59,7 +66,14 @@ export function createLocalDaemons({ live, ctx }) {
59
66
  }
60
67
  if (child && child.done && typeof child.done.then === 'function') {
61
68
  child.done.then((res) => {
62
- if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
69
+ if (child.status === 'killed') {
70
+ whisperError = 'whisper server process was killed'
71
+ } else if (child.status === 'completed') {
72
+ const code = child.result?.exitCode ?? (res && res.code)
73
+ if (code !== 0 && code !== null && code !== undefined) {
74
+ whisperError = `whisper server process exited with code ${code}`
75
+ }
76
+ } else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
63
77
  whisperError = `whisper server process exited with code ${res.code}`
64
78
  }
65
79
  }).catch((err) => {
@@ -69,18 +83,31 @@ export function createLocalDaemons({ live, ctx }) {
69
83
 
70
84
  // Wait for it to become healthy (up to 30s for cold model loading on Metal/CPU).
71
85
  for (let i = 0; i < 60; i++) {
86
+ if (disposed) return false
72
87
  await new Promise((r) => setTimeout(r, 500))
88
+ if (disposed) return false
73
89
  if (await whisperAlive()) {
74
90
  whisperError = null
75
91
  return true
76
92
  }
77
- if (child && child.exitCode !== null && child.exitCode !== undefined) {
78
- whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
79
- return false
80
- }
81
- if (child && (child.status === 'exited' || child.status === 'failed')) {
82
- whisperError = `whisper server process exited prematurely with status ${child.status}`
83
- return false
93
+ if (child) {
94
+ if (child.status === 'killed') {
95
+ whisperError = 'whisper server process was killed'
96
+ return false
97
+ }
98
+ if (child.status === 'completed') {
99
+ const code = child.result?.exitCode ?? child.exitCode ?? 0
100
+ whisperError = `whisper server process exited prematurely with code ${code}`
101
+ return false
102
+ }
103
+ if (child.status === 'exited' || child.status === 'failed') {
104
+ whisperError = `whisper server process exited prematurely with status ${child.status}`
105
+ return false
106
+ }
107
+ if (child.exitCode !== null && child.exitCode !== undefined) {
108
+ whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
109
+ return false
110
+ }
84
111
  }
85
112
  }
86
113
  whisperError = 'whisper server process started but failed health check on port 8001'
@@ -110,7 +137,7 @@ export function createLocalDaemons({ live, ctx }) {
110
137
  }
111
138
 
112
139
  async function startSensevoice() {
113
- if (startingSensevoice) return false
140
+ if (startingSensevoice || disposed) return false
114
141
  startingSensevoice = true
115
142
  try {
116
143
  const cfg = live()
@@ -128,15 +155,21 @@ export function createLocalDaemons({ live, ctx }) {
128
155
  try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
129
156
  const cmdArgs = buildSensevoiceArgs(cfg, port)
130
157
  const commandStr = `${JSON.stringify(cfg.sensevoiceBin)}${cmdArgs}`
131
- const specOpts = { command: commandStr, timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
158
+ const specOpts = { command: commandStr, onExpiry: 'none', timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
132
159
  const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
133
160
 
134
161
  if (typeof ctx.shell?.execute === 'function') {
135
162
  sensevoiceChild = await ctx.shell.execute(spec)
136
163
  } else if (typeof ctx.shell?.start === 'function') {
137
164
  sensevoiceChild = await ctx.shell.start(spec)
138
- } else {
139
- sensevoiceChild = null
165
+ }
166
+
167
+ if (disposed) {
168
+ if (sensevoiceChild && typeof sensevoiceChild.kill === 'function') {
169
+ try { sensevoiceChild.kill() } catch { /* ignore */ }
170
+ sensevoiceChild = null
171
+ }
172
+ return false
140
173
  }
141
174
 
142
175
  if (sensevoiceChild && typeof sensevoiceChild.on === 'function') {
@@ -151,7 +184,14 @@ export function createLocalDaemons({ live, ctx }) {
151
184
  }
152
185
  if (sensevoiceChild && sensevoiceChild.done && typeof sensevoiceChild.done.then === 'function') {
153
186
  sensevoiceChild.done.then((res) => {
154
- if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
187
+ if (sensevoiceChild.status === 'killed') {
188
+ sensevoiceError = 'sensevoice process was killed'
189
+ } else if (sensevoiceChild.status === 'completed') {
190
+ const code = sensevoiceChild.result?.exitCode ?? (res && res.code)
191
+ if (code !== 0 && code !== null && code !== undefined) {
192
+ sensevoiceError = `sensevoice process exited with code ${code}`
193
+ }
194
+ } else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
155
195
  sensevoiceError = `sensevoice process exited with code ${res.code}`
156
196
  }
157
197
  }).catch((err) => {
@@ -161,18 +201,31 @@ export function createLocalDaemons({ live, ctx }) {
161
201
 
162
202
  // Wait for it to become healthy (up to 30s).
163
203
  for (let i = 0; i < 60; i++) {
204
+ if (disposed) return false
164
205
  await new Promise((r) => setTimeout(r, 500))
206
+ if (disposed) return false
165
207
  if (await sensevoiceAlive()) {
166
208
  sensevoiceError = null
167
209
  return true
168
210
  }
169
- if (sensevoiceChild && sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
170
- sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
171
- return false
172
- }
173
- if (sensevoiceChild && (sensevoiceChild.status === 'exited' || sensevoiceChild.status === 'failed')) {
174
- sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}`
175
- return false
211
+ if (sensevoiceChild) {
212
+ if (sensevoiceChild.status === 'killed') {
213
+ sensevoiceError = 'sensevoice process was killed'
214
+ return false
215
+ }
216
+ if (sensevoiceChild.status === 'completed') {
217
+ const code = sensevoiceChild.result?.exitCode ?? sensevoiceChild.exitCode ?? 0
218
+ sensevoiceError = `sensevoice process exited prematurely with code ${code}`
219
+ return false
220
+ }
221
+ if (sensevoiceChild.status === 'exited' || sensevoiceChild.status === 'failed') {
222
+ sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}`
223
+ return false
224
+ }
225
+ if (sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
226
+ sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
227
+ return false
228
+ }
176
229
  }
177
230
  }
178
231
  sensevoiceError = 'sensevoice process started but failed health check'
@@ -195,6 +248,7 @@ export function createLocalDaemons({ live, ctx }) {
195
248
  }
196
249
 
197
250
  function dispose() {
251
+ disposed = true
198
252
  if (child && typeof child.kill === 'function') {
199
253
  try { child.kill() } catch { /* ignore */ }
200
254
  child = null
package/lib/updater.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Implements canonical DSH plugin-updater specification.
3
3
 
4
4
  import { spawn } from 'node:child_process';
5
- import { existsSync, readFileSync, unlinkSync } from 'node:fs';
5
+ import { existsSync, readFileSync, unlinkSync, renameSync } from 'node:fs';
6
6
  import { readFile } from 'node:fs/promises';
7
7
  import { homedir } from 'node:os';
8
8
  import { basename, dirname, isAbsolute, resolve } from 'node:path';
@@ -203,15 +203,23 @@ export function checkProfileLock(profileDir) {
203
203
  return { locked: true, pid };
204
204
  }
205
205
  // Verify before unlinking: ensure the file content still holds the dead pid we read,
206
- // so we don't delete a successor's live lock acquired in the interim (#205).
206
+ // using atomic rename coordinator so a successor's live lock is never deleted (#205).
207
207
  try {
208
- const freshPid = readLockPid(lockPath);
209
- if (freshPid !== pid) {
210
- return { locked: true, pid: freshPid };
208
+ const staleName = `${lockPath}.stale.${Date.now()}.${Math.random().toString(36).slice(2)}`;
209
+ renameSync(lockPath, staleName);
210
+ const renamedPid = readLockPid(staleName);
211
+ if (renamedPid === pid) {
212
+ try { unlinkSync(staleName); } catch {}
213
+ return { locked: false, cleanedStale: true };
211
214
  }
212
- unlinkSync(lockPath);
213
- return { locked: false, cleanedStale: true };
215
+ try { renameSync(staleName, lockPath); } catch {}
216
+ return { locked: true, pid: renamedPid };
214
217
  } catch {
218
+ if (!existsSync(lockPath)) return { locked: false };
219
+ const currentPid = readLockPid(lockPath);
220
+ if (currentPid !== null && isProcessAlive(currentPid)) {
221
+ return { locked: true, pid: currentPid };
222
+ }
215
223
  return { locked: false };
216
224
  }
217
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",