mixdog 0.9.147 → 0.9.149

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.
Files changed (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { resetEmbeddingColumnsForModel } from './memory.mjs'
5
+
6
+ function mockDb({ entriesDims = 640, coreDims = 640, identityMatches = true } = {}) {
7
+ const execs = []
8
+ const queries = []
9
+ return {
10
+ execs,
11
+ queries,
12
+ async exec(sql) {
13
+ execs.push(sql)
14
+ },
15
+ async query(sql, params = []) {
16
+ queries.push({ sql, params })
17
+ if (sql.includes('FROM pg_attribute')) {
18
+ return { rows: [{ atttypmod: params[0] === 'entries' ? entriesDims : coreDims }] }
19
+ }
20
+ if (sql.includes('SELECT value = $2::jsonb')) {
21
+ return { rows: identityMatches ? [{ matches: true }] : [] }
22
+ }
23
+ return { rows: [] }
24
+ },
25
+ }
26
+ }
27
+
28
+ const identity = { model: 'onnx-community/harrier-oss-v1-270m-ONNX', dtype: 'q4' }
29
+
30
+ test('matching embedding dimensions and identity keep stored vectors', async () => {
31
+ const db = mockDb()
32
+ assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), false)
33
+ assert.deepEqual(db.execs, [])
34
+ })
35
+
36
+ test('a model identity change invalidates vectors even at the same dimensions', async () => {
37
+ const db = mockDb({ identityMatches: false })
38
+ assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), true)
39
+ assert.ok(db.execs.some((sql) => sql.includes('UPDATE entries SET embedding = NULL')))
40
+ assert.ok(db.execs.some((sql) => sql.includes('UPDATE core_entries SET embedding = NULL')))
41
+ assert.ok(db.execs.some((sql) => sql.includes('DROP TABLE IF EXISTS memory.embedding_cache')))
42
+ assert.ok(db.queries.some(({ params }) => params[0] === 'embedding.current_model'))
43
+ })
44
+
45
+ test('a dimension change rebuilds both halfvec columns', async () => {
46
+ const db = mockDb({ entriesDims: 384, coreDims: 384 })
47
+ assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), true)
48
+ assert.ok(db.execs.some((sql) => sql.includes('ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(640)')))
49
+ assert.ok(db.execs.some((sql) => sql.includes('ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(640)')))
50
+ })
@@ -46,7 +46,6 @@ export async function searchRelevantHybrid(db, query, options = {}) {
46
46
  // page still needs a broad pool so the final decision can outrank them.
47
47
  const candidateWindow = Math.max(240, limit * 8)
48
48
  const includeMembers = Boolean(options.includeMembers)
49
- const writeBackMemberHits = options.writeBackMemberHits !== false
50
49
  const rootOnly = options.rootOnly === true
51
50
  // Pre-filter knobs. Without them, FTS/vec rank the whole tree and a
52
51
  // post-filter time window can wipe the result set.
@@ -423,8 +422,11 @@ LEFT JOIN exact x ON x.id = c.id`
423
422
  if (r.exact_rank != null) exactCount++
424
423
  }
425
424
  } catch (err) {
425
+ // A failing hybrid CTE is a DB/schema fault. Returning [] rendered it to the
426
+ // caller as "no memory found", so a broken index or a migration gap looked
427
+ // like an empty store instead of an error.
426
428
  __mixdogMemoryLog(`[recall] hybrid CTE failed: ${err.message}\n`)
427
- return []
429
+ throw err
428
430
  }
429
431
 
430
432
  if (rawRows.length === 0) return []
@@ -739,27 +741,10 @@ LEFT JOIN exact x ON x.id = c.id`
739
741
  if (rootIdsForReturn.length >= limit) break
740
742
  }
741
743
 
