@goodandready/dsh-voice 0.9.25 → 0.9.26

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/README.md CHANGED
@@ -223,6 +223,19 @@ Registers `transcribe_audio(file_path, language?)` in `ctx.tools`, allowing agen
223
223
  * `GET /dsh-voice/config` — Returns live plugin configuration snapshot.
224
224
  * `PUT /dsh-voice/config` — Updates and persists plugin configuration across network.
225
225
  * `GET/POST /dsh-voice/sensevoice-installer` — SenseVoice 1-click model installation status and trigger (protected by `isTrustedCaller`, model paths redacted).
226
+ * `POST /api/dsh-voice/update` — Triggers plugin self-update to latest compatible npm version (protected by local caller verification and profile lock check).
227
+ * `GET /api/dsh-voice/update` — Returns update check status (current version, latest version, updateAvailable).
228
+
229
+ ### 🔒 Profile Lock & Operator Recovery Policy
230
+ When updating or installing plugins, DeepSeek Harness profiles coordinate concurrent operations using `<profile-dir>/package.json.lock`.
231
+ * **Canonical Contender Policy**: The contender process never deletes or unlinks an existing lockfile. Automatic stale lock removal by contenders is prohibited to eliminate TOCTOU (Time-of-Check to Time-of-Use) race conditions and prevent unintended lockfile clobbering.
232
+ * **Diagnostics**: If `<profile-dir>/package.json.lock` is present, update attempts return `409 Conflict` with clear diagnostics:
233
+ - If held by an active process, the PID is reported.
234
+ - If filesystem inspection encounters access errors (`EACCES`, `EIO`), the operation fails closed with the error code preserved.
235
+ * **Operator Recovery**: If a prior installation process was forcefully killed or terminated unexpectedly (e.g. system OOM or power outage), manual operator recovery is required:
236
+ ```bash
237
+ rm /home/vadim/.dsh/profiles/web/package.json.lock
238
+ ```
226
239
 
227
240
  ---
228
241
 
package/README.ru.md CHANGED
@@ -220,6 +220,19 @@ dsh plugin --profile web add @goodandready/dsh-voice
220
220
  * `POST /dsh-voice/transcribe` — `{ dataBase64, mimeType, mode }` → `{ ok, text, provider, tookMs }`
221
221
  * `POST /dsh-voice/polish` — `{ text }` → `{ ok, text }`
222
222
  * `GET /dsh-voice/status` — состояние демонов, цепочки, конфигурация SenseVoice и реалтайма.
223
+ * `GET/PUT /dsh-voice/config` — получение и сохранение конфигурации плагина.
224
+ * `GET/POST /dsh-voice/sensevoice-installer` — статус и установка модели SenseVoice в 1 клик.
225
+ * `POST /api/dsh-voice/update` — запуск обновления плагина до актуальной версии из npm (с проверкой прав и блокировки профиля).
226
+ * `GET /api/dsh-voice/update` — проверка наличия обновлений.
227
+
228
+ ### 🔒 Политика блокировок профиля и ручное восстановление оператором
229
+ При установке и обновлении плагинов DeepSeek Harness синхронизирует параллельные процессы через `<profile-dir>/package.json.lock`.
230
+ * **Канонический принцип претендента**: Процесс-претендент никогда не удаляет существующий lock-файл. Автоматическое удаление чужих блокировок исключено для предотвращения TOCTOU-гонок.
231
+ * **Диагностика**: При наличии lock-файла запрос на обновление возвращает `409 Conflict` с подробной информацией (PID процесса либо код ошибки доступа к файловой системе `EACCES`/`EIO`).
232
+ * **Восстановление оператором**: Если предыдущий процесс установки аварийно завершился (например, OOM или отключение питания) и оставил зависший lock-файл, снятие блокировки выполняет оператор вручную:
233
+ ```bash
234
+ rm /home/vadim/.dsh/profiles/web/package.json.lock
235
+ ```
223
236
 
224
237
  ---
225
238
 
