mixdog 1.1.2 → 1.2.1

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 (424) hide show
  1. package/README.md +8 -5
  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 +10 -0
  7. package/src/defaults/skills/pptx/SKILL.md +6 -6
  8. package/src/defaults/skills/pptx/references/review.md +6 -4
  9. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  10. package/src/defaults/skills/setup/references/actions.md +18 -5
  11. package/src/defaults/skills/visualize/SKILL.md +160 -0
  12. package/src/defaults/skills/visualize/references/craft.md +88 -0
  13. package/src/headless-command.mjs +56 -3
  14. package/src/headless-exec.mjs +17 -1
  15. package/src/help.mjs +2 -0
  16. package/src/rules/shared/60-verification.md +3 -1
  17. package/src/rules/shared/70-delivery.md +4 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  22. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  23. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  24. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  25. package/src/runtime/agent/orchestrator/config.mjs +19 -5
  26. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  27. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  28. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  29. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
  30. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
  31. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
  32. package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
  33. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  35. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  36. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  43. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  45. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  50. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
  51. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  52. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  53. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  56. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  57. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  59. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  60. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  61. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
  62. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  64. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  66. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
  67. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
  78. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  79. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  80. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
  81. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  82. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  83. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
  85. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  86. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  87. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  88. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  90. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  91. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  92. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  93. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  94. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  95. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  96. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  97. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
  98. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  99. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  100. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  101. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  102. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  103. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  104. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  105. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  106. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  107. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  108. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  109. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  112. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  114. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
  115. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
  116. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
  124. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  125. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  126. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  128. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  131. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  133. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  136. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  137. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  138. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
  139. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  140. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  141. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  142. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  143. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  144. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  145. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
  146. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
  147. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  148. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
  149. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  150. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  151. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  152. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
  153. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  157. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  158. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  159. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  160. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  161. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  162. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
  163. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  164. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  165. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  166. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  167. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
  168. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  169. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  175. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  176. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  177. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  179. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  180. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  181. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  182. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  183. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  184. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  185. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  186. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  187. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  188. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  189. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  190. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  191. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  192. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  193. package/src/runtime/attachments/store.d.mts +3 -0
  194. package/src/runtime/attachments/store.mjs +38 -0
  195. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  196. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  197. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  199. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  200. package/src/runtime/memory/lib/memory.mjs +1 -1
  201. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  202. package/src/runtime/shared/atomic-file.mjs +33 -6
  203. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  204. package/src/runtime/shared/code-graph-state.mjs +12 -0
  205. package/src/runtime/shared/defaults-separation.mjs +2 -0
  206. package/src/runtime/shared/extension-scopes.mjs +5 -3
  207. package/src/runtime/shared/file-lock.mjs +37 -11
  208. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  209. package/src/runtime/shared/git-runner.mjs +4 -3
  210. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  211. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  212. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  213. package/src/runtime/shared/llm/model-catalog.mjs +240 -130
  214. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  215. package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
  216. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  217. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  218. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  219. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  220. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  221. package/src/runtime/shared/pristine-execution.mjs +28 -4
  222. package/src/runtime/shared/provider-accounts.mjs +125 -21
  223. package/src/runtime/shared/provider-api-key.mjs +33 -1
  224. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  225. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  226. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  227. package/src/runtime/shared/shell-display.mjs +9 -3
  228. package/src/runtime/shared/singleton-owner.mjs +36 -6
  229. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  230. package/src/runtime/shared/skill-mentions.mjs +133 -0
  231. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  232. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  233. package/src/runtime/shared/tool-primitives.mjs +35 -2
  234. package/src/runtime/shared/tool-surface.mjs +7 -0
  235. package/src/runtime/shared/tool-work-units.mjs +14 -0
  236. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  237. package/src/runtime/shared/turn-changes.mjs +791 -0
  238. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  239. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  240. package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
  241. package/src/runtime/shared/worker-requests.mjs +15 -2
  242. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  243. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  244. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  245. package/src/runtime/shared/worktrees/git.mjs +247 -0
  246. package/src/runtime/shared/worktrees/include.mjs +480 -0
  247. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  248. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  249. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  250. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  251. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  252. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  253. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  254. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  255. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  256. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  257. package/src/runtime/visualize-bridge/client.mjs +181 -0
  258. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  259. package/src/session-runtime/automation-agents.mjs +8 -2
  260. package/src/session-runtime/boot/apis.mjs +90 -0
  261. package/src/session-runtime/boot/config.mjs +7 -2
  262. package/src/session-runtime/boot/facade.mjs +21 -0
  263. package/src/session-runtime/boot/infrastructure.mjs +30 -18
  264. package/src/session-runtime/boot/session.mjs +1 -0
  265. package/src/session-runtime/boot/tools.mjs +4 -0
  266. package/src/session-runtime/config-lifecycle.mjs +10 -1
  267. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  268. package/src/session-runtime/conversation-import/service.mjs +369 -0
  269. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
  270. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  271. package/src/session-runtime/global-extensions.mjs +2 -2
  272. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  273. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  274. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  275. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  276. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  277. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  278. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  279. package/src/session-runtime/mcp-glue/connect.mjs +68 -16
  280. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  281. package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
  282. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  283. package/src/session-runtime/mcp-glue.mjs +22 -2
  284. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  285. package/src/session-runtime/model-route/set-route.mjs +15 -2
  286. package/src/session-runtime/new-session-config.mjs +3 -2
  287. package/src/session-runtime/notification-bus.mjs +18 -3
  288. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  289. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  290. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  291. package/src/session-runtime/provider-readiness.mjs +16 -10
  292. package/src/session-runtime/resource-extension-sync.mjs +45 -7
  293. package/src/session-runtime/resource-mcp-api.mjs +121 -84
  294. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  295. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  296. package/src/session-runtime/resource-skills-api.mjs +23 -2
  297. package/src/session-runtime/route-resolution.mjs +10 -1
  298. package/src/session-runtime/route-state.mjs +16 -2
  299. package/src/session-runtime/runtime-core.mjs +5 -0
  300. package/src/session-runtime/runtime-facade.mjs +8 -0
  301. package/src/session-runtime/runtime-review-api.mjs +9 -0
  302. package/src/session-runtime/schedule-session-run.mjs +1 -1
  303. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  304. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  305. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  306. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  307. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  308. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  310. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  311. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  312. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  313. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  314. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  315. package/src/session-runtime/services/agent-tool.mjs +1 -0
  316. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  317. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  318. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  319. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  320. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  321. package/src/session-runtime/services/provider-admin.mjs +64 -18
  322. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  323. package/src/session-runtime/session-create/session-options.mjs +10 -1
  324. package/src/session-runtime/session-turn-api.mjs +1 -0
  325. package/src/session-runtime/settings-api.mjs +4 -2
  326. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  327. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  328. package/src/session-runtime/setup-tool/executor.mjs +23 -5
  329. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  330. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
  331. package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
  332. package/src/session-runtime/skills-api.mjs +7 -3
  333. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  334. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
  335. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  336. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  337. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  338. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  339. package/src/session-runtime/turn/turn-run.mjs +6 -7
  340. package/src/session-runtime/webhook-session-run.mjs +16 -4
  341. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  342. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  343. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  344. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  345. package/src/session-runtime/workspace/transitions.mjs +651 -0
  346. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  347. package/src/skill-script-entry.mjs +36 -0
  348. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  349. package/src/standalone/channel-transport.mjs +13 -1
  350. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  351. package/src/standalone/daemon-owner.mjs +372 -0
  352. package/src/standalone/daemon-stored-session-views.mjs +15 -4
  353. package/src/standalone/daemon.mjs +179 -8
  354. package/src/standalone/desktop-service-registry.mjs +20 -0
  355. package/src/standalone/local-session-runtime.mjs +11 -0
  356. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  357. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  358. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  359. package/src/standalone/session-client.mjs +104 -13
  360. package/src/standalone/session-protocol.mjs +21 -0
  361. package/src/standalone/session-proxy.mjs +3 -0
  362. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  363. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  364. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  365. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  366. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  367. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  368. package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
  369. package/src/standalone/session-transport/call-route.mjs +13 -0
  370. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  371. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  372. package/src/standalone/session-transport/client-registry.mjs +20 -3
  373. package/src/standalone/session-transport/routes.mjs +79 -2
  374. package/src/standalone/session-transport/stack.mjs +2 -0
  375. package/src/standalone/session-transport.mjs +24 -1
  376. package/src/tui/App.jsx +9 -1
  377. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  378. package/src/tui/app/app-view.jsx +6 -0
  379. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  380. package/src/tui/app/message-selector.mjs +2 -1
  381. package/src/tui/app/model-options.mjs +37 -5
  382. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  383. package/src/tui/app/model-picker.mjs +4 -2
  384. package/src/tui/app/skill-mention-palette.mjs +88 -0
  385. package/src/tui/app/slash-palette.mjs +23 -7
  386. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  387. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  388. package/src/tui/app/use-prompt-state.mjs +5 -0
  389. package/src/tui/components/Message.jsx +22 -1
  390. package/src/tui/components/PromptInput.jsx +21 -3
  391. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  392. package/src/tui/components/TranscriptItem.jsx +9 -1
  393. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  394. package/src/tui/components/user-message-skills.mjs +38 -0
  395. package/src/tui/dist/index.mjs +839 -207
  396. package/src/tui/index.jsx +10 -2
  397. package/src/tui/session/compaction-view.mjs +147 -0
  398. package/src/tui/session/context-state.mjs +6 -0
  399. package/src/tui/session/initial-state.mjs +3 -0
  400. package/src/tui/session/queue-helpers.mjs +7 -2
  401. package/src/tui/session/quota-resume.mjs +2 -1
  402. package/src/tui/session/session-api/commands.mjs +10 -2
  403. package/src/tui/session/session-api/controls.mjs +4 -0
  404. package/src/tui/session/session-api/extensions.mjs +4 -2
  405. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  406. package/src/tui/session/session-api/integrations.mjs +17 -0
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  408. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  409. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  410. package/src/tui/session/session-api/routes.mjs +10 -1
  411. package/src/tui/session/session-api/settings.mjs +9 -0
  412. package/src/tui/session/session-api/worktree.mjs +60 -0
  413. package/src/tui/session/session-api.mjs +2 -0
  414. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  415. package/src/tui/session/session-flow/drain.mjs +27 -3
  416. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  417. package/src/tui/session/session-flow.mjs +2 -0
  418. package/src/tui/session/transcript-intake.mjs +14 -5
  419. package/src/tui/session/tui-steering-persist.mjs +25 -9
  420. package/src/tui/session/turn-stream.mjs +32 -4
  421. package/src/tui/session/turn-tool-cards.mjs +10 -2
  422. package/src/tui/session/turn.mjs +56 -10
  423. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  424. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -0,0 +1,1080 @@
