@goodandready/dsh-voice 0.9.25 → 0.9.27

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,24 @@ 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 & Controlled Operator Recovery Policy
230
+ When updating or installing plugins, DeepSeek Harness profiles coordinate concurrent operations using `<profile-dir>/package.json.lock` (for example, `~/.dsh/profiles/<profile>/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. Filesystem errors require inspecting filesystem health, permissions, or mount options — **do not** remove the lockfile in response to `EACCES`/`EIO`.
235
+ * **Controlled Operator Recovery Workflow**:
236
+ If a previous installation was forcefully interrupted (e.g. system crash, OOM kill) leaving a stale lockfile, recovery is strictly an operator action and must follow this controlled procedure:
237
+ 1. **Establish Exclusive Maintenance**: Ensure no concurrent processes, CLI commands (`dsh plugin add`), web UI operations, or automated updaters are running or scheduled to start for the target profile.
238
+ 2. **Verify Quiescence and Ownership**: Check for active processes (`pgrep`, `ps`, `fuser`, or `lsof`) to confirm that no live process is currently performing package operations in the target profile directory. Lock age alone or observing an unparsed PID is **not** sufficient justification to delete a lock while another installer could start. If quiescence or non-ownership cannot be verified, do not remove the lock.
239
+ 3. **Remove Confirmed Orphan Lock**: Only after confirming the lock is an orphan and exclusive maintenance is established, remove the specific profile lock without recursive or force flags:
240
+ ```bash
241
+ rm ~/.dsh/profiles/<profile>/package.json.lock
242
+ ```
243
+ 4. **Resume Operations**: Re-enable installations and retry the plugin update.
226
244
 
227
245
  ---
228
246
 
package/README.ru.md CHANGED
@@ -220,6 +220,26 @@ 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` (например, `~/.dsh/profiles/<profile>/package.json.lock`).
230
+ * **Канонический принцип претендента**: Процесс-претендент никогда не удаляет чужой lock-файл. Автоматическое удаление чужих блокировок претендентом запрещено для исключения TOCTOU-гонок и повреждения состояния профиля.
231
+ * **Диагностика**: При наличии lock-файла запрос на обновление возвращает `409 Conflict` с подробной информацией:
232
+ - Если блокировка удерживается активным процессом, выводится его PID.
233
+ - При ошибках файловой системы (`EACCES`, `EIO`) операция завершается безопасно (fail-closed) с сохранением кода ошибки. Ошибки ФС требуют проверки прав доступа, состояния диска или опций монтирования — **не удаляйте** lock-файл при ошибках `EACCES`/`EIO`.
234
+ * **Регламент контролируемого восстановления оператором**:
235
+ Если предыдущий процесс установки аварийно оборвался (например, OOM или сбой питания) и оставил зависший lock-файл, снятие блокировки выполняет оператор по следующему регламенту:
236
+ 1. **Обеспечение эксклюзивного окна обслуживания**: Убедитесь, что никакие параллельные процессы, команды CLI (`dsh plugin add`), веб-интерфейс или фоновые апдейтеры не выполняют установку и не могут запуститься для данного профиля.
237
+ 2. **Проверка отсутствия живого владельца (quiescence)**: Проверьте с помощью `pgrep`, `ps`, `fuser` или `lsof`, что в каталоге профиля действительно нет активных процессов-установщиков. Сам по себе возраст файла или нераспарсенный PID **не являются** достаточным основанием для удаления lock-файла, пока не исключён параллельный запуск другого процесса. Если состояние покоя не подтверждено, удаление запрещено.
238
+ 3. **Удаление только подтверждённого осиротевшего lock-файла**: Только после подтверждения статуса сироты удалите конкретный файл блокировки без рекурсивных и force-флагов:
239
+ ```bash
240
+ rm ~/.dsh/profiles/<profile>/package.json.lock
241
+ ```
242
+ 4. **Возобновление работы**: Разрешите запуск процессов установки и повторите обновление плагина.
223
243
 
224
244
  ---
225
245
 
package/README.zh.md CHANGED
@@ -142,6 +142,24 @@ 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`(例如 `~/.dsh/profiles/<profile>/package.json.lock`)协调并发操作。
150
+ * **规范竞争者策略**: 竞争者进程绝不删除现有的锁定文件。禁止竞争者自动清理残留锁,以杜绝 TOCTOU 竞态条件并避免破坏配置文件状态。
151
+ * **诊断信息**: 若存在 lock 文件,更新请求将返回 `409 Conflict` 并附带诊断说明:
152
+ - 若由活跃进程持有,则报告其 PID。
153
+ - 若遭遇文件系统访问错误(`EACCES`、`EIO`),则安全失败(fail-closed)并保留错误码。文件系统错误需要检查磁盘健康度、目录权限或挂载选项——**请勿**因 `EACCES`/`EIO` 错误而删除锁文件。
154
+ * **受控操作员恢复流程**:
155
+ 若先前安装进程异常终止(如 OOM 终止或系统断电)留下僵死锁,必须由操作员按以下受控流程进行清理:
156
+ 1. **建立独占维护窗口**: 确保目标配置文件当前没有正在运行或计划启动的并发安装任务(CLI 命令 `dsh plugin add`、Web UI 交互或自动更新器)。
157
+ 2. **确认静止状态与无活跃属主**: 通过 `pgrep`、`ps`、`fuser` 或 `lsof` 确认目标配置文件目录下确实没有正在执行包操作的活跃进程。仅凭锁文件存在时间较长或 PID 未能解析**不足以**作为删除依据,前提必须是彻底排除其他进程启动的可能。若无法确认静止状态,切勿删除。
158
+ 3. **安全删除已确认的孤立锁**: 只有在完全确认锁文件已孤立且处于维护窗口期时,才允许由操作员手动删除该特定锁文件(严禁使用递归或 force 标志):
159
+ ```bash
160
+ rm ~/.dsh/profiles/<profile>/package.json.lock
161
+ ```
162
+ 4. **恢复正常运行**: 重新开启安装通道并重试插件更新。
145
163
 
146
164
  ---
147
165
 
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}). Follow documented controlled operator recovery procedure if stale.',
223
+ 'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
224
+ 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
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})。若残留请按受控操作员恢复流程处理。',
441
+ 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
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}. Filesystem inspection required (do not remove lockfile).`;
215
+ }
216
+ if (lockStatus.pid) {
217
+ return `Profile is locked by process (PID ${lockStatus.pid}). Follow documented controlled operator recovery procedure if the process terminated abnormally.`;
218
+ }
219
+ return 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if the process terminated abnormally.';
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.27",
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",