mixdog 1.1.2 → 1.2.0

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 (288) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +6 -0
  7. package/src/defaults/skills/setup/references/actions.md +5 -2
  8. package/src/defaults/skills/visualize/SKILL.md +157 -0
  9. package/src/defaults/skills/visualize/references/craft.md +88 -0
  10. package/src/headless-command.mjs +56 -3
  11. package/src/headless-exec.mjs +17 -1
  12. package/src/help.mjs +2 -0
  13. package/src/rules/shared/70-delivery.md +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  16. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  17. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  18. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  19. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/config.mjs +9 -1
  21. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
  22. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
  23. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
  24. package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
  25. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
  29. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  30. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  32. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
  33. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  34. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  37. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  38. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  39. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  40. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
  41. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
  42. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  43. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  44. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  45. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  50. package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  53. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
  54. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  57. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  58. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  59. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  60. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  61. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
  62. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  63. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  65. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  66. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  69. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
  70. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
  71. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
  72. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
  76. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  77. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  78. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  79. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  80. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  81. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  82. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  85. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  86. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  87. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  88. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
  89. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  90. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  91. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  92. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
  93. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
  94. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
  95. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  96. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
  97. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  100. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  101. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
  103. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  114. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  116. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  117. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  119. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  120. package/src/runtime/attachments/store.mjs +38 -0
  121. package/src/runtime/shared/atomic-file.mjs +8 -2
  122. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  123. package/src/runtime/shared/code-graph-state.mjs +12 -0
  124. package/src/runtime/shared/defaults-separation.mjs +2 -0
  125. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  126. package/src/runtime/shared/git-runner.mjs +4 -3
  127. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  128. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  129. package/src/runtime/shared/llm/model-catalog.mjs +240 -118
  130. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  131. package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
  132. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  133. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  134. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  135. package/src/runtime/shared/provider-accounts.mjs +2 -2
  136. package/src/runtime/shared/provider-api-key.mjs +21 -1
  137. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  138. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  139. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  140. package/src/runtime/shared/singleton-owner.mjs +36 -6
  141. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  142. package/src/runtime/shared/tool-surface.mjs +7 -0
  143. package/src/runtime/shared/tool-work-units.mjs +14 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  145. package/src/runtime/shared/turn-changes.mjs +791 -0
  146. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  147. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  148. package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
  149. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  150. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  151. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  152. package/src/runtime/shared/worktrees/git.mjs +247 -0
  153. package/src/runtime/shared/worktrees/include.mjs +480 -0
  154. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  155. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  156. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  157. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  158. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  159. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  160. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  161. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  162. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  163. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  164. package/src/runtime/visualize-bridge/client.mjs +181 -0
  165. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  166. package/src/session-runtime/boot/apis.mjs +35 -0
  167. package/src/session-runtime/boot/config.mjs +2 -0
  168. package/src/session-runtime/boot/facade.mjs +20 -0
  169. package/src/session-runtime/boot/infrastructure.mjs +1 -0
  170. package/src/session-runtime/boot/session.mjs +1 -0
  171. package/src/session-runtime/boot/tools.mjs +4 -0
  172. package/src/session-runtime/config-lifecycle.mjs +10 -1
  173. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
  174. package/src/session-runtime/global-extensions.mjs +2 -2
  175. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  176. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  177. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  178. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  179. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  180. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  181. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  182. package/src/session-runtime/mcp-glue/connect.mjs +11 -6
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
  184. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  185. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  186. package/src/session-runtime/model-route/set-route.mjs +15 -2
  187. package/src/session-runtime/notification-bus.mjs +9 -1
  188. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  189. package/src/session-runtime/resource-extension-sync.mjs +30 -7
  190. package/src/session-runtime/resource-mcp-api.mjs +20 -7
  191. package/src/session-runtime/route-resolution.mjs +10 -1
  192. package/src/session-runtime/route-state.mjs +16 -2
  193. package/src/session-runtime/runtime-core.mjs +5 -0
  194. package/src/session-runtime/runtime-facade.mjs +8 -0
  195. package/src/session-runtime/runtime-review-api.mjs +9 -0
  196. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  197. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  198. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  199. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  200. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  201. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  202. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  203. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  204. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  205. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  206. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  207. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  208. package/src/session-runtime/services/agent-tool.mjs +1 -0
  209. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  210. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  211. package/src/session-runtime/session-create/session-options.mjs +10 -1
  212. package/src/session-runtime/session-turn-api.mjs +1 -0
  213. package/src/session-runtime/settings-api.mjs +4 -2
  214. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  215. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  216. package/src/session-runtime/setup-tool/executor.mjs +11 -4
  217. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
  218. package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
  219. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  220. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
  221. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  222. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  223. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  224. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  225. package/src/session-runtime/turn/turn-run.mjs +6 -7
  226. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  227. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  228. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  229. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  230. package/src/session-runtime/workspace/transitions.mjs +651 -0
  231. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  232. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  233. package/src/standalone/channel-transport.mjs +13 -1
  234. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  235. package/src/standalone/daemon-owner.mjs +372 -0
  236. package/src/standalone/daemon-stored-session-views.mjs +4 -1
  237. package/src/standalone/daemon.mjs +139 -5
  238. package/src/standalone/desktop-service-registry.mjs +20 -0
  239. package/src/standalone/local-session-runtime.mjs +11 -0
  240. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  241. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  242. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  243. package/src/standalone/session-client.mjs +104 -13
  244. package/src/standalone/session-protocol.mjs +16 -0
  245. package/src/standalone/session-proxy.mjs +3 -0
  246. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  247. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  248. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  249. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  250. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  251. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  252. package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
  253. package/src/standalone/session-transport/call-route.mjs +13 -0
  254. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  255. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  256. package/src/standalone/session-transport/client-registry.mjs +20 -3
  257. package/src/standalone/session-transport/routes.mjs +79 -2
  258. package/src/standalone/session-transport/stack.mjs +2 -0
  259. package/src/standalone/session-transport.mjs +24 -1
  260. package/src/tui/app/message-selector.mjs +2 -1
  261. package/src/tui/app/model-options.mjs +35 -5
  262. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  263. package/src/tui/app/model-picker.mjs +4 -2
  264. package/src/tui/dist/index.mjs +332 -36
  265. package/src/tui/index.jsx +10 -2
  266. package/src/tui/session/compaction-view.mjs +147 -0
  267. package/src/tui/session/context-state.mjs +6 -0
  268. package/src/tui/session/initial-state.mjs +3 -0
  269. package/src/tui/session/quota-resume.mjs +2 -1
  270. package/src/tui/session/session-api/commands.mjs +10 -2
  271. package/src/tui/session/session-api/controls.mjs +4 -0
  272. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  273. package/src/tui/session/session-api/integrations.mjs +17 -0
  274. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  275. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  276. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  277. package/src/tui/session/session-api/routes.mjs +10 -1
  278. package/src/tui/session/session-api/settings.mjs +2 -0
  279. package/src/tui/session/session-api/worktree.mjs +60 -0
  280. package/src/tui/session/session-api.mjs +2 -0
  281. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  282. package/src/tui/session/session-flow/drain.mjs +15 -1
  283. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  284. package/src/tui/session/session-flow.mjs +2 -0
  285. package/src/tui/session/transcript-intake.mjs +14 -5
  286. package/src/tui/session/turn-stream.mjs +32 -4
  287. package/src/tui/session/turn-tool-cards.mjs +10 -2
  288. package/src/tui/session/turn.mjs +39 -10