742
- let writeBackCount = 0
743
- if (writeBackMemberHits && memberHitRootIds.size > 0) {
744
- // Batch UPDATE — single round-trip instead of N (one per member-hit root).
745
- const validRootIds = []
746
- for (const rootId of memberHitRootIds) {
747
- const r = rootIdsForReturn.find(x => x.root.id === rootId)?.root ?? byId.get(rootId)
748
- if (r) validRootIds.push(Number(rootId))
749
- }
750
- if (validRootIds.length > 0) {
751
- try {
752
- const { rowCount } = await db.query(
753
- `UPDATE entries SET last_seen_at = $1::bigint WHERE id = ANY($2::bigint[]) AND is_root = 1
754
- AND (last_seen_at IS NULL OR last_seen_at < $1::bigint - 3600000)`,
755
- [String(nowMs), validRootIds],
756
- )
757
- writeBackCount = rowCount ?? validRootIds.length
758
- } catch (err) {
759
- __mixdogMemoryLog(`[recall] writeback batch failed (count=${validRootIds.length}): ${err.message}\n`)
760
- }
761
- }
762
- }
744
+ // Recall is a read. The member-hit write-back that used to run here bumped
745
+ // last_seen_at, which feeds the stored score/freshness ranking — so merely
746
+ // searching reordered later results. Removed outright: every caller already
747
+ // passed writeBackMemberHits: false.
763
748
 
764
749
  // ── Final fetch: full row for each root by id = ANY(bigint[]) ────────────
765
750
  const topIds = rootIdsForReturn.map(x => Number(x.root.id))
@@ -929,7 +914,7 @@ LEFT JOIN exact x ON x.id = c.id`
929
914
  }
930
915
 
931
916
  __mixdogMemoryLog(
932
- `[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length} write_back=${writeBackCount}\n`,
917
+ `[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length}\n`,
933
918
  )
934
919
 
935
920
  return results
@@ -18,7 +18,7 @@ export const VALID_CATEGORY = new Set([
18
18
  'rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue',
19
19
  ])
20
20
 
21
- export async function init(db, dims) {
21
+ export async function init(db, dims, embeddingIdentity = null) {
22
22
  const dimCount = Number(dims)
23
23
  if (!Number.isInteger(dimCount) || dimCount <= 0) {
24
24
  throw new Error(`init: dims must be a positive integer, got ${dims}`)
@@ -266,6 +266,13 @@ export async function init(db, dims) {
266
266
  ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
267
267
  ['embedding.current_dims', JSON.stringify(dimCount)],
268
268
  )
269
+ if (embeddingIdentity != null) {
270
+ await db.query(
271
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
272
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
273
+ ['embedding.current_model', JSON.stringify(embeddingIdentity)],
274
+ )
275
+ }
269
276
  await db.query(
270
277
  `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
271
278
  ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
@@ -343,15 +350,24 @@ export async function refreshHotActive(db) {
343
350
  }
344
351
  }
345
352
 