package/README.zh.md CHANGED
@@ -142,6 +142,17 @@ v0.8.18 质量审查后的修复批次(Gitea #79–#87,PR #88):
142
142
  * `GET /dsh-voice/config` — 获取插件运行时配置快照
143
143
  * `PUT /dsh-voice/config` — 跨网络安全持久化更新插件配置
144
144
  * `GET/POST /dsh-voice/sensevoice-installer` — SenseVoice 一键模型安装状态与触发接口(受 `isTrustedCaller` 保护,绝对路径脱敏)
145
+ * `POST /api/dsh-voice/update` — 触发插件自动更新至 npm 最新兼容版本(受本地调用与锁校验保护)
146
+ * `GET /api/dsh-voice/update` — 获取更新检查状态与版本信息
147
+
148
+ ### 🔒 配置文件锁与操作员恢复机制
149
+ 在安装或更新插件时,DeepSeek Harness 使用 `<profile-dir>/package.json.lock` 协调并发操作。
150
+ * **规范竞争者策略**: 竞争者进程绝不删除现有的锁定文件。禁止竞争者自动清理残留锁,以杜绝 TOCTOU 竞态条件。
151
+ * **诊断信息**: 若存在 lock 文件,更新请求将返回 `409 Conflict` 并附带诊断说明(关联 PID 或文件系统访问错误码 `EACCES`/`EIO`)。
152
+ * **操作员手动恢复**: 若先前安装进程异常终止(如 OOM 或系统断电)留下僵死锁,需由操作员手动清理:
153
+ ```bash
154
+ rm /home/vadim/.dsh/profiles/web/package.json.lock
155
+ ```
145
156
 
146
157
  ---
147
158
 
package/lib/client.js CHANGED
@@ -219,6 +219,9 @@ window.__ModuleLoader__.load({
219
219
  'updateSuccess': 'Updated successfully! Restart DSH to apply.',
220
220
  'updateFailed': 'Update failed. Check logs.',
221
221
  'updateCheckFailed': 'Update check failed',
222
+ 'profileLockedPid': 'Profile is locked by process (PID {pid}). Operator recovery is required if stale: remove package.json.lock.',
223
+ 'profileLockedExisting': 'Profile is locked by an existing lockfile. Operator recovery is required if stale: remove package.json.lock.',
224
+ 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection or operator recovery required.',
222
225
  'checkingUpdates': 'Checking updates…',
223
226
  'updateStatusUnknown': 'Version status unknown',
224
227
  'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
@@ -434,6 +437,9 @@ window.__ModuleLoader__.load({
434
437
  'updateSuccess': '更新成功!请重启 DSH 生效。',
435
438
  'updateFailed': '更新失败,请查看日志。',
436
439
  'updateCheckFailed': '检查更新失败',
440
+ 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请由操作员手动处理:删除 package.json.lock。',
441
+ 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请由操作员手动处理:删除 package.json.lock。',
442
+ 'profileLockCheckError': '检查配置文件锁失败。需要排查文件系统或由操作员恢复。',
437
443
  'checkingUpdates': '正在检查更新…',
438
444
  'updateStatusUnknown': '版本状态未知',
439
445
  'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
@@ -3084,6 +3090,17 @@ voice._chainsPromise = null
3084
3090
  })
3085
3091
  const data = await res.json().catch(() => ({}))
3086
3092
  if (!res.ok || data.ok === false) {
3093
+ if (data.code) {
3094
+ const base = t('profileLockCheckError')
3095
+ throw new Error(base ? `${base} (${data.code})` : (data.error || `HTTP ${res.status}`))
3096
+ }
3097
+ if (data.locked) {
3098
+ const key = data.pid ? 'profileLockedPid' : 'profileLockedExisting'
3099
+ const translated = t(key)
3100
+ if (translated && translated !== key) {
3101
+ throw new Error(data.pid ? translated.replace('{pid}', String(data.pid)) : translated)
3102
+ }
3103
+ }
3087
3104
  throw new Error(data.error || `HTTP ${res.status}`)
3088
3105
  }
3089
3106
  setUpdaterMsg(data.message || t('updateSuccess'))
package/lib/updater.js CHANGED
@@ -208,11 +208,24 @@ export function checkProfileLock(profileDir) {
208
208
  return { locked: true, pid };
209
209
  }
210
210
 
211
+ export function formatLockDiagnostic(lockStatus) {
212
+ if (!lockStatus?.locked) return null;
213
+ if (lockStatus.error) {
214
+ return `Profile lock check failed with error ${lockStatus.error}. Operator recovery or filesystem inspection required.`;
215
+ }
216
+ if (lockStatus.pid) {
217
+ return `Profile is locked by process (PID ${lockStatus.pid}). If previous installation crashed, operator recovery is required: remove package.json.lock manually.`;
218
+ }
219
+ return 'Profile is locked by an existing lockfile. If previous installation crashed, operator recovery is required: remove package.json.lock manually.';
220
+ }
221
+
211
222
  async function installExact(target, packageSpec, options) {
212
223
  if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
213
224
  const lockCheck = checkProfileLock(target.profileDir);
214
225
  if (lockCheck.locked) {
215
- throw new Error(`Another package installation is currently in progress${lockCheck.pid ? ` (PID ${lockCheck.pid})` : ''}.`);
226
+ const err = new Error(formatLockDiagnostic(lockCheck));
227
+ if (lockCheck.error) err.code = lockCheck.error;
228
+ throw err;
216
229
  }
217
230
  await new Promise((resolvePromise, reject) => {
218
231
  const child = spawn(process.execPath, [
@@ -295,8 +308,11 @@ export function registerPluginUpdater(ctx, options) {
295
308
  if (installing || lockStatus.locked) {
296
309
  json(response, 409, {
297
310
  error: lockStatus.locked
298
- ? `Another package installation is currently in progress${lockStatus.pid ? ` (PID ${lockStatus.pid})` : ''}.`
311
+ ? formatLockDiagnostic(lockStatus)
299
312
  : 'This plugin is already updating.',
313
+ code: lockStatus.error,
314
+ locked: Boolean(lockStatus.locked),
315
+ pid: lockStatus.pid ?? null,
300
316
  });
301
317
  return;
302
318
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
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",