1
+ // Model proxy bridge: remote-mode model calls for processes the daemon does
2
+ // not host.
3
+ //
4
+ // With MIXDOG_MODEL_PROXY=client the daemon holds no model credentials; its
5
+ // ModelProxyProviders talk through the in-process channel the desktop client
6
+ // attaches to (model-proxy-channel.mjs). A skill script started by the model's
7
+ // shell (headless exec, one fresh runtime per run) is a separate process and
8
+ // cannot reach that channel, so the daemon relays for it over loopback:
9
+ //
10
+ // child -> daemon {"type":"hello","v":1,"token":…} (once, first)
11
+ // daemon -> child {"type":"state","connected","generation"} (initial + on change)
12
+ // child -> daemon {"type":"reserve","id","bytes"} (request frame size)
13
+ // daemon -> child {"type":"grant","id"} (FIFO, within the byte cap)
14
+ // child -> daemon {"type":"req","id","callbacks":[…],"call":<wire>} (only once granted)
15
+ // daemon -> child {"type":"event","id","seq","name","args":<wire>}
16
+ // child -> daemon {"type":"result","id","seq","ok","value"|"error"}
17
+ // daemon -> child {"type":"done","id","ok","value"|"error"}
18
+ // child -> daemon {"type":"cancel","id"}
19
+ //
20
+ // NDJSON frames; every frame is decoded with the channel's wire codec, so
21
+ // bytes and errors (status, code, partial usage…) survive exactly as on the
22
+ // PC wire. The endpoint and its per-lifetime token are published only in the
23
+ // owner-only daemon.json. The bridge itself never holds a credential: it
24
+ // forwards to the same channel the daemon's own providers use.
25
+ //
26
+ // Admission: a request's bytes are reserved before they are sent, so the
27
+ // daemon never has to stop reading a socket for budget. Control frames
28
+ // (result, cancel, reserve) are always read and handled at once, bounded by
29
+ // their own frame limit; only reservations wait, first come first served.
30
+ import net from 'node:net';
31
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
32
+ import { readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { createLoopbackListener } from '../../../shared/loopback-listener.mjs';
35
+ import { isPidAlive } from '../../../shared/pid-liveness.mjs';
36
+ import {
37
+ MODEL_PROXY_METHODS,
38
+ ModelProxyUnavailableError,
39
+ decodeModelProxyWire,
40
+ encodeModelProxyWire,
41
+ modelProxyErrorFromWire,
42
+ modelProxyErrorToWire,
43
+ modelProxyWireValue,
44
+ } from './model-proxy-channel.mjs';
45
+
46
+ export const MODEL_PROXY_BRIDGE_PROTOCOL = 1;
47
+
48
+ const MiB = 1024 * 1024;
49
+ /**
50
+ * Byte limits are 1.5× the PC's character limits (model-proxy-host.ts: 128 MiB
51
+ * per call, 256 MiB in flight): multibyte transcript text the PC accepts is
52
+ * never refused here, and an ASCII payload (base64 attachments) meets the PC's
53
+ * own limit first.
54
+ */
55
+ export const MODEL_PROXY_BRIDGE_LIMITS = Object.freeze({
56
+ helloTimeoutMs: 5_000,
57
+ helloMaxBytes: 1024,
58
+ /** Every frame but `req`: results, cancels, reservations (callback answers are tiny). */
59
+ controlFrameMaxBytes: MiB,
60
+ requestFrameMaxBytes: 192 * MiB,
61
+ /** Granted request frames (being received or running), across all connections. */
62
+ aggregateBytes: 384 * MiB,
63
+ /** Unwritten bytes queued to one client before it is dropped as not reading. */
64
+ outgoingQueueBytes: 64 * MiB,
65
+ /** A relayed callback the client has not answered by then aborts its call. */
66
+ callbackAckMs: 30_000,
67
+ /** A partial frame, or a grant whose own request frame, receives no bytes for this long: its client is dropped. */
68
+ frameStallMs: 60_000,
69
+ /** A granted request frame must have arrived in full by then, however steadily it trickles. */
70
+ grantMaxMs: 300_000,
71
+ maxConnections: 16,
72
+ maxRequestsPerConnection: 16,
73
+ maxCallbacks: 32,
74
+ });
75
+
76
+ const TOKEN_RE = /^[0-9a-f]{64}$/;
77
+ const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
78
+ // Same rule as splitModelProxySendOptions (/^on[A-Z]/), bounded.
79
+ const CALLBACK_NAME_RE = /^on[A-Z][A-Za-z0-9_]{0,62}$/;
80
+ const MAX_FIELD_CHARS = 256;
81
+ const NEWLINE = 0x0a;
82
+ const REQ_PREFIX = Buffer.from('{"type":"req",');
83
+ // The bridge client writes `{"type":"req","id":"<id>",…`: enough head bytes name the grant.
84
+ const REQ_ID_RE = /^\{"type":"req","id":"([A-Za-z0-9_-]{1,64})"/;
85
+ const REQ_HEAD_BYTES = 96;
86
+ const RECONNECT_DELAY_MS = 250;
87
+
88
+ function bridgeError(code, message) {
89
+ const error = new Error(`model proxy bridge: ${message}`);
90
+ error.name = 'ModelProxyBridgeError';
91
+ error.code = code;
92
+ return error;
93
+ }
94
+
95
+ function abortError(message) {
96
+ const error = new Error(message);
97
+ error.name = 'AbortError';
98
+ return error;
99
+ }
100
+
101
+ function abortReason(signal) {
102
+ if (signal?.reason instanceof Error) return signal.reason;
103
+ return abortError(String(signal?.reason || 'model call aborted'));
104
+ }
105
+
106
+ function unavailable(detail, reason) {
107
+ const error = new ModelProxyUnavailableError(detail);
108
+ if (reason) error.reason = reason;
109
+ return error;
110
+ }
111
+
112
+ function isPlainObject(value) {
113
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
114
+ }
115
+
116
+ /** Log-safe token: no frame content, only a bounded identifier. */
117
+ function logField(value) {
118
+ const text = String(value ?? '');
119
+ return text ? text.slice(0, 80).replace(/[^\w.:/@+-]/g, '_') : '-';
120
+ }
121
+
122
+ // ── Framing ─────────────────────────────────────────────────────────────────
123
+
124
+ /** Newline framing over chunk lists: a large frame is joined once, and its
125
+ * size is checked against its limit before it is ever parsed. */
126
+ class LineBuffer {
127
+ #chunks = [];
128
+ #headCache = null;
129
+ size = 0;
130
+
131
+ /** The frame's first bytes (stable once long enough: the frame start never changes). */
132
+ #head(piece, length) {
133
+ if (this.#headCache) return this.#headCache;
134
+ const parts = [];
135
+ let have = 0;
136
+ for (const part of [...this.#chunks, piece]) {
137
+ if (have >= length) break;
138
+ parts.push(part);
139
+ have += part.length;
140
+ }
141
+ const head = Buffer.concat(parts).subarray(0, length);
142
+ if (head.length >= length) this.#headCache = head;
143
+ return head;
144
+ }
145
+
146
+ /** The first `length` bytes of the buffered partial frame. */
147
+ peek(length) {
148
+ const parts = [];
149
+ let have = 0;
150
+ for (const part of this.#chunks) {
151
+ if (have >= length) break;
152
+ parts.push(part);
153
+ have += part.length;
154
+ }
155
+ return Buffer.concat(parts).subarray(0, length);
156
+ }
157
+
158
+ /**
159
+ * Feed `chunk`; `onLine(line)` runs for each complete frame as it is found
160
+ * (so a frame can change the limits of the next) and returns false to stop.
161
+ * `limit.min()` is the size below which no frame is refused and
162
+ * `limit.forHead(head)` the limit of a frame starting with `head`. Returns
163
+ * false once a frame exceeds its limit.
164
+ */
165
+ push(chunk, limit, onLine) {
166
+ let start = 0;
167
+ for (;;) {
168
+ const index = chunk.indexOf(NEWLINE, start);
169
+ const piece = chunk.subarray(start, index < 0 ? chunk.length : index);
170
+ const size = this.size + piece.length;
171
+ if (size > limit.min() && size > limit.forHead(this.#head(piece, REQ_PREFIX.length))) return false;
172
+ if (index < 0) {
173
+ if (piece.length) {
174
+ this.#chunks.push(piece);
175
+ this.size = size;
176
+ }
177
+ return true;
178
+ }
179
+ const line = this.#chunks.length ? Buffer.concat([...this.#chunks, piece]) : piece;
180
+ this.#chunks = [];
181
+ this.#headCache = null;
182
+ this.size = 0;
183
+ if (onLine(line) === false) return true;
184
+ start = index + 1;
185
+ }
186
+ }
187
+ }
188
+
189
+ const startsWithReq = (head) => head.length >= REQ_PREFIX.length && head.equals(REQ_PREFIX);
190
+
191
+ // ── Daemon side ─────────────────────────────────────────────────────────────
192
+
193
+ /**
194
+ * The daemon's loopback relay to `channel`. `admit()` gates every new
195
+ * connection and request (owner fencing, shutdown); `trackWork(active)` hears
196
+ * every change of the running-request count; `log` gets one line per request
197
+ * (id, method, provider, model, duration, error code) and never a token or a
198
+ * frame.
199
+ */
200
+ export function createModelProxyBridgeServer({
201
+ channel,
202
+ admit = () => true,
203
+ trackWork = () => {},
204
+ log = () => {},
205
+ limits: limitOverrides = {},
206
+ } = {}) {
207
+ const limits = { ...MODEL_PROXY_BRIDGE_LIMITS, ...limitOverrides };
208
+ const tokenBytes = randomBytes(32);
209
+ const token = tokenBytes.toString('hex');
210
+ const connections = new Set();
211
+ /** Reservations waiting for byte budget, oldest first. */
212
+ const waiting = [];
213
+ let draining = false;
214
+ let redrain = false;
215
+ const settling = new Set();
216
+ /** Bytes of granted reservations and of running requests' frames. */
217
+ let budgetUsed = 0;
218
+ let active = 0;
219
+ let closing = false;
220
+ let closed = null;
221
+
222
+ const server = net.createServer({ allowHalfOpen: false }, onConnection);
223
+ const listener = createLoopbackListener({
224
+ server,
225
+ onListening: () => {},
226
+ onError: (error) => log(`model-proxy bridge listener error: ${logField(error?.code || error?.name)}`),
227
+ });
228
+ const unsubscribe = channel.onStateChange(() => {
229
+ for (const conn of connections) if (conn.authenticated) sendState(conn);
230
+ });
231
+
232
+ function setActive(delta) {
233
+ active += delta;
234
+ trackWork(active);
235
+ }
236
+
237
+ function onConnection(socket) {
238
+ if (closing || connections.size >= limits.maxConnections || !admit()) {
239
+ socket.destroy();
240
+ return;
241
+ }
242
+ socket.setNoDelay(true);
243
+ const conn = {
244
+ socket,
245
+ authenticated: false,
246
+ closed: false,
247
+ lines: new LineBuffer(),
248
+ limit: null,
249
+ reservations: new Map(),
250
+ /** The grant the buffered partial frame belongs to, and its size when last seen. */
251
+ partialGrant: null,
252
+ partialSize: 0,
253
+ requests: new Map(),
254
+ stallTimer: null,
255
+ helloTimer: setTimeout(() => {
256
+ if (!conn.authenticated) closeConnection(conn);
257
+ }, limits.helloTimeoutMs),
258
+ };
259
+ conn.helloTimer.unref?.();
260
+ conn.limit = frameLimit(conn);
261
+ connections.add(conn);
262
+ socket.on('data', (chunk) => onData(conn, chunk));
263
+ socket.on('error', () => {});
264
+ socket.on('close', () => closeConnection(conn));
265
+ }
266
+
267
+ function frameLimit(conn) {
268
+ return {
269
+ min: () => (conn.authenticated ? limits.controlFrameMaxBytes : limits.helloMaxBytes),
270
+ forHead: (head) => {
271
+ if (!conn.authenticated) return limits.helloMaxBytes;
272
+ // A request frame may only be as large as the largest grant it could be for.
273
+ return startsWithReq(head) ? largestGrant(conn) : limits.controlFrameMaxBytes;
274
+ },
275
+ };
276
+ }
277
+
278
+ function largestGrant(conn) {
279
+ let largest = 0;
280
+ for (const reservation of conn.reservations.values()) {
281
+ if (reservation.granted && reservation.bytes > largest) largest = reservation.bytes;
282
+ }
283
+ return largest;
284
+ }
285
+
286
+ /** Never paused for budget: request bytes were reserved before they were sent. */
287
+ function onData(conn, chunk) {
288
+ if (conn.closed) return;
289
+ const withinLimits = conn.lines.push(chunk, conn.limit, (line) => {
290
+ handleLine(conn, line);
291
+ return !conn.closed;
292
+ });
293
+ if (conn.closed) return;
294
+ if (!withinLimits) {
295
+ fail(conn, 'MODEL_PROXY_BRIDGE_FRAME_TOO_LARGE');
296
+ return;
297
+ }
298
+ trackGrantProgress(conn);
299
+ armStall(conn);
300
+ }
301
+
302
+ /**
303
+ * A grant's stall timer is refreshed only by its own `req` frame growing:
304
+ * control frames and frames for other ids never keep a silent grant alive.
305
+ * (A frame that completes settles its grant in handleRequest.)
306
+ */
307
+ function trackGrantProgress(conn) {
308
+ const size = conn.lines.size;
309
+ if (size === 0) {
310
+ conn.partialGrant = null;
311
+ return;
312
+ }
313
+ const id = REQ_ID_RE.exec(conn.lines.peek(REQ_HEAD_BYTES).toString('latin1'))?.[1];
314
+ const reservation = id === undefined ? undefined : conn.reservations.get(id);
315
+ const owner = reservation?.granted ? reservation : null;
316
+ if (owner && (owner !== conn.partialGrant || size > conn.partialSize)) owner.timer?.refresh();
317
+ conn.partialGrant = owner;
318
+ conn.partialSize = size;
319
+ }
320
+
321
+ function armStall(conn) {
322
+ if (conn.lines.size === 0) {
323
+ clearTimeout(conn.stallTimer);
324
+ conn.stallTimer = null;
325
+ return;
326
+ }
327
+ if (conn.stallTimer) {
328
+ conn.stallTimer.refresh();
329
+ return;
330
+ }
331
+ conn.stallTimer = setTimeout(() => fail(conn, 'MODEL_PROXY_BRIDGE_FRAME_STALLED'), limits.frameStallMs);
332
+ conn.stallTimer.unref?.();
333
+ }
334
+
335
+ /**
336
+ * Grants waiting reservations strictly in FIFO order. Non-reentrant: a grant
337
+ * can close its connection (slow client), which calls back in here; a nested
338
+ * call only requests another pass, so the loop always acts on the current
339
+ * head of the queue. A reservation leaves the queue only once granted or
340
+ * withdrawn (cancel, disconnect, fencing); budget held by grants and running
341
+ * requests is always released by their completion or by a bounded timeout,
342
+ * so a waiting reservation is eventually granted.
343
+ */
344
+ function drain() {
345
+ if (draining) {
346
+ redrain = true;
347
+ return;
348
+ }
349
+ draining = true;
350
+ try {
351
+ do {
352
+ redrain = false;
353
+ while (waiting.length) {
354
+ const reservation = waiting[0];
355
+ if (reservation.conn.closed || reservation.conn.reservations.get(reservation.id) !== reservation) {
356
+ waiting.shift();
357
+ continue;
358
+ }
359
+ if (budgetUsed + reservation.bytes > limits.aggregateBytes) break;
360
+ waiting.shift();
361
+ grant(reservation);
362
+ }
363
+ } while (redrain);
364
+ } finally {
365
+ draining = false;
366
+ }
367
+ }
368
+
369
+ function grant(reservation) {
370
+ const { conn } = reservation;
371
+ budgetUsed += reservation.bytes;
372
+ reservation.granted = true;
373
+ // The granted frame must keep arriving, and arrive in full by the ceiling,
374
+ // or its budget is reclaimed.
375
+ reservation.timer = setTimeout(() => fail(conn, 'MODEL_PROXY_BRIDGE_FRAME_STALLED'), limits.frameStallMs);
376
+ reservation.timer.unref?.();
377
+ reservation.deadline = setTimeout(() => fail(conn, 'MODEL_PROXY_BRIDGE_FRAME_STALLED'), limits.grantMaxMs);
378
+ reservation.deadline.unref?.();
379
+ send(conn, { type: 'grant', id: reservation.id });
380
+ }
381
+
382
+ /** Withdraws a reservation; a granted one gives its budget back (the caller drains). */
383
+ function dropReservation(conn, reservation) {
384
+ conn.reservations.delete(reservation.id);
385
+ clearTimeout(reservation.timer);
386
+ clearTimeout(reservation.deadline);
387
+ if (reservation.granted) budgetUsed -= reservation.bytes;
388
+ }
389
+
390
+ function handleLine(conn, line) {
391
+ if (!conn.authenticated) {
392
+ handleHello(conn, line);
393
+ return;
394
+ }
395
+ let frame;
396
+ try {
397
+ frame = decodeModelProxyWire(line.toString('utf8'));
398
+ } catch {
399
+ fail(conn, 'MODEL_PROXY_BRIDGE_BAD_FRAME');
400
+ return;
401
+ }
402
+ if (!isPlainObject(frame) || (line.length > limits.controlFrameMaxBytes && frame.type !== 'req')) {
403
+ fail(conn, 'MODEL_PROXY_BRIDGE_BAD_FRAME');
404
+ return;
405
+ }
406
+ if (frame.type === 'req') handleRequest(conn, frame, line.length + 1);
407
+ else if (frame.type === 'reserve') handleReserve(conn, frame);
408
+ else if (frame.type === 'result') handleResult(conn, frame);
409
+ else if (frame.type === 'cancel') handleCancel(conn, frame);
410
+ else fail(conn, 'MODEL_PROXY_BRIDGE_BAD_FRAME');
411
+ }
412
+
413
+ function handleHello(conn, line) {
414
+ let hello = null;
415
+ try {
416
+ hello = JSON.parse(line.toString('utf8'));
417
+ } catch {
418
+ /* rejected below */
419
+ }
420
+ const presented = typeof hello?.token === 'string' && TOKEN_RE.test(hello.token) ? hello.token : null;
421
+ const valid =
422
+ hello?.type === 'hello' &&
423
+ hello.v === MODEL_PROXY_BRIDGE_PROTOCOL &&
424
+ presented !== null &&
425
+ timingSafeEqual(Buffer.from(presented, 'hex'), tokenBytes);
426
+ if (!valid) {
427
+ fail(conn, 'MODEL_PROXY_BRIDGE_UNAUTHORIZED');
428
+ return;
429
+ }
430
+ if (!admit()) {
431
+ fail(conn, 'MODEL_PROXY_UNAVAILABLE');
432
+ return;
433
+ }
434
+ clearTimeout(conn.helloTimer);
435
+ conn.authenticated = true;
436
+ sendState(conn);
437
+ }
438
+
439
+ function refuse(conn, id, error) {
440
+ send(conn, { type: 'done', id, ok: false, error: modelProxyErrorToWire(error) });
441
+ }
442
+
443
+ function validCallbackNames(names) {
444
+ return (
445
+ Array.isArray(names) &&
446
+ names.length <= limits.maxCallbacks &&
447
+ names.every((name) => typeof name === 'string' && CALLBACK_NAME_RE.test(name)) &&
448
+ new Set(names).size === names.length
449
+ );
450
+ }
451
+
452
+ /** A request announces its frame size first; it is granted when the byte budget allows. */
453
+ function handleReserve(conn, frame) {
454
+ const { id, bytes } = frame;
455
+ if (typeof id !== 'string' || !ID_RE.test(id) || !Number.isSafeInteger(bytes) || bytes < 1) {
456
+ fail(conn, 'MODEL_PROXY_BRIDGE_BAD_FRAME');
457
+ return;
458
+ }
459
+ if (conn.requests.has(id) || conn.reservations.has(id)) {
460
+ fail(conn, 'MODEL_PROXY_BRIDGE_DUPLICATE_ID');
461
+ return;
462
+ }
463
+ if (!admit()) {
464
+ refuse(conn, id, unavailable('the daemon is not accepting new model calls'));
465
+ return;
466
+ }
467
+ if (conn.requests.size + conn.reservations.size >= limits.maxRequestsPerConnection) {
468
+ refuse(conn, id, bridgeError('MODEL_PROXY_BRIDGE_LIMIT', 'too many concurrent calls on one connection'));
469
+ return;
470
+ }
471
+ if (bytes > limits.requestFrameMaxBytes || bytes > limits.aggregateBytes) {
472
+ refuse(conn, id, bridgeError('MODEL_PROXY_REQUEST_TOO_LARGE', 'the model call exceeds the bridge limit'));
473
+ return;
474
+ }
475
+ const reservation = { conn, id, bytes, granted: false, timer: null, deadline: null };
476
+ conn.reservations.set(id, reservation);
477
+ waiting.push(reservation);
478
+ drain();
479
+ }
480
+
481
+ function requestRefusal(call, names) {
482
+ if (!admit()) return unavailable('the daemon is not accepting new model calls');
483
+ if (!isPlainObject(call) || !MODEL_PROXY_METHODS.includes(call.method)) {
484
+ return bridgeError('MODEL_PROXY_BRIDGE_METHOD', 'unsupported model proxy method');
485
+ }
486
+ const provider = call.provider ?? '';
487
+ const model = call.model ?? null;
488
+ if (
489
+ typeof provider !== 'string' ||
490
+ provider.length > MAX_FIELD_CHARS ||
491
+ (model !== null && (typeof model !== 'string' || model.length > MAX_FIELD_CHARS)) ||
492
+ !validCallbackNames(names)
493
+ ) {
494
+ return bridgeError('MODEL_PROXY_BRIDGE_BAD_REQUEST', 'malformed model proxy request');
495
+ }
496
+ return null;
497
+ }
498
+
499
+ function handleRequest(conn, frame, bytes) {
500
+ const { id, call, callbacks: names } = frame;
501
+ const reservation = typeof id === 'string' ? conn.reservations.get(id) : undefined;
502
+ // Only a granted reservation admits a request frame, and only up to its size.
503
+ if (!reservation?.granted || bytes > reservation.bytes) {
504
+ fail(conn, 'MODEL_PROXY_BRIDGE_BAD_FRAME');
505
+ return;
506
+ }
507
+ conn.reservations.delete(id);
508
+ clearTimeout(reservation.timer);
509
+ clearTimeout(reservation.deadline);
510
+ const refusal = requestRefusal(call, names);
511
+ if (refusal) {
512
+ budgetUsed -= reservation.bytes;
513
+ refuse(conn, id, refusal);
514
+ drain();
515
+ return;
516
+ }
517
+ const entry = {
518
+ id,
519
+ method: call.method,
520
+ provider: call.provider ?? '',
521
+ model: call.model ?? null,
522
+ // The granted bytes stay reserved until the call settles.
523
+ bytes: reservation.bytes,
524
+ controller: new AbortController(),
525
+ seq: 0,
526
+ waiters: new Map(),
527
+ queue: Promise.resolve(),
528
+ startedAt: Date.now(),
529
+ };
530
+ conn.requests.set(id, entry);
531
+ setActive(1);
532
+ const callbacks = Object.fromEntries(names.map((name) => [name, (...args) => relay(conn, entry, name, args)]));
533
+ const { callbacks: _names, ...upstream } = call;
534
+ const settled = channel
535
+ .request(upstream, { signal: entry.controller.signal, callbacks })
536
+ .then(
537
+ (value) => finish(conn, entry, true, value),
538
+ (error) => finish(conn, entry, false, error)
539
+ )
540
+ .finally(() => settling.delete(settled));
541
+ settling.add(settled);
542
+ }
543
+
544
+ /** One callback of one request, strictly after the previous one answered. */
545
+ function relay(conn, entry, name, args) {
546
+ const run = () =>
547
+ new Promise((resolve, reject) => {
548
+ if (conn.closed || entry.controller.signal.aborted) {
549
+ reject(entry.controller.signal.reason ?? abortError('model call aborted'));
550
+ return;
551
+ }
552
+ entry.seq += 1;
553
+ const seq = entry.seq;
554
+ const timer = setTimeout(() => {
555
+ entry.waiters.delete(seq);
556
+ const error = unavailable('the bridge client did not acknowledge a model callback');
557
+ reject(error);
558
+ abortEntry(entry, error);
559
+ }, limits.callbackAckMs);
560
+ timer.unref?.();
561
+ entry.waiters.set(seq, { resolve, reject, timer });
562
+ send(conn, { type: 'event', id: entry.id, seq, name, args: modelProxyWireValue(args) ?? [] });
563
+ });
564
+ const next = entry.queue.then(run, run);
565
+ entry.queue = next.then(
566
+ () => {},
567
+ () => {}
568
+ );
569
+ return next;
570
+ }
571
+
572
+ function rejectWaiters(entry, reason) {
573
+ const waiters = [...entry.waiters.values()];
574
+ entry.waiters.clear();
575
+ for (const waiter of waiters) {
576
+ clearTimeout(waiter.timer);
577
+ waiter.reject(reason);
578
+ }
579
+ }
580
+
581
+ function abortEntry(entry, reason) {
582
+ if (!entry.controller.signal.aborted) entry.controller.abort(reason);
583
+ rejectWaiters(entry, reason);
584
+ }
585
+
586
+ async function finish(conn, entry, ok, value) {
587
+ // Every relayed callback is answered (or failed) before `done`.
588
+ await entry.queue;
589
+ rejectWaiters(entry, abortError('model call settled'));
590
+ conn.requests.delete(entry.id);
591
+ budgetUsed -= entry.bytes;
592
+ setActive(-1);
593
+ const code = ok ? '' : ` error=${logField(value?.code || value?.name || 'Error')}`;
594
+ log(
595
+ `model-proxy bridge ${logField(entry.method)} id=${logField(entry.id)} provider=${logField(entry.provider)}` +
596
+ ` model=${logField(entry.model)} ${Date.now() - entry.startedAt}ms${code}`
597
+ );
598
+ if (ok) send(conn, { type: 'done', id: entry.id, ok: true, value: modelProxyWireValue(value) ?? null });
599
+ else send(conn, { type: 'done', id: entry.id, ok: false, error: modelProxyErrorToWire(value) });
600
+ drain();
601
+ }
602
+
603
+ function handleResult(conn, frame) {
604
+ const entry = conn.requests.get(frame.id);
605
+ const waiter = entry?.waiters.get(frame.seq);
606
+ if (!waiter) return;
607
+ entry.waiters.delete(frame.seq);
608
+ clearTimeout(waiter.timer);
609
+ if (frame.ok === true) waiter.resolve(frame.value ?? null);
610
+ else waiter.reject(modelProxyErrorFromWire(frame.error));
611
+ }
612
+
613
+ function handleCancel(conn, frame) {
614
+ const reservation = conn.reservations.get(frame.id);
615
+ if (reservation) {
616
+ dropReservation(conn, reservation);
617
+ drain();
618
+ return;
619
+ }
620
+ const entry = conn.requests.get(frame.id);
621
+ if (entry) abortEntry(entry, abortError('the bridge client cancelled the model call'));
622
+ }
623
+
624
+ function sendState(conn) {
625
+ send(conn, { type: 'state', connected: channel.connected, generation: channel.generation });
626
+ }
627
+
628
+ function send(conn, frame) {
629
+ if (conn.closed) return;
630
+ if (conn.socket.writableLength > limits.outgoingQueueBytes) {
631
+ fail(conn, 'MODEL_PROXY_BRIDGE_SLOW_CLIENT');
632
+ return;
633
+ }
634
+ conn.socket.write(`${JSON.stringify(frame)}\n`);
635
+ }
636
+
637
+ function fail(conn, code) {
638
+ if (conn.closed) return;
639
+ conn.socket.write(`${JSON.stringify({ type: 'error', code })}\n`);
640
+ closeConnection(conn, true);
641
+ }
642
+
643
+ /** Never replays: every running call of the connection is aborted upstream. */
644
+ function closeConnection(conn, flush = false) {
645
+ if (conn.closed) return;
646
+ conn.closed = true;
647
+ connections.delete(conn);
648
+ clearTimeout(conn.helloTimer);
649
+ clearTimeout(conn.stallTimer);
650
+ const reason = unavailable('the bridge client disconnected');
651
+ for (const entry of conn.requests.values()) abortEntry(entry, reason);
652
+ for (const reservation of [...conn.reservations.values()]) dropReservation(conn, reservation);
653
+ if (flush) {
654
+ conn.socket.end();
655
+ setTimeout(() => conn.socket.destroy(), 1_000).unref?.();
656
+ } else {
657
+ conn.socket.destroy();
658
+ }
659
+ drain();
660
+ }
661
+
662
+ return {
663
+ /** Listen on 127.0.0.1 (an OS-chosen port); resolves the endpoint to publish. */
664
+ async start() {
665
+ const port = await listener.start();
666
+ return { port, token };
667
+ },
668
+ get activeCount() {
669
+ return active;
670
+ },
671
+ get connectionCount() {
672
+ return connections.size;
673
+ },
674
+ /** Abort every running call (fenced owner, shutdown); new ones are gated by admit(). */
675
+ abortAll(reason = 'the daemon is stopping') {
676
+ const error = unavailable(String(reason));
677
+ for (const conn of connections) {
678
+ for (const entry of conn.requests.values()) abortEntry(entry, error);
679
+ // Reserved calls never start: refused now, not when their frame arrives.
680
+ for (const reservation of [...conn.reservations.values()]) {
681
+ dropReservation(conn, reservation);
682
+ refuse(conn, reservation.id, error);
683
+ }
684
+ }
685
+ drain();
686
+ },
687
+ /** Stop listening, abort every call, close every socket and wait until each call settled. */
688
+ close(reason = 'the daemon is stopping') {
689
+ if (closed) return closed;
690
+ closing = true;
691
+ unsubscribe();
692
+ this.abortAll(reason);
693
+ for (const conn of [...connections]) closeConnection(conn);
694
+ closed = Promise.all([listener.stop(), Promise.allSettled([...settling])]).then(() => {});
695
+ return closed;
696
+ },
697
+ };
698
+ }
699
+
700
+ // ── Child side ──────────────────────────────────────────────────────────────
701
+
702
+ /**
703
+ * The daemon's bridge endpoint from `<runtimeRoot>/daemon.json`. Fails closed
704
+ * with MODEL_PROXY_UNAVAILABLE (and a `reason`) when the file is missing, its
705
+ * daemon is gone, or it publishes no bridge. Never echoes file contents.
706
+ */
707
+ export function readModelProxyBridgeDiscovery(runtimeRoot) {
708
+ const root = String(runtimeRoot || '').trim();
709
+ if (!root) throw unavailable('MIXDOG_RUNTIME_ROOT does not name the daemon runtime root', 'runtime-root-missing');
710
+ let record;
711
+ try {
712
+ record = JSON.parse(readFileSync(join(root, 'daemon.json'), 'utf8'));
713
+ } catch {
714
+ throw unavailable('no readable daemon discovery file in the runtime root', 'discovery-missing');
715
+ }
716
+ if (!isPidAlive(record?.pid)) {
717
+ throw unavailable('the daemon named by the discovery file is not running', 'daemon-not-running');
718
+ }
719
+ const endpoint = record?.endpoints?.modelProxy;
720
+ const port = Number(endpoint?.port);
721
+ if (
722
+ !Number.isInteger(port) ||
723
+ port < 1 ||
724
+ port > 65_535 ||
725
+ typeof endpoint?.token !== 'string' ||
726
+ !TOKEN_RE.test(endpoint.token)
727
+ ) {
728
+ throw unavailable('the daemon publishes no model proxy bridge endpoint', 'endpoint-missing');
729
+ }
730
+ return { port, token: endpoint.token, pid: Number(record.pid) };
731
+ }
732
+
733
+ /**
734
+ * The child half: exactly the channel surface ModelProxyProvider and
735
+ * loadModelProxyProviderConfig use. Connects lazily; `connected` means the
736
+ * bridge is authenticated AND the daemon has a usable PC client. The
737
+ * generation advances on every (re)connect, disconnect and upstream change.
738
+ */
739
+ export class ModelProxyBridgeChannel {
740
+ #host;
741
+ #port;
742
+ #token;
743
+ #connectTimeoutMs;
744
+ #maxFrameBytes;
745
+ #socket = null;
746
+ #lines = null;
747
+ #connecting = null;
748
+ #authenticated = false;
749
+ #upstream = false;
750
+ #upstreamGeneration = null;
751
+ #generation = 0;
752
+ #requests = new Map();
753
+ #nextId = 0;
754
+ #changeWaiters = new Set();
755
+ #disposed = false;
756
+
757
+ constructor({
758
+ port,
759
+ token,
760
+ host = '127.0.0.1',
761
+ connectTimeoutMs = 10_000,
762
+ maxFrameBytes = MODEL_PROXY_BRIDGE_LIMITS.requestFrameMaxBytes,
763
+ } = {}) {
764
+ this.#host = host;
765
+ this.#port = port;
766
+ this.#token = token;
767
+ this.#connectTimeoutMs = connectTimeoutMs;
768
+ this.#maxFrameBytes = maxFrameBytes;
769
+ }
770
+
771
+ get connected() {
772
+ return this.#authenticated && this.#upstream;
773
+ }
774
+
775
+ get generation() {
776
+ return this.#generation;
777
+ }
778
+
779
+ /** Connects if needed; true once the bridge and a PC client are usable, false after `timeoutMs`. */
780
+ async whenConnected(timeoutMs) {
781
+ const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
782
+ for (;;) {
783
+ if (this.connected) return true;
784
+ const remaining = deadline - Date.now();
785
+ if (this.#disposed || remaining <= 0) return false;
786
+ if (this.#authenticated) {
787
+ await this.#waitForChange(remaining);
788
+ continue;
789
+ }
790
+ const failed = await this.#withTimeout(this.#connect(), remaining).then(
791
+ () => false,
792
+ () => true
793
+ );
794
+ if (failed && !this.#authenticated) {
795
+ await this.#waitForChange(Math.min(RECONNECT_DELAY_MS, Math.max(0, deadline - Date.now())));
796
+ }
797
+ }
798
+ }
799
+
800
+ request(call, { signal = null, callbacks = {} } = {}) {
801
+ if (signal?.aborted) return Promise.reject(abortReason(signal));
802
+ if (this.#disposed) return Promise.reject(unavailable('the model proxy bridge was closed'));
803
+ return new Promise((resolve, reject) => {
804
+ this.#nextId += 1;
805
+ const entry = {
806
+ id: `r${this.#nextId}`,
807
+ callbacks,
808
+ resolve,
809
+ reject,
810
+ signal,
811
+ onAbort: null,
812
+ settled: false,
813
+ sent: false,
814
+ queue: Promise.resolve(),
815
+ };
816
+ if (signal) {
817
+ entry.onAbort = () => {
818
+ const wasSent = entry.sent && !entry.settled;
819
+ this.#settle(entry, false, abortReason(signal));
820
+ if (wasSent) this.#write({ type: 'cancel', id: entry.id });
821
+ };
822
+ signal.addEventListener('abort', entry.onAbort, { once: true });
823
+ }
824
+ this.#requests.set(entry.id, entry);
825
+ this.#updateRef();
826
+ this.#connect().then(
827
+ () => this.#send(entry, call),
828
+ (error) => this.#settle(entry, false, error)
829
+ );
830
+ });
831
+ }
832
+
833
+ /** Close for good: outstanding calls fail, later ones fail fast. */
834
+ dispose(reason = 'the model proxy bridge was closed') {
835
+ if (this.#disposed) return;
836
+ this.#disposed = true;
837
+ const error = unavailable(reason);
838
+ if (this.#socket) this.#drop(this.#socket, error);
839
+ for (const entry of [...this.#requests.values()]) this.#settle(entry, false, error);
840
+ this.#notifyChange();
841
+ }
842
+
843
+ #send(entry, call) {
844
+ if (entry.settled) return;
845
+ if (!this.#upstream) {
846
+ this.#settle(entry, false, unavailable('no desktop client is connected to run model calls'));
847
+ return;
848
+ }
849
+ let line;
850
+ try {
851
+ // Built in this order so a large frame starts with the `req` prefix the
852
+ // daemon checks before buffering it.
853
+ line = `{"type":"req","id":${JSON.stringify(entry.id)},"callbacks":${JSON.stringify(
854
+ Object.keys(entry.callbacks)
855
+ )},"call":${encodeModelProxyWire(call)}}`;
856
+ } catch (error) {
857
+ this.#settle(entry, false, error);
858
+ return;
859
+ }
860
+ const bytes = Buffer.byteLength(line) + 1;
861
+ if (bytes > this.#maxFrameBytes) {
862
+ this.#settle(
863
+ entry,
864
+ false,
865
+ bridgeError('MODEL_PROXY_REQUEST_TOO_LARGE', 'the model call exceeds the bridge limit')
866
+ );
867
+ return;
868
+ }
869
+ // The frame follows its grant; a cancel from here on also withdraws the reservation.
870
+ entry.line = line;
871
+ entry.sent = true;
872
+ this.#socket.write(`${JSON.stringify({ type: 'reserve', id: entry.id, bytes })}\n`);
873
+ }
874
+
875
+ #handleGrant(socket, frame) {
876
+ const entry = this.#requests.get(String(frame.id));
877
+ if (!entry?.line) return;
878
+ const line = entry.line;
879
+ entry.line = null;
880
+ socket.write(`${line}\n`);
881
+ }
882
+
883
+ #connect() {
884
+ if (this.#disposed) return Promise.reject(unavailable('the model proxy bridge was closed'));
885
+ if (this.#authenticated) return Promise.resolve();
886
+ if (this.#connecting) return this.#connecting.promise;
887
+ const attempt = Promise.withResolvers();
888
+ attempt.promise.catch(() => {});
889
+ this.#connecting = attempt;
890
+ const socket = net.connect({ host: this.#host, port: this.#port });
891
+ this.#socket = socket;
892
+ this.#lines = new LineBuffer();
893
+ socket.setNoDelay(true);
894
+ attempt.timer = setTimeout(
895
+ () => this.#drop(socket, unavailable('the model proxy bridge did not answer')),
896
+ this.#connectTimeoutMs
897
+ );
898
+ socket.on('connect', () => {
899
+ socket.write(`${JSON.stringify({ type: 'hello', v: MODEL_PROXY_BRIDGE_PROTOCOL, token: this.#token })}\n`);
900
+ });
901
+ socket.on('data', (chunk) => this.#onData(socket, chunk));
902
+ socket.on('error', () => {});
903
+ socket.on('close', () => this.#drop(socket, unavailable('the model proxy bridge disconnected')));
904
+ this.#updateRef();
905
+ return attempt.promise;
906
+ }
907
+
908
+ #onData(socket, chunk) {
909
+ if (socket !== this.#socket) return;
910
+ const max = () => this.#maxFrameBytes;
911
+ const withinLimits = this.#lines.push(chunk, { min: max, forHead: max }, (line) => {
912
+ let frame;
913
+ try {
914
+ frame = decodeModelProxyWire(line.toString('utf8'));
915
+ } catch {
916
+ this.#drop(socket, unavailable('the model proxy bridge sent a malformed frame'));
917
+ return false;
918
+ }
919
+ if (isPlainObject(frame)) this.#handleFrame(socket, frame);
920
+ return socket === this.#socket;
921
+ });
922
+ if (!withinLimits && socket === this.#socket) {
923
+ this.#drop(socket, unavailable('the model proxy bridge sent an oversized frame'));
924
+ }
925
+ }
926
+
927
+ #handleFrame(socket, frame) {
928
+ switch (frame.type) {
929
+ case 'state':
930
+ this.#handleState(frame);
931
+ return;
932
+ case 'grant':
933
+ this.#handleGrant(socket, frame);
934
+ return;
935
+ case 'event':
936
+ this.#handleEvent(frame);
937
+ return;
938
+ case 'done': {
939
+ const entry = this.#requests.get(String(frame.id));
940
+ if (!entry) return;
941
+ const ok = frame.ok === true;
942
+ const value = ok ? (frame.value ?? null) : modelProxyErrorFromWire(frame.error);
943
+ entry.queue.then(() => this.#settle(entry, ok, value));
944
+ return;
945
+ }
946
+ case 'error':
947
+ this.#drop(socket, unavailable(`the model proxy bridge refused the connection (${logField(frame.code)})`));
948
+ return;
949
+ default:
950
+ }
951
+ }
952
+
953
+ #handleState(frame) {
954
+ const connected = frame.connected === true;
955
+ const generation = frame.generation;
956
+ if (!this.#authenticated) {
957
+ this.#authenticated = true;
958
+ this.#upstream = connected;
959
+ this.#upstreamGeneration = generation;
960
+ this.#generation += 1;
961
+ const attempt = this.#connecting;
962
+ this.#connecting = null;
963
+ clearTimeout(attempt?.timer);
964
+ attempt?.resolve();
965
+ this.#updateRef();
966
+ this.#notifyChange();
967
+ return;
968
+ }
969
+ if (connected === this.#upstream && generation === this.#upstreamGeneration) return;
970
+ this.#upstream = connected;
971
+ this.#upstreamGeneration = generation;
972
+ this.#generation += 1;
973
+ this.#notifyChange();
974
+ }
975
+
976
+ #handleEvent(frame) {
977
+ const entry = this.#requests.get(String(frame.id));
978
+ const seq = frame.seq;
979
+ if (!entry) {
980
+ // Given up here: answer at once so the daemon never waits out the ack.
981
+ this.#write({
982
+ type: 'result',
983
+ id: frame.id,
984
+ seq,
985
+ ok: false,
986
+ error: modelProxyErrorToWire(abortError('model call aborted')),
987
+ });
988
+ return;
989
+ }
990
+ const run = async () => {
991
+ const callback = entry.callbacks[String(frame.name || '')];
992
+ let reply;
993
+ try {
994
+ const args = Array.isArray(frame.args) ? frame.args : [];
995
+ const value = typeof callback === 'function' ? await callback(...args) : null;
996
+ reply = { type: 'result', id: entry.id, seq, ok: true, value: modelProxyWireValue(value) ?? null };
997
+ } catch (error) {
998
+ reply = { type: 'result', id: entry.id, seq, ok: false, error: modelProxyErrorToWire(error) };
999
+ }
1000
+ if (Buffer.byteLength(JSON.stringify(reply)) + 1 > MODEL_PROXY_BRIDGE_LIMITS.controlFrameMaxBytes) {
1001
+ const error = bridgeError('MODEL_PROXY_BRIDGE_RESULT_TOO_LARGE', 'a callback result exceeds the bridge limit');
1002
+ reply = { type: 'result', id: entry.id, seq, ok: false, error: modelProxyErrorToWire(error) };
1003
+ }
1004
+ this.#write(reply);
1005
+ };
1006
+ entry.queue = entry.queue.then(run, run);
1007
+ }
1008
+
1009
+ #write(frame) {
1010
+ if (!this.#socket || !this.#authenticated) return;
1011
+ this.#socket.write(`${JSON.stringify(frame)}\n`);
1012
+ }
1013
+
1014
+ #settle(entry, ok, value) {
1015
+ if (entry.settled) return;
1016
+ entry.settled = true;
1017
+ entry.line = null;
1018
+ this.#requests.delete(entry.id);
1019
+ if (entry.onAbort) entry.signal.removeEventListener('abort', entry.onAbort);
1020
+ entry.callbacks = {};
1021
+ this.#updateRef();
1022
+ if (ok) entry.resolve(value);
1023
+ else entry.reject(value);
1024
+ }
1025
+
1026
+ #drop(socket, error) {
1027
+ if (socket !== this.#socket) return;
1028
+ this.#socket = null;
1029
+ this.#lines = null;
1030
+ socket.destroy();
1031
+ const wasAuthenticated = this.#authenticated;
1032
+ this.#authenticated = false;
1033
+ this.#upstream = false;
1034
+ this.#upstreamGeneration = null;
1035
+ if (wasAuthenticated) this.#generation += 1;
1036
+ const attempt = this.#connecting;
1037
+ this.#connecting = null;
1038
+ if (attempt) {
1039
+ clearTimeout(attempt.timer);
1040
+ attempt.reject(error);
1041
+ }
1042
+ for (const entry of [...this.#requests.values()]) this.#settle(entry, false, error);
1043
+ this.#notifyChange();
1044
+ }
1045
+
1046
+ /** The socket keeps the process alive only while something waits on it. */
1047
+ #updateRef() {
1048
+ if (!this.#socket) return;
1049
+ if (this.#connecting || this.#requests.size || this.#changeWaiters.size) this.#socket.ref();
1050
+ else this.#socket.unref();
1051
+ }
1052
+
1053
+ #waitForChange(ms) {
1054
+ return new Promise((resolve) => {
1055
+ const waiter = () => {
1056
+ clearTimeout(timer);
1057
+ this.#changeWaiters.delete(waiter);
1058
+ this.#updateRef();
1059
+ resolve();
1060
+ };
1061
+ const timer = setTimeout(waiter, ms);
1062
+ this.#changeWaiters.add(waiter);
1063
+ this.#updateRef();
1064
+ });
1065
+ }
1066
+
1067
+ #notifyChange() {
1068
+ for (const waiter of [...this.#changeWaiters]) waiter();
1069
+ }
1070
+
1071
+ #withTimeout(promise, ms) {
1072
+ let timer;
1073
+ return Promise.race([
1074
+ promise,
1075
+ new Promise((_, reject) => {
1076
+ timer = setTimeout(() => reject(unavailable('the model proxy bridge did not answer in time')), ms);
1077
+ }),
1078
+ ]).finally(() => clearTimeout(timer));
1079
+ }
1080
+ }