346
- async function resetEmbeddingColumnsForDims(db, dimCount) {
353
+ export async function resetEmbeddingColumnsForModel(db, dimCount, embeddingIdentity = null) {
347
354
  const entriesDims = await getEmbeddingColumnDims(db, 'entries')
348
355
  const coreDims = await getEmbeddingColumnDims(db, 'core_entries')
349
- const needsEntriesReset = entriesDims != null && entriesDims !== dimCount
350
- const needsCoreReset = coreDims != null && coreDims !== dimCount
356
+ const normalizedIdentity = embeddingIdentity == null ? null : JSON.stringify(embeddingIdentity)
357
+ let identityChanged = false
358
+ if (normalizedIdentity != null) {
359
+ const identity = await db.query(
360
+ `SELECT value = $2::jsonb AS matches FROM meta WHERE key = $1`,
361
+ ['embedding.current_model', normalizedIdentity],
362
+ )
363
+ identityChanged = identity.rows.length === 0 || identity.rows[0].matches !== true
364
+ }
365
+ const needsEntriesReset = entriesDims != null && (entriesDims !== dimCount || identityChanged)
366
+ const needsCoreReset = coreDims != null && (coreDims !== dimCount || identityChanged)
351
367
  if (!needsEntriesReset && !needsCoreReset) return false
352
368
 
353
369
  __mixdogMemoryLog(
354
- `[memory] embedding dimension changed; resetting vectors for halfvec(${dimCount}) ` +
370
+ `[memory] embedding model changed; resetting vectors for halfvec(${dimCount}) ` +
355
371
  `(entries=${entriesDims ?? 'missing'}, core_entries=${coreDims ?? 'missing'})\n`,
356
372
  )
357
373
 
@@ -360,11 +376,19 @@ async function resetEmbeddingColumnsForDims(db, dimCount) {
360
376
  await db.exec(`DROP INDEX IF EXISTS core_entries_embedding_hnsw`)
361
377
 
362
378
  if (needsEntriesReset) {
363
- await db.exec(`ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
379
+ if (entriesDims !== dimCount) {
380
+ await db.exec(`ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
381
+ } else {
382
+ await db.exec(`UPDATE entries SET embedding = NULL WHERE embedding IS NOT NULL`)
383
+ }
364
384
  await db.exec(`UPDATE entries SET summary_hash = NULL WHERE summary_hash IS NOT NULL`)
365
385
  }
366
386
  if (needsCoreReset) {
367
- await db.exec(`ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
387
+ if (coreDims !== dimCount) {
388
+ await db.exec(`ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
389
+ } else {
390
+ await db.exec(`UPDATE core_entries SET embedding = NULL WHERE embedding IS NOT NULL`)
391
+ }
368
392
  }
369
393
 
370
394
  await db.exec(`DROP TABLE IF EXISTS memory.embedding_cache`)
@@ -373,6 +397,13 @@ async function resetEmbeddingColumnsForDims(db, dimCount) {
373
397
  ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
374
398
  ['embedding.current_dims', JSON.stringify(dimCount)],
375
399
  )
400
+ if (normalizedIdentity != null) {
401
+ await db.query(
402
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
403
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
404
+ ['embedding.current_model', normalizedIdentity],
405
+ )
406
+ }
376
407
  return true
377
408
  }
378
409
 
@@ -430,7 +461,7 @@ async function _migrateRecallIndexesIfStale(db) {
430
461
  }
431
462
  }
432
463
 
433
- export async function ensureCurrentSchemaExtensions(db, dims) {
464
+ export async function ensureCurrentSchemaExtensions(db, dims, embeddingIdentity = null) {
434
465
  // One-time cleanup: attachment-only placeholder rows ('(attachment)' user
435
466
  // content, e.g. Discord provider discord.mjs:724) predate the
436
467
  // shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
@@ -607,11 +638,18 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
607
638
  ['embedding.current_dims', JSON.stringify(dims)],
608
639
  )
609
640
  }
641
+ if (embeddingIdentity != null) {
642
+ await db.query(
643
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
644
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
645
+ ['embedding.current_model', JSON.stringify(embeddingIdentity)],
646
+ )
647
+ }
610
648
 
611
649
  await ensureHotActiveSearchObjects(db)
612
650
  }
613
651
 