@@ -20,6 +20,8 @@ process.env.MIXDOG_DAEMON_HOST = '1';
20
20
  // Size the libuv threadpool before any async fs work spins it up (see
21
21
  // uv-threadpool-boot.mjs) — imports below already touch fs.
22
22
  await import('../runtime/shared/uv-threadpool-boot.mjs');
23
+ // Before anything can import sharp: Electron on Linux must use its wasm build.
24
+ await import('../runtime/shared/sharp-electron-linux.mjs');
23
25
 
24
26
  // No V8 compile cache here, on purpose. Measured on this runtime: a cache hit
25
27
  // saves nothing (spawn→ready 316-347ms with the cache vs 318-323ms without,
@@ -37,7 +39,15 @@ import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
37
39
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
38
40
  import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
39
41
  import { listenerLeakWarningLine } from '../runtime/shared/listener-leak-warning.mjs';
40
- import { claimSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
42
+ import { claimSingletonOwner, readSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
43
+ import {
44
+ createOwnerHeartbeat,
45
+ daemonOwnerIsStale,
46
+ daemonOwnerTimings,
47
+ discoveryPid,
48
+ proveDaemonOwnerRecord,
49
+ startOwnerLivenessListener,
50
+ } from './daemon-owner.mjs';
41
51
  import { remoteIntentPath } from '../runtime/shared/remote-intent.mjs';
42
52
  import { setChannelNotifySink } from '../runtime/channels/lib/parent-bridge.mjs';
43
53
  import { setOwnerContext } from '../runtime/channels/lib/runtime-paths.mjs';
@@ -61,6 +71,7 @@ import { createSessionService } from './session-service.mjs';
61
71
  import { createLocalSessionBridge } from './daemon-local-session-bridge.mjs';
62
72
  import { createStoredSessionViews } from './daemon-stored-session-views.mjs';
63
73
  import * as quotaResumeStore from '../runtime/shared/quota-resume-store.mjs';
74
+ import { registerWorktreeCacheCleanups } from '../runtime/shared/worktrees/cache-cleanups.mjs';
64
75
  import { createInlineSessionRuntimeHost } from './session-runtime-inline-host.mjs';
65
76
  import { getStandaloneMemoryRuntime } from '../session-runtime/services/memory-runtime-proxy.mjs';
66
77
  import { createBootPhaseProfiler } from './boot-phase-profiler.mjs';
@@ -126,6 +137,13 @@ let remoteSessionState = { enabled: false, sessionId: null };
126
137
  let shuttingDown = false;
127
138
  let shutdownRecheckTimer = null;
128
139
  let replacementRequested = null;
140
+ let ownerHeartbeat = null;
141
+ // Answers /health with this pid on the port the owner record names, from
142
+ // before the claim until exit (daemon-owner.mjs).
143
+ let ownerLiveness = null;
144
+ // The endpoints daemon.json carries once published; a heartbeat re-publishes
145
+ // them when the file goes missing (TEMP cleanup).
146
+ let publishedEndpoints = null;
129
147
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
130
148
  eventLoopDelay.enable();
131
149
  let idleGc = null;
@@ -181,6 +199,7 @@ const SHUTDOWN_BUDGET_MS = 15_000;
181
199
  async function shutdown(reason, code = 0) {
182
200
  if (shuttingDown) return;
183
201
  shuttingDown = true;
202
+ ownerHeartbeat?.stop();
184
203
  exitRecorder.setReason(`shutdown: ${reason}`, code);
185
204
  if (shutdownRecheckTimer) {
186
205
  clearTimeout(shutdownRecheckTimer);
@@ -258,7 +277,9 @@ async function shutdown(reason, code = 0) {
258
277
  daemonTelemetry?.stop();
259
278
  eventLoopDelay.disable();
260
279
  try {
261
- rmSync(DAEMON_DISCOVERY_PATH, { force: true });
280
+ // Only our own: a successor that reclaimed a stale owner record may
281
+ // already have published its endpoint here.
282
+ if (discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) rmSync(DAEMON_DISCOVERY_PATH, { force: true });
262
283
  } catch {}
263
284
  try {
264
285
  releaseSingletonOwner(OWNER_PATH, process.pid);
@@ -421,7 +442,18 @@ function requestDaemonReplacement({ protocol, revision, version, force } = {}) {
421
442
  /** Pid-verified singleton claim (claimSingletonOwner reclaims a dead-pid owner
422
443
  * file and refuses only a LIVE peer). Loser exits so the spawner attaches to
423
444
  * the winner instead of running a second daemon. */
424
- function claimDaemonOwnership(bootPhases) {
445
+ async function claimDaemonOwnership(bootPhases) {
446
+ const timings = daemonOwnerTimings();
447
+ // A live-pid record is reclaimable only when its holder's endpoint does not
448
+ // answer with its pid (daemon-owner.mjs); probed before the sync claim.
449
+ const proof = await proveDaemonOwnerRecord(OWNER_PATH, {
450
+ discoveryPath: DAEMON_DISCOVERY_PATH,
451
+ staleMs: timings.staleMs,
452
+ });
453
+ // Up BEFORE the claim, so even the first record names a port that answers
454
+ // with this pid: a contender never reclaims a booting daemon on a stale-
455
+ // looking stamp (a clock jump) alone.
456
+ ownerLiveness = await startOwnerLivenessListener({ log });
425
457
  const claim = claimSingletonOwner(OWNER_PATH, {
426
458
  kind: 'mixdog-daemon',
427
459
  pid: process.pid,
@@ -430,14 +462,47 @@ function claimDaemonOwnership(bootPhases) {
430
462
  protocol: SESSION_PROTOCOL,
431
463
  revision: SESSION_REVISION,
432
464
  version: runtimeVersion(),
465
+ heartbeatAt: new Date().toISOString(),
466
+ ...(ownerLiveness ? { livenessPort: ownerLiveness.port } : {}),
467
+ },
468
+ // A live pid that neither beats nor answers is a reused pid, not a peer.
469
+ isStale: (owner) => {
470
+ const stale = daemonOwnerIsStale(owner, { proof, staleMs: timings.staleMs });
471
+ if (stale) {
472
+ log(
473
+ `reclaiming stale owner record pid=${owner.pid} claimedAt=${owner.claimedAt}` +
474
+ ` heartbeatAt=${owner.heartbeatAt ?? '-'} probe=${proof?.verdict ?? '-'}`
475
+ );
476
+ }
477
+ return stale;
433
478
  },
434
479
  });
435
480
  if (!claim.owned) {
436
481
  log(`live peer holds owner lock (pid=${claim.owner?.pid}) — exiting for attach`);
482
+ void ownerLiveness?.close();
483
+ ownerLiveness = null;
437
484
  exitRecorder.setReason(`lost singleton claim to live peer pid=${claim.owner?.pid}`, 0);
438
485
  process.exit(0);
439
486
  }
440
487
  bootPhases.mark('owner-claimed');
488
+ // Beats from here on, through boot: a slow boot is never mistaken for a
489
+ // reused pid. Losing the record to another pid, or the lease running out
490
+ // unrefreshed, means another daemon owns (or may now claim) this data dir —
491
+ // at most one may keep serving.
492
+ ownerHeartbeat = createOwnerHeartbeat({
493
+ ownerPath: OWNER_PATH,
494
+ owner: claim.owner,
495
+ heartbeatMs: timings.heartbeatMs,
496
+ fenceMs: timings.fenceMs,
497
+ tickMs: timings.tickMs,
498
+ stallMs: timings.stallMs,
499
+ log,
500
+ onBeat: republishMissingDiscovery,
501
+ onSuperseded: (record) => fenceDaemon(`owner claim superseded by pid=${record?.pid}`),
502
+ onFenced: ({ lastRefreshAt, sinceMs }) =>
503
+ fenceDaemon(`owner lease expired (no refresh for ${sinceMs}ms, last ${lastRefreshAt})`),
504
+ });
505
+ ownerHeartbeat.start();
441
506
  process.on('exit', () => {
442
507
  try {
443
508
  releaseSingletonOwner(OWNER_PATH, process.pid);
@@ -498,6 +563,8 @@ function daemonStatusSnapshot() {
498
563
  /** The file every attacher reads to find this daemon: wire identity plus both
499
564
  * loopback endpoints and their privileged tokens. */
500
565
  function publishDaemonDiscovery({ channel, session }) {
566
+ // A re-publish keeps the first publication's startedAt.
567
+ publishedEndpoints = { channel, session, startedAt: publishedEndpoints?.startedAt ?? Date.now() };
501
568
  writeJsonAtomicSync(
502
569
  DAEMON_DISCOVERY_PATH,
503
570
  {
@@ -506,7 +573,7 @@ function publishDaemonDiscovery({ channel, session }) {
506
573
  version: runtimeVersion(),
507
574
  capabilityFingerprint: SESSION_CAPABILITY_FINGERPRINT,
508
575
  pid: process.pid,
509
- startedAt: Date.now(),
576
+ startedAt: publishedEndpoints.startedAt,
510
577
  endpoints: {
511
578
  channel: { port: channel.port, token: channel.token },
512
579
  session: { port: session.port, token: session.token },
@@ -516,6 +583,42 @@ function publishDaemonDiscovery({ channel, session }) {
516
583
  );
517
584
  }
518
585
 
586
+ /** Asked by both front doors before every register/call: an expired lease
587
+ * fences here (answered 503), and after an event-loop stall nothing is
588
+ * served until the record is re-verified as ours. */
589
+ function admitOwnerWork() {
590
+ return ownerHeartbeat ? ownerHeartbeat.admit() : true;
591
+ }
592
+
593
+ /** This process no longer holds (or is about to lose) the owner lease: refuse
594
+ * new work at once, unpublish its endpoint so launchers stop finding it, then
595
+ * shut down within the shutdown budget — before a contender may reclaim. */
596
+ function fenceDaemon(reason) {
597
+ if (shuttingDown) return;
598
+ log(`${reason} — refusing new work and stopping so only one daemon serves`);
599
+ sessionTransport?.commitDrain?.(reason);
600
+ transport?.commitDrain?.(reason);
601
+ try {
602
+ if (discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) rmSync(DAEMON_DISCOVERY_PATH, { force: true });
603
+ } catch (e) {
604
+ log(`daemon.json unpublish failed: ${e?.message || e}`);
605
+ }
606
+ void shutdown(reason);
607
+ }
608
+
609
+ /** Heartbeat follow-up while this process still owns the lock: a daemon.json
610
+ * deleted (TEMP cleanup) or overwritten while it runs is written again, so
611
+ * new clients keep finding the one daemon that serves this data dir. */
612
+ function republishMissingDiscovery() {
613
+ if (!publishedEndpoints || shuttingDown || discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) return;
614
+ try {
615
+ publishDaemonDiscovery(publishedEndpoints);
616
+ log('daemon.json was missing or named another pid — re-published');
617
+ } catch (error) {
618
+ log(`daemon.json re-publish failed: ${error?.message || error}`);
619
+ }
620
+ }
621
+
519
622
  /** Ready handshake for the spawner, plus the log-sink handoff pinned to the
520
623
  * same boundary. Transport is already listening; signal ready before the heavy
521
624
  * channel-worker connect so the spawner's ready wait never blocks on service
@@ -569,6 +672,7 @@ async function startChannelFrontDoor({ channelsRuntime, bootPhases }) {
569
672
  transport = createChannelTransport({
570
673
  handleCall: channelsRuntime.handleCall,
571
674
  agentBroker: agentDispatchBroker,
675
+ admitWork: admitOwnerWork,
572
676
  log,
573
677
  // The durable channel link lives in this file. Without it a restart loses
574
678
  // the pinned session (nothing to restore) and every catalog reports Remote
@@ -617,8 +721,27 @@ async function main() {
617
721
  // are per-user and fail closed if another account owns the configured path.
618
722
  ensurePrivateRuntimeRoot(RUNTIME_ROOT);
619
723
 
620
- claimDaemonOwnership(bootPhases);
724
+ await claimDaemonOwnership(bootPhases);
621
725
  registerMemoryRuntimeLazy();
726
+ // Every in-process session runtime removes managed worktrees through the
727
+ // shared manager; its root cleanups drop the path-keyed caches (turn
728
+ // snapshots, code graph) of a removed worktree. Modules load on first use.
729
+ registerWorktreeCacheCleanups();
730
+ // Managed worktrees, off the boot path: finish interrupted deletions and
731
+ // creations, release stale leases and, with autoCleanup on, remove clean,
732
+ // merged worktrees no stored session retains (never one a session keeps).
733
+ void import('../runtime/shared/worktrees/maintenance.mjs')
734
+ .then((maintenance) => maintenance.reconcileWorktreesAtStartup())
735
+ .then((report) => {
736
+ const changed = report.removed.length + report.recovered.length + report.pruned.length;
737
+ if (changed || report.errors.length) {
738
+ log(
739
+ `worktree reconcile: removed ${report.removed.length}, recovered ${report.recovered.length}, ` +
740
+ `pruned ${report.pruned.length}, errors ${report.errors.length}`
741
+ );
742
+ }
743
+ })
744
+ .catch((error) => log(`worktree reconcile failed: ${error?.message || error}`));
622
745
  // The daemon owns prompt attachments; its census runs off the boot path
623
746
  // (first attempt after the store's start delay, then per its interval).
624
747
  void import('../runtime/attachments/store.mjs').then((store) => store.startAttachmentGc()).catch(() => {});
@@ -764,8 +887,19 @@ async function main() {
764
887
  } catch {}
765
888
  },
766
889
  onUpgradeRequested: requestDaemonReplacement,
890
+ admitWork: admitOwnerWork,
767
891
  });
768
892
  const sessionEndpoint = await bootPhases.measure('session-transport-start', () => sessionTransport.start());
893
+ // A boot whose holder stopped answering may have been superseded by a
894
+ // contender between beats; never publish over the winner's endpoint. An
895
+ // unreadable record (an antivirus lock) is not a takeover.
896
+ const ownerPid = Number(readSingletonOwner(OWNER_PATH).owner?.pid) || 0;
897
+ if (shuttingDown) return;
898
+ if (ownerPid && ownerPid !== process.pid) {
899
+ log(`owner record now names pid=${ownerPid} — exiting for attach before publishing`);
900
+ exitRecorder.setReason(`owner claim superseded by pid=${ownerPid} before publish`, 0);
901
+ process.exit(0);
902
+ }
769
903
  publishDaemonDiscovery({ channel: { port, token }, session: sessionEndpoint });
770
904
  bootPhases.mark('discovery-published');
771
905
  log(`session front door on 127.0.0.1:${sessionEndpoint.port}`);
@@ -1,6 +1,8 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { sanitizeForWire } from './session-wire-values.mjs';
4
+ import { modelProxyClientMode } from '../runtime/shared/model-proxy-mode.mjs';
5
+ import { getModelProxyChannel } from '../runtime/agent/orchestrator/providers/model-proxy-channel.mjs';
4
6
 
5
7
  const requireDesktopModule = createRequire(import.meta.url);
6
8
 
@@ -185,9 +187,24 @@ export class DesktopServiceRegistry {
185
187
  if (this.#closed) throw new Error('session service is closed');
186
188
  const token = subscriberToken(ctx);
187
189
  if (token) service.subscribers.add(token);
190
+ this.#syncModelProxyPresence();
188
191
  return { desktopId: service.desktopId };
189
192
  }
190
193
 
194
+ // Proxied model calls need a live desktop subscriber; tell the channel the
195
+ // moment the last one leaves instead of waiting for its claim timeout.
196
+ #syncModelProxyPresence() {
197
+ if (!modelProxyClientMode()) return;
198
+ let present = false;
199
+ for (const service of this.#servicesById.values()) {
200
+ if (service.subscribers.size) {
201
+ present = true;
202
+ break;
203
+ }
204
+ }
205
+ getModelProxyChannel().setClientPresent(present);
206
+ }
207
+
191
208
  // Every addressed call also (re)subscribes its caller to the service lane.
192
209
  #requireSubscribed(desktopId, ctx) {
193
210
  const service = this.#require(desktopId);
@@ -211,6 +228,7 @@ export class DesktopServiceRegistry {
211
228
 
212
229
  ready({ desktopId } = {}, ctx = null) {
213
230
  const { service, token } = this.#requireSubscribed(desktopId, ctx);
231
+ this.#syncModelProxyPresence();
214
232
  this.#onReady({ desktopId: service.desktopId, clientToken: token || null });
215
233
  return { ok: true };
216
234
  }
@@ -219,6 +237,7 @@ export class DesktopServiceRegistry {
219
237
  const service = this.#require(desktopId);
220
238
  const token = subscriberToken(ctx);
221
239
  if (token) service.subscribers.delete(token);
240
+ this.#syncModelProxyPresence();
222
241
  return { ok: true, unsubscribed: true };
223
242
  }
224
243
 
@@ -228,6 +247,7 @@ export class DesktopServiceRegistry {
228
247
  for (const service of this.#servicesById.values()) {
229
248
  service.subscribers.delete(token);
230
249
  }
250
+ this.#syncModelProxyPresence();
231
251
  }
232
252
 
233
253
  notifySessionRuntimeReleased(sessionId, reason) {
@@ -122,14 +122,17 @@ export async function createLocalSessionRuntime({
122
122
  model,
123
123
  effort,
124
124
  fast,
125
+ autoEffort,
125
126
  modelParameters,
126
127
  contextPercent,
128
+ contextTokens,
127
129
  selectedContextWindow,
128
130
  toolMode = 'full',
129
131
  remote = false,
130
132
  cwd,
131
133
  desktopSession,
132
134
  sessionProfile,
135
+ worktree,
133
136
  executeAgentControl,
134
137
  } = {}) {
135
138
  const startedAt = performance.now();
@@ -150,14 +153,18 @@ export async function createLocalSessionRuntime({
150
153
  model,
151
154
  effort,
152
155
  fast,
156
+ autoEffort,
153
157
  modelParameters,
154
158
  contextPercent,
159
+ contextTokens,
155
160
  selectedContextWindow,
156
161
  toolMode,
157
162
  remote,
158
163
  ...(cwd ? { cwd } : {}),
159
164
  ...(desktopSession ? { desktopSession } : {}),
160
165
  ...(sessionProfile && typeof sessionProfile === 'object' ? { sessionProfile } : {}),
166
+ // Session creation worktree intent; consumed by the workspace transition scope.
167
+ ...(worktree && typeof worktree === 'object' ? { worktree } : {}),
161
168
  ...(typeof executeAgentControl === 'function' ? { executeAgentControl } : {}),
162
169
  });
163
170
  bootProfile('session:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
@@ -396,6 +403,10 @@ export async function createLocalSessionRuntime({
396
403
  subscribeRuntimeNotifications,
397
404
  });
398
405
  Object.assign(bag, createSessionFlow(bag));
406
+ // Runtime-driven completion wakes (no attached surface acked the completion)
407
+ // get the same idle auto-clear as a user submit.
408
+ lifecycle.unsubscribeCompletionWakeGate =
409
+ runtime.setCompletionWakeGate?.(() => bag.autoClearBeforeWake()) || null;
399
410
  Object.assign(
400
411
  bag,
401
412
  createGoalContinuation({
@@ -13,7 +13,7 @@ const URGENT_CALLS = new Set([
13
13
  'desktop.unsubscribe',
14
14
  ]);
15
15
 
16
- export function createAttachmentCalls({ request, port, serverToken, clientToken, stopStream, log }) {
16
+ export function createAttachmentCalls({ request, port, serverToken, clientToken, stopStream, stopHeartbeat, log }) {
17
17
  let closed = false;
18
18
 
19
19
  async function call(name, args = {}, { timeoutMs = 300_000, callId = null } = {}) {
@@ -38,6 +38,7 @@ export function createAttachmentCalls({ request, port, serverToken, clientToken,
38
38
  // Preserve the daemon's machine-readable classification across the wire.
39
39
  const err = new Error(out.error);
40
40
  if (out.code) err.code = String(out.code);
41
+ if (out.details && typeof out.details === 'object') err.details = out.details;
41
42
  throw err;
42
43
  }
43
44
  return out?.result;
@@ -47,6 +48,7 @@ export function createAttachmentCalls({ request, port, serverToken, clientToken,
47
48
  if (closed) return;
48
49
  closed = true;
49
50
  stopStream();
51
+ stopHeartbeat?.();
50
52
  try {
51
53
  await request({
52
54
  port,
@@ -0,0 +1,62 @@
1
+ // The error ensureDaemon throws when no session endpoint appeared before its
2
+ // deadline. The first line is the stable message callers and tests match; the
3
+ // lines below carry what the last pass observed — an unanswered discovery, an
4
+ // owner that never published, the newest candidate's exit and first stderr
5
+ // error — so the desktop error card's details name the cause, not only the
6
+ // symptom.
7
+ export const DAEMON_UNAVAILABLE_MESSAGE = 'daemon session endpoint is unavailable';
8
+
9
+ function exitCodeText(code) {
10
+ // Windows reports NTSTATUS crash codes as large unsigned values.
11
+ return Number.isSafeInteger(code) && code > 0x7fffffff ? `0x${code.toString(16).toUpperCase()}` : String(code);
12
+ }
13
+
14
+ function candidateLines(candidate, { attempt, maxAttempts, now }) {
15
+ const head = `last daemon candidate (attempt ${attempt} of ${maxAttempts})`;
16
+ if (!candidate.pid) return [`${head} failed to spawn: ${candidate.error || 'unknown error'}`];
17
+ let state;
18
+ if (candidate.exitedAt) {
19
+ state =
20
+ `exited code=${candidate.exitCode === null ? '-' : exitCodeText(candidate.exitCode)}` +
21
+ ` signal=${candidate.exitSignal || '-'} after ${candidate.uptimeMs ?? '?'}ms` +
22
+ (candidate.exitReason ? ` (${candidate.exitReason})` : '');
23
+ } else if (candidate.ready) {
24
+ state = 'reported ready but its endpoint never answered';
25
+ } else {
26
+ const spawnedAt = Date.parse(candidate.spawnedAt || '');
27
+ state = `still running, not ready ${Number.isFinite(spawnedAt) ? now - spawnedAt : '?'}ms after spawn`;
28
+ }
29
+ const lines = [`${head}: pid=${candidate.pid} ${state}`];
30
+ if (candidate.stderrErrorLine) lines.push(`first stderr error: ${candidate.stderrErrorLine}`);
31
+ else if (candidate.stderrLastLine) lines.push(`last stderr line: ${candidate.stderrLastLine}`);
32
+ if (candidate.stderrPath) lines.push(`stderr capture: ${candidate.stderrPath}`);
33
+ return lines;
34
+ }
35
+
36
+ export function daemonUnavailableError({
37
+ discovery = null,
38
+ owner = null,
39
+ candidate = null,
40
+ spawnAttempts = 0,
41
+ maxSpawnAttempts = 0,
42
+ captureDir,
43
+ now = Date.now(),
44
+ }) {
45
+ const lines = [DAEMON_UNAVAILABLE_MESSAGE];
46
+ if (discovery) lines.push(`discovery: pid=${discovery.pid} port=${discovery.port} did not answer its health check`);
47
+ if (owner) {
48
+ lines.push(
49
+ `daemon owner: pid=${owner.pid} (claimed ${owner.claimedAt || 'at an unknown time'}) held the lock` +
50
+ ' but never published an endpoint'
51
+ );
52
+ }
53
+ if (candidate) {
54
+ lines.push(...candidateLines(candidate, { attempt: spawnAttempts, maxAttempts: maxSpawnAttempts, now }));
55
+ } else {
56
+ lines.push(`no daemon candidate was spawned (spawn attempts allowed: ${maxSpawnAttempts})`);
57
+ }
58
+ lines.push(`daemon crash captures: ${captureDir}`);
59
+ const error = new Error(lines.join('\n'));
60
+ error.daemonUnavailable = { discovery, owner, candidate, spawnAttempts, captureDir };
61
+ return error;
62
+ }
@@ -0,0 +1,47 @@
1
+ // session-client/lease-heartbeat.mjs
2
+ // A remote attachment has no local pid the daemon can probe, so its liveness
3
+ // is a lease the client renews: POST /client/heartbeat every `heartbeatMs`,
4
+ // one request in flight at a time, each bounded by heartbeatMs / 2. A failed
5
+ // beat is only logged — the next tick retries, and the stream's own recovery
6
+ // decides when the attachment is truly lost.
7
+ import { clampLeaseMs } from '../session-transport/client-registry/client-grace.mjs';
8
+
9
+ export function createLeaseHeartbeat({ request, port, serverToken, clientToken, heartbeatMs = 10_000, leaseMs, log }) {
10
+ // Always beat at least three times per (server-clamped) lease.
11
+ heartbeatMs = Math.min(heartbeatMs, Math.floor(clampLeaseMs(leaseMs) / 3));
12
+ let timer = null;
13
+ let inFlight = false;
14
+
15
+ async function beat() {
16
+ if (inFlight) return;
17
+ inFlight = true;
18
+ try {
19
+ await request({
20
+ port,
21
+ token: serverToken,
22
+ method: 'POST',
23
+ path: '/client/heartbeat',
24
+ body: { token: clientToken },
25
+ timeoutMs: heartbeatMs / 2,
26
+ control: true,
27
+ });
28
+ } catch (error) {
29
+ log(`heartbeat failed: ${error?.message || error}`);
30
+ } finally {
31
+ inFlight = false;
32
+ }
33
+ }
34
+
35
+ function start() {
36
+ if (timer) return;
37
+ timer = setInterval(beat, heartbeatMs);
38
+ timer.unref?.();
39
+ }
40
+
41
+ function stop() {
42
+ if (timer) clearInterval(timer);
43
+ timer = null;
44
+ }
45
+
46
+ return { start, stop };
47
+ }