614
- export async function openDatabase(dataDir, dims) {
652
+ export async function openDatabase(dataDir, dims, embeddingIdentity = null) {
615
653
  const key = resolve(dataDir)
616
654
 
617
655
  // Fast path — already resolved.
@@ -632,14 +670,14 @@ export async function openDatabase(dataDir, dims) {
632
670
  // race skips the redundant DDL instead of re-running init().
633
671
  await withSchemaBootstrapLock(pool, async () => {
634
672
  if (!(await isBootstrapComplete(db))) {
635
- await init(db, dims)
673
+ await init(db, dims, embeddingIdentity)
636
674
  }
637
675
  })
638
676
  }
639
677
  if (await isBootstrapComplete(db)) {
640
- await resetEmbeddingColumnsForDims(db, Number(dims))
678
+ await resetEmbeddingColumnsForModel(db, Number(dims), embeddingIdentity)
641
679
  }
642
- await ensureCurrentSchemaExtensions(db, Number(dims))
680
+ await ensureCurrentSchemaExtensions(db, Number(dims), embeddingIdentity)
643
681
  await validateEmbeddingDims(db, Number(dims))
644
682
 
645
683
  dbs.set(key, db)
@@ -14,6 +14,7 @@ import { join } from 'path'
14
14
  import { spawn, spawnSync } from 'child_process'
15
15
  import { createConnection } from 'net'
16
16
  import { createServer } from 'net'
17
+ import { isPidAlive } from '../../../shared/pid-liveness.mjs'
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // Helpers
@@ -53,16 +54,6 @@ function isTcpPortFree(port) {
53
54
  const PG_PORT_MIN = 55432
54
55
  const PG_PORT_MAX = 55632
55
56
 
56
- function isPidAlive(pid) {
57
- if (!Number.isInteger(pid) || pid <= 0) return false
58
- try {
59
- process.kill(pid, 0)
60
- return true
61
- } catch (err) {
62
- return err?.code === 'EPERM'
63
- }
64
- }
65
-
66
57
  function readPostmasterInfo(pgdataDir) {
67
58
  try {
68
59
  const lines = readFileSync(join(pgdataDir, 'postmaster.pid'), 'utf8').split('\n')
@@ -29,6 +29,7 @@ import {
29
29
  writeServiceAdvert as _writePgServiceAdvert,
30
30
  } from '../../../shared/service-discovery.mjs';
31
31
  import { withFileLockSync } from '../../../shared/atomic-file.mjs';
32
+ import { isPidAlive } from '../../../shared/pid-liveness.mjs';
32
33
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../../../shared/runtime-root.mjs';
33
34
 
34
35
  // ── pg-process interface (Track A) ───────────────────────────────────────────
@@ -350,15 +351,6 @@ function readPostmasterInfo(pgdata) {
350
351
  } catch { return { pid: null, port: null }; }
351
352
  }
352
353
 
353
- function isPidAlive(pid) {
354
- try { process.kill(pid, 0); return true; }
355
- catch (e) {
356
- // EPERM: process exists but we lack permission → alive.
357
- // ESRCH: no such process → dead.
358
- return e.code === 'EPERM';
359
- }
360
- }
361
-
362
354
  /**
363
355
  * Best-effort process-name check: confirms the pid's comm name contains 'postgres'.
364
356
  * Falls back to true (alive) when the name cannot be determined.
@@ -432,7 +432,7 @@ export function createQueryHandlers({
432
432
  // Race against the same deadline as the fan-out itself: a stuck
433
433
  // embedding worker would previously park here indefinitely because
434
434
  // the timer hadn't been started yet from the fan-out's perspective.
435
- await Promise.race([embedTexts(queries), deadlineRace])
435
+ await Promise.race([embedTexts(queries, { inputType: 'query' }), deadlineRace])
436
436
  } else if (embeddingWarmupCanStart()) {
437
437
  void warmupEmbeddingProvider().catch((err) => {
438
438
  log(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
@@ -569,7 +569,7 @@ export function createQueryHandlers({
569
569
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
570
570
  let queryVector = null
571
571
  if (isEmbeddingModelReady()) {
572
- queryVector = await embedText(retrievalQuery, { priority: true })
572
+ queryVector = await embedText(retrievalQuery, { priority: true, inputType: 'query' })
573
573
  } else if (embeddingWarmupCanStart()) {
574
574
  // Never turn a cold model into an 8-second foreground lock. Start the
575
575
  // isolated worker and return lexical results now; the next recall gets
@@ -612,7 +612,6 @@ export function createQueryHandlers({
612
612
  projectScope,
613
613
  category,
614
614
  excludeStatuses,
615
- writeBackMemberHits: false,
616
615
  latestByConcept: latestIntent && args.period == null,
617
616
  // useHotActive was set to true here so default (no-period) calls
618
617
  // routed through the mv_hot_active materialized view — a narrow
@@ -648,7 +647,6 @@ export function createQueryHandlers({
648
647
  ...searchOptions,
649
648
  includeMembers: false,
650
649
  rootOnly: true,
651
- writeBackMemberHits: false,
652
650
  })
653
651
  : Promise.resolve([]),
654
652
  ])
@@ -334,12 +334,12 @@ export function createSessionIngestRuntime({
334
334
  currentSnapshot.push({ key: snapshotKey, occurrence })
335
335
  if (i >= start) {
336
336
  considered += 1
337
- prepared.push({ m, role, content, occurrence, index: i })
337
+ prepared.push({ m, role, content, occurrence, index: i, untimestamped })
338
338
  }
339
339
  }
340
340
  ordinalState.snapshot = currentSnapshot
341
341
  ordinalState.seeded = true
342
- for (const { m, role, content, occurrence, index } of prepared) {
342
+ for (const { m, role, content, occurrence, index, untimestamped } of prepared) {
343
343
  const fallbackTs = Date.now() - (messages.length - index)
344
344
  const rawTimestamp = m.ts ?? m.timestamp
345
345
  const timeSource = untimestamped ? 'collected' : 'recorded'
@@ -286,6 +286,18 @@ export function cancelBackgroundTasks(options = {}) {
286
286
  return { cancelled };
287
287
  }
288
288
 
289
+ export function acknowledgeBackgroundTaskCompletion(taskId, options = {}) {
290
+ const task = getBackgroundTask(taskId, options);
291
+ if (!task || !TERMINAL_STATUSES.has(task.status)) return null;
292
+ // A terminal task read is an explicit consumer ACK. It wins over a racing
293
+ // async notify settlement and prevents any later reconcile from re-firing
294
+ // the same completion.
295
+ task.completionAcknowledged = true;
296
+ task.notified = true;
297
+ task.notifiedWithBody = true;
298
+ return task;
299
+ }
300
+
289
301
  function resultTextForTask(task) {
290
302
  if (task.resultText) return task.resultText;
291
303
  if (task.result !== undefined) {
@@ -327,6 +339,7 @@ export function completeBackgroundTask(taskId, {
327
339
  if (resultText != null) task.resultText = compactText(resultText);
328
340
  if (error != null) task.error = presentErrorText(error, { surface: task.surface });
329
341
  if (resultType) task.resultType = resultType;
342
+ if (instruction) task.notificationInstruction = compactText(instruction, 1_000);
330
343
  if (notify) {
331
344
  task.meta = sanitizeTaskMeta(task.meta);
332
345
  notifyTaskCompletion(task, instruction);
@@ -337,6 +350,7 @@ export function completeBackgroundTask(taskId, {
337
350
  export function notifyTaskCompletion(task, instruction) {
338
351
  if (!task) return false;
339
352
  if (!TERMINAL_STATUSES.has(task.status)) return false;
353
+ if (task.completionAcknowledged === true) return false;
340
354
  // `notified` tracks whether *any* completion notification was sent (e.g. an
341
355
  // early header-only preview from the agent surface). A later call that can
342
356
  // finally include the result body must still fire once, otherwise the owner
@@ -364,6 +378,7 @@ export function notifyTaskCompletion(task, instruction) {
364
378
  // marks, preserving exact-once.
365
379
  onSettled: (delivered) => {
366
380
  if (delivered) return;
381
+ if (task.completionAcknowledged === true) return;
367
382
  task.notified = false;
368
383
  task.notifiedWithBody = false;
369
384
  },
@@ -0,0 +1,78 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import {
8
+ cleanupBackgroundTasks,
9
+ completeBackgroundTask,
10
+ getBackgroundTask,
11
+ registerBackgroundTask,
12
+ renderBackgroundTask,
13
+ } from './background-tasks.mjs';
14
+ import { modelVisibleToolCompletionMessage } from './tool-execution-contract.mjs';
15
+ import { executeTaskTool } from '../agent/orchestrator/tools/builtin/task-tool.mjs';
16
+ import {
17
+ _dropPendingMessageState,
18
+ _settlePendingMessageWrites,
19
+ drainPendingMessages,
20
+ enqueuePendingMessage,
21
+ markCompletionEntry,
22
+ } from '../agent/orchestrator/session/manager/pending-messages.mjs';
23
+ import { _clearDeliveredCompletions } from '../agent/orchestrator/session/manager/delivered-completions.mjs';
24
+
25
+ test('terminal task read ACKs queued and racing completion notifications', async () => {
26
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-task-read-ack-'));
27
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
28
+ const sessionId = `sess_task_read_ack_${process.pid}`;
29
+ const taskId = `task_read_ack_${process.pid}`;
30
+ process.env.MIXDOG_DATA_DIR = root;
31
+ try {
32
+ const task = registerBackgroundTask({
33
+ taskId,
34
+ surface: 'shell',
35
+ operation: 'shell',
36
+ context: { callerSessionId: sessionId },
37
+ });
38
+ const instruction = `Async shell task ${taskId} (completed, exit 0) finished.`;
39
+ completeBackgroundTask(taskId, {
40
+ status: 'completed',
41
+ resultText: 'command output',
42
+ resultType: 'shell_task_result',
43
+ instruction,
44
+ notify: false,
45
+ });
46
+ const completionText = renderBackgroundTask(task, { includeResult: true });
47
+ const visible = modelVisibleToolCompletionMessage(completionText, {
48
+ type: 'shell_task_result',
49
+ execution_surface: 'shell',
50
+ execution_id: taskId,
51
+ status: 'completed',
52
+ instruction,
53
+ });
54
+ const entry = markCompletionEntry(visible, { executionId: taskId });
55
+ assert.ok(enqueuePendingMessage(sessionId, entry) > 0);
56
+ await _settlePendingMessageWrites();
57
+
58
+ assert.match(
59
+ await executeTaskTool({ action: 'read', task_id: taskId }, { sessionId }),
60
+ /status: completed/,
61
+ );
62
+ assert.equal(getBackgroundTask(taskId).completionAcknowledged, true);
63
+
64
+ // Covers both orderings: already queued before the read, and a fallback
65
+ // enqueue that races in after the read ACK.
66
+ assert.ok(enqueuePendingMessage(sessionId, entry) > 0);
67
+ assert.deepEqual(drainPendingMessages(sessionId), []);
68
+ await _settlePendingMessageWrites();
69
+ } finally {
70
+ _dropPendingMessageState(sessionId);
71
+ await _settlePendingMessageWrites().catch(() => {});
72
+ cleanupBackgroundTasks({ force: true });
73
+ _clearDeliveredCompletions();
74
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
75
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
76
+ rmSync(root, { recursive: true, force: true });
77
+ }
78
+ });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
4
  import { randomUUID } from 'node:crypto';
5
+ import { isPidAlive } from './pid-liveness.mjs';
5
6
  import { detachedSpawnOpts } from './spawn-flags.mjs';
6
7
 
7
8
  function positiveInt(value) {
@@ -169,15 +170,6 @@ function sendBrokerMessage(message) {
169
170
  }
170
171
  }
171
172
 
172
- function targetPidAlive(pid) {
173
- try {
174
- process.kill(pid, 0);
175
- return true;
176
- } catch (error) {
177
- return error?.code === 'EPERM';
178
- }
179
- }
180
-
181
173
  function stopBrokerTargetSweepIfIdle() {
182
174
  if (brokerTargets.size > 0 || !brokerTargetSweepTimer) return;
183
175
  clearInterval(brokerTargetSweepTimer);
@@ -198,7 +190,7 @@ function ensureBrokerTargetSweep() {
198
190
  if (brokerTargetSweepTimer) return;
199
191
  brokerTargetSweepTimer = setInterval(() => {
200
192
  for (const [id, target] of brokerTargets) {
201
- if (!targetPidAlive(target.childPid)) removeBrokerTarget(id);
193
+ if (!isPidAlive(target.childPid)) removeBrokerTarget(id);
202
194
  }
203
195
  stopBrokerTargetSweepIfIdle();
204
196
  }, 250);
@@ -0,0 +1,27 @@
1
+ // Canonical process-liveness probe.
2
+ //
3
+ // Discovery files, singleton claims, transports, spawn guardians and the pg
4
+ // supervisor each used to carry their own byte-identical copy of this pair.
5
+ // The rule they all agreed on: a pid is only usable when it is a positive
6
+ // integer, and `process.kill(pid, 0)` failing with EPERM means the process
7
+ // EXISTS but belongs to another user (alive); ESRCH — and any other failure —
8
+ // means it is not addressable.
9
+ //
10
+ // Callers that must fail SAFE on an unknown error (treat anything but ESRCH as
11
+ // alive) keep their own probe on purpose; this is the fail-closed variant.
12
+
13
+ export function parsePid(value) {
14
+ const pid = Number(value);
15
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
16
+ }
17
+
18
+ export function isPidAlive(value) {
19
+ const pid = parsePid(value);
20
+ if (!pid) return false;
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ } catch (error) {
25
+ return error?.code === 'EPERM';
26
+ }
27
+ }
@@ -27,7 +27,6 @@
27
27
  "MIXDOG_CACHE_MESSAGES_TTL",
28
28
  "MIXDOG_OAI_TRANSPORT",
29
29
  "MIXDOG_OPENAI_OAUTH_WS_WARMUP",
30
- "MIXDOG_OAI_CODEX_WIRE_PARITY",
31
30
  "MIXDOG_OAI_WS_DUMP_DIR",
32
31
  "MIXDOG_SESSION_TRANSCRIPT_LOG",
33
32
  "MIXDOG_USAGE_LOG"
@@ -6,6 +6,7 @@ import {
6
6
  } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join, resolve } from 'node:path';
9
+ import { isPidAlive } from './pid-liveness.mjs';
9
10
 
10
11
  let cachedDefaultRoot = '';
11
12
 
@@ -15,17 +16,6 @@ function currentUid() {
15
16
  return Number.isInteger(uid) && uid >= 0 ? uid : null;
16
17
  }
17
18
 
18
- function isPidAlive(value) {
19
- const pid = Number(value);
20
- if (!Number.isInteger(pid) || pid <= 0) return false;
21
- try {
22
- process.kill(pid, 0);
23
- return true;
24
- } catch (error) {
25
- return error?.code === 'EPERM';
26
- }
27
- }
28
-
29
19
  function safeLiveLegacyRoot(root, uid) {
30
20
  try {
31
21
  const directory = lstatSync(root);
@@ -13,6 +13,7 @@
13
13
  import { readFileSync, unlinkSync } from 'node:fs'
14
14
  import { join } from 'node:path'
15
15
  import { writeJsonAtomicSync } from './atomic-file.mjs'
16
+ import { isPidAlive } from './pid-liveness.mjs'
16
17
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from './runtime-root.mjs'
17
18
 
18
19
  function discoveryRoot() {
@@ -23,23 +24,6 @@ export function discoveryPath(service) {
23
24
  return join(discoveryRoot(), `${service}.json`)
24
25
  }
25
26
 
26
- function parsePid(value) {
27
- const pid = Number(value)
28
- return Number.isInteger(pid) && pid > 0 ? pid : null
29
- }
30
-
31
- export function isPidAlive(value) {
32
- const pid = parsePid(value)
33
- if (!pid) return false
34
- try {
35
- process.kill(pid, 0)
36
- return true
37
- } catch (e) {
38
- // EPERM: process exists but we lack permission → alive. ESRCH → dead.
39
- return e?.code === 'EPERM'
40
- }
41
- }
42
-
43
27
  // Per-process "unreachable port" distrust set: service → { port, updatedAt,
44
28
  // pid, ts } a consumer WITHOUT its own health probe proved dead this process
45
29
  // (connect-failure). pid-liveness alone cannot catch a recycled owner pid: the
@@ -1,16 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
-
4
- export function isPidAlive(pid) {
5
- const n = Number(pid);
6
- if (!Number.isInteger(n) || n <= 0) return false;
7
- try {
8
- process.kill(n, 0);
9
- return true;
10
- } catch (error) {
11
- return error?.code === 'EPERM';
12
- }
13
- }
3
+ import { isPidAlive } from './pid-liveness.mjs';
14
4
 
15
5
  function readJson(path) {
16
6
  try {
@@ -36,6 +36,8 @@ import { dirname, basename, join } from 'node:path';
36
36
  import { fileURLToPath } from 'node:url';
37
37
  import { spawn } from 'node:child_process';
38
38
  import { resolvePluginData } from './plugin-paths.mjs';
39
+ // Same probe every discovery/lock reader uses; `process.pid` is trivially alive.
40
+ import { isPidAlive as pidAlive } from './pid-liveness.mjs';
39
41
  import { detachedSpawnOpts, hiddenSpawnOpts } from './spawn-flags.mjs';
40
42
  import { renameWithRetrySync } from './atomic-file.mjs';
41
43
  import {
@@ -67,18 +69,6 @@ function rmDir(dir) {
67
69
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
68
70
  }
69
71
 
70
- function pidAlive(pid) {
71
- if (!Number.isInteger(pid) || pid <= 0) return false;
72
- if (pid === process.pid) return true;
73
- try {
74
- process.kill(pid, 0);
75
- return true;
76
- } catch (err) {
77
- // EPERM = exists but not signalable → alive; ESRCH = gone.
78
- return err?.code === 'EPERM';
79
- }
80
- }
81
-
82
72
  function sleepSync(ms) {
83
73
  const dur = Math.max(1, Number(ms) || 1);
84
74
  try {
@@ -11,7 +11,11 @@ export function normalizeToolTerminalStatus(value) {
11
11
  if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
12
12
  if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
13
13
  if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
14
- if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
14
+ // `cancel-unconfirmed` is task control's honest answer when a cancel was
15
+ // delivered but the process exit could not be confirmed. It is terminal for
16
+ // the tool call (the detail lives in the body), so it renders as cancelled
17
+ // rather than falling through to '' and leaving the surface with no status.
18
+ if (/^(cancelled|canceled|cancel|cancel[-_ ]unconfirmed|cancel[-_ ]pending|cancelling|canceling)$/.test(raw)) return 'cancelled';
15
19
  if (/^(denied|deny|refused|rejected)$/.test(raw)) return 'denied';
16
20
  return '';
17
21
  }