mixdog 1.3.1 → 1.3.3

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 (468) hide show
  1. package/README.md +8 -4
  2. package/package.json +8 -8
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  5. package/src/agents/reviewer/AGENT.md +5 -7
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +9 -1
  8. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  9. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  10. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  11. package/src/defaults/skills/setup/SKILL.md +23 -1
  12. package/src/defaults/skills/setup/references/actions.md +50 -3
  13. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  14. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  15. package/src/defaults/skills/visualize/SKILL.md +9 -6
  16. package/src/defaults/skills/visualize/references/craft.md +8 -1
  17. package/src/help.mjs +1 -1
  18. package/src/lib/keychain-cjs.cjs +4 -1
  19. package/src/lib/rules-builder.cjs +14 -7
  20. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  21. package/src/rules/agent-mode/MIXY.md +6 -8
  22. package/src/rules/lead/LEAD.md +7 -10
  23. package/src/rules/shared/10-tool-workflow.md +4 -0
  24. package/src/rules/shared/20-research.md +2 -0
  25. package/src/rules/shared/45-destructive.md +5 -1
  26. package/src/rules/shared/60-verification.md +6 -4
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  30. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  31. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  37. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  38. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  39. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  40. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  41. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  42. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +84 -32
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  45. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +84 -42
  48. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  50. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  51. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  52. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  54. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  55. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  56. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  57. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  62. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  64. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  65. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  69. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  70. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  72. package/src/runtime/agent/orchestrator/providers/registry.mjs +19 -0
  73. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  74. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  75. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  78. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  79. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  81. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  82. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  83. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +52 -6
  85. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +35 -13
  86. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  87. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +8 -3
  90. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  92. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  93. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  94. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -2
  95. package/src/runtime/agent/orchestrator/session/agent-room.mjs +196 -45
  96. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  97. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  98. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  99. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +45 -19
  100. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  102. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +185 -37
  103. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +6 -2
  107. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +4 -1
  108. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  110. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +240 -0
  111. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +102 -35
  112. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  113. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +19 -9
  114. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  115. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  116. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  117. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  118. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +199 -109
  119. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  120. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  123. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  124. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  125. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  126. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -0
  127. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  128. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +33 -8
  129. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  130. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +27 -0
  131. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  134. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  135. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  136. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  137. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  138. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +27 -14
  139. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +14 -0
  140. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  141. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  142. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  143. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  144. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +115 -0
  145. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  146. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  147. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  148. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  149. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  150. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  151. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  152. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  153. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  154. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  155. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  156. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +4 -9
  157. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +14 -1
  158. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  159. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  160. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  161. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  162. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +52 -11
  163. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  165. package/src/runtime/agent/orchestrator/session/routine-result.mjs +126 -12
  166. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  167. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +16 -3
  168. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  169. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  170. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  171. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  172. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  173. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  174. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  175. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  176. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +15 -5
  177. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +49 -1
  178. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +80 -1
  179. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  180. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  181. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  182. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  183. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  184. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  185. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  186. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  187. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  188. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  189. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  190. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +47 -14
  191. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  192. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +6 -3
  193. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  194. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  195. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  196. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  197. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  198. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  199. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +10 -12
  200. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  201. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  202. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  203. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  204. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  205. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +42 -23
  206. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  207. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  208. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  209. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  210. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  211. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  212. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  213. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  214. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  215. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  216. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  217. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  218. package/src/runtime/attachments/legacy-office.mjs +110 -0
  219. package/src/runtime/attachments/limits.mjs +5 -1
  220. package/src/runtime/attachments/office-extract.mjs +3 -6
  221. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  222. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  223. package/src/runtime/attachments/store.mjs +109 -33
  224. package/src/runtime/browser-bridge/client.mjs +73 -20
  225. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  226. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  227. package/src/runtime/channels/lib/state-file.mjs +15 -15
  228. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  229. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  230. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  231. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  232. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  233. package/src/runtime/effort-judge/judge-client.mjs +66 -16
  234. package/src/runtime/effort-judge/model-install.mjs +47 -7
  235. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  236. package/src/runtime/local-provider/catalog.mjs +5 -2
  237. package/src/runtime/local-provider/registered-models.mjs +64 -27
  238. package/src/runtime/local-provider/server.mjs +2 -3
  239. package/src/runtime/media/adapters/image-api.mjs +89 -0
  240. package/src/runtime/media/auth.mjs +64 -1
  241. package/src/runtime/media/catalog.mjs +66 -4
  242. package/src/runtime/media/jobs.mjs +4 -0
  243. package/src/runtime/media/lanes.mjs +46 -5
  244. package/src/runtime/media/media-usage.mjs +17 -0
  245. package/src/runtime/media/renditions.mjs +15 -15
  246. package/src/runtime/media/store.mjs +36 -35
  247. package/src/runtime/media/tool.mjs +8 -1
  248. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  249. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  250. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  251. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  252. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  253. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  254. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  255. package/src/runtime/office/core/journal.mjs +7 -14
  256. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  257. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  258. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  259. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  260. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  261. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  262. package/src/runtime/office/portable/portable-soffice.mjs +135 -29
  263. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  264. package/src/runtime/shared/agent-avatar.mjs +3 -2
  265. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  266. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  267. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  268. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  269. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  270. package/src/runtime/shared/agent-room-host.mjs +58 -0
  271. package/src/runtime/shared/agent-store.mjs +24 -7
  272. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  273. package/src/runtime/shared/atomic-file.d.mts +50 -0
  274. package/src/runtime/shared/atomic-file.mjs +394 -132
  275. package/src/runtime/shared/background-tasks.mjs +7 -4
  276. package/src/runtime/shared/bot-hops.mjs +35 -0
  277. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  278. package/src/runtime/shared/bounded-download.mjs +4 -2
  279. package/src/runtime/shared/config.mjs +39 -9
  280. package/src/runtime/shared/defaults-separation.mjs +6 -2
  281. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  282. package/src/runtime/shared/err-text.mjs +4 -0
  283. package/src/runtime/shared/error-presentation.mjs +22 -4
  284. package/src/runtime/shared/file-has-data.mjs +28 -0
  285. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  286. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  287. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  288. package/src/runtime/shared/projects.mjs +50 -41
  289. package/src/runtime/shared/provider-accounts.mjs +17 -12
  290. package/src/runtime/shared/redact-directories.mjs +18 -0
  291. package/src/runtime/shared/remove-directory.mjs +28 -0
  292. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  293. package/src/runtime/shared/routine-trigger.mjs +14 -6
  294. package/src/runtime/shared/run-child.d.mts +3 -0
  295. package/src/runtime/shared/run-child.mjs +24 -3
  296. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  297. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  298. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  299. package/src/runtime/shared/singleton-owner.mjs +7 -6
  300. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  301. package/src/runtime/shared/staged-update.mjs +4 -2
  302. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  303. package/src/runtime/shared/tool-execution-contract.mjs +10 -0
  304. package/src/runtime/shared/tool-work-units.mjs +12 -9
  305. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  306. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  307. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  308. package/src/runtime/shared/turn-snapshot.mjs +277 -124
  309. package/src/runtime/shared/user-cwd.mjs +9 -4
  310. package/src/runtime/shared/user-data-guard.mjs +92 -61
  311. package/src/runtime/shared/utf8-cut.mjs +10 -0
  312. package/src/runtime/shared/worktrees/activity-gate.mjs +251 -61
  313. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  314. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  315. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  316. package/src/runtime/shared/worktrees/registry.mjs +29 -27
  317. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  318. package/src/runtime/tidy/core-install.mjs +26 -4
  319. package/src/runtime/tidy/install.mjs +91 -23
  320. package/src/runtime/tidy/process.mjs +41 -8
  321. package/src/runtime/tidy/resolve.mjs +10 -7
  322. package/src/runtime/web-search/lib/config.mjs +4 -16
  323. package/src/session-runtime/agent-home-delivery.mjs +542 -204
  324. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  325. package/src/session-runtime/agent-room-delivery.mjs +81 -35
  326. package/src/session-runtime/agent-task-sessions.mjs +811 -179
  327. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  328. package/src/session-runtime/boot/apis.mjs +48 -24
  329. package/src/session-runtime/boot/config.mjs +1 -0
  330. package/src/session-runtime/boot/facade.mjs +16 -7
  331. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  332. package/src/session-runtime/boot/providers.mjs +2 -2
  333. package/src/session-runtime/boot/shared.mjs +12 -6
  334. package/src/session-runtime/boot/tools.mjs +23 -3
  335. package/src/session-runtime/bot-status.mjs +432 -0
  336. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  337. package/src/session-runtime/config-lifecycle.mjs +60 -55
  338. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  339. package/src/session-runtime/held-requests.mjs +31 -33
  340. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  341. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  342. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +24 -26
  343. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  344. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  345. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  346. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  347. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  348. package/src/session-runtime/media-api.mjs +5 -2
  349. package/src/session-runtime/model-recency.mjs +1 -0
  350. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  351. package/src/session-runtime/model-route/set-route.mjs +61 -1
  352. package/src/session-runtime/owned-automation-run.mjs +8 -1
  353. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  354. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  355. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  356. package/src/session-runtime/route-resolution.mjs +16 -8
  357. package/src/session-runtime/route-state.mjs +1 -59
  358. package/src/session-runtime/routine-home-turn.mjs +5 -3
  359. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  360. package/src/session-runtime/runtime-paths.mjs +11 -5
  361. package/src/session-runtime/runtime-review-api.mjs +4 -1
  362. package/src/session-runtime/runtime-tunables.mjs +6 -0
  363. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  364. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  365. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  366. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  367. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
  368. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  369. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  370. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +7 -0
  371. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  372. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  373. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  374. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  375. package/src/session-runtime/services/agent-tool.mjs +6 -0
  376. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  377. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  378. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  379. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  380. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  381. package/src/session-runtime/services/provider-admin.mjs +26 -6
  382. package/src/session-runtime/services/schedule-access.mjs +33 -4
  383. package/src/session-runtime/services/seeds.mjs +4 -3
  384. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  385. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  386. package/src/session-runtime/services/worktree-access.mjs +27 -0
  387. package/src/session-runtime/session-create/materialize.mjs +2 -3
  388. package/src/session-runtime/session-create/session-options.mjs +15 -2
  389. package/src/session-runtime/session-hooks.mjs +9 -17
  390. package/src/session-runtime/settings-builtin-tools-api.mjs +65 -1
  391. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  392. package/src/session-runtime/setup-tool/executor.mjs +12 -2
  393. package/src/session-runtime/setup-tool/settings-contract.mjs +34 -1
  394. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  395. package/src/session-runtime/skills-api.mjs +12 -5
  396. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +349 -0
  397. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  398. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  399. package/src/session-runtime/tool-policy/policy-stage.mjs +183 -0
  400. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  401. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  402. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  403. package/src/session-runtime/tool-policy-refresh.mjs +48 -369
  404. package/src/session-runtime/tool-surface.mjs +10 -4
  405. package/src/session-runtime/turn/session-ops.mjs +8 -0
  406. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  407. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  408. package/src/session-runtime/turn/turn-run.mjs +7 -2
  409. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  410. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +527 -333
  411. package/src/session-runtime/workflow-agents-api/agent-route.mjs +85 -63
  412. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  413. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  414. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  415. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  416. package/src/session-runtime/workspace/transitions.mjs +279 -67
  417. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  418. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  419. package/src/standalone/daemon-agent-control.mjs +4 -1
  420. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  421. package/src/standalone/daemon-session-control.mjs +173 -15
  422. package/src/standalone/daemon-telemetry.mjs +9 -0
  423. package/src/standalone/daemon.mjs +33 -10
  424. package/src/standalone/local-session-runtime.mjs +33 -16
  425. package/src/standalone/session-client.mjs +7 -2
  426. package/src/standalone/session-protocol.mjs +8 -0
  427. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  428. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  429. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  430. package/src/standalone/session-service.mjs +21 -0
  431. package/src/standalone/session-state-projection.mjs +19 -9
  432. package/src/standalone/session-tool-output.mjs +36 -9
  433. package/src/standalone/staged-import.mjs +47 -0
  434. package/src/tui/App.jsx +2 -0
  435. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  436. package/src/tui/app/message-selector.mjs +15 -2
  437. package/src/tui/app/model-options.mjs +16 -2
  438. package/src/tui/app/model-picker.mjs +2 -1
  439. package/src/tui/app/route-pickers.mjs +3 -1
  440. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  441. package/src/tui/app/slash-commands.mjs +1 -1
  442. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  443. package/src/tui/dist/index.mjs +617 -369
  444. package/src/tui/paste-attachments.mjs +0 -1
  445. package/src/tui/paste-text-policy.mjs +4 -5
  446. package/src/tui/prompt-history-store.mjs +16 -21
  447. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  448. package/src/tui/session/context-state.mjs +4 -0
  449. package/src/tui/session/notice-surface.mjs +3 -0
  450. package/src/tui/session/session-api/intake/abort.mjs +52 -18
  451. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  452. package/src/tui/session/session-api/media.mjs +2 -1
  453. package/src/tui/session/session-api/routes-agent-definitions.mjs +76 -0
  454. package/src/tui/session/session-api/routes-write.mjs +203 -0
  455. package/src/tui/session/session-api/routes.mjs +23 -247
  456. package/src/tui/session/session-api/settings.mjs +14 -1
  457. package/src/tui/session/session-flow/drain.mjs +28 -3
  458. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  459. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  460. package/src/tui/session/session-flow/steering.mjs +8 -3
  461. package/src/tui/session/tool-approval.mjs +142 -14
  462. package/src/tui/session/transcript-intake.mjs +2 -0
  463. package/src/tui/session/turn.mjs +23 -14
  464. package/src/tui/transcript-tool-failures.mjs +8 -2
  465. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  466. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  467. package/src/rules/agent-mode/ROUTINE.md +0 -6
  468. package/src/session-runtime/room-member-gate.mjs +0 -48
@@ -6,11 +6,26 @@
6
6
  */
7
7
  import { createHash } from 'node:crypto';
8
8
  import { makeModelCache } from '../agent/orchestrator/providers/model-cache.mjs';
9
- import { resolveAntigravityAuth, resolveGeminiKey, resolveXaiAuth } from './auth.mjs';
9
+ import {
10
+ imageApiLane,
11
+ resolveAntigravityAuth,
12
+ resolveGeminiKey,
13
+ resolveImageApiAuth,
14
+ resolveXaiAuth,
15
+ } from './auth.mjs';
10
16
  import { catalogHttpError, staleCatalog } from './catalog-errors.mjs';
11
17
 
12
18
  const TTL_MS = 24 * 60 * 60_000;
13
19
  const REQUEST_MS = 10_000;
20
+ // Gateway image catalogs can sit behind a slow edge: api.nano-gpt.com's
21
+ // /models answered only after ~44 s, so the 10 s default would read a slow
22
+ // catalog as an outage. Same budget as custom-provider discovery.
23
+ const GATEWAY_CATALOG_MS = 60_000;
24
+
25
+ /** Per-lane timeout of one catalog request. */
26
+ export function mediaCatalogTimeoutMs(lane) {
27
+ return imageApiLane(lane) ? GATEWAY_CATALOG_MS : REQUEST_MS;
28
+ }
14
29
  const natural = new Intl.Collator('en', { numeric: true });
15
30
 
16
31
  function methodsAllow(row, method) {
@@ -39,8 +54,51 @@ function grokLane(lane) {
39
54
  return lane === 'xai' || lane === 'grok-oauth';
40
55
  }
41
56
 
57
+ // Image API catalogs (OpenRouter, NanoGPT) describe each model with typed
58
+ // capability descriptors; only enum values become pickable controls.
59
+ const IMAGE_API_CONTROLS = Object.freeze({
60
+ aspect_ratio: 'aspectRatio',
61
+ resolution: 'resolution',
62
+ quality: 'quality',
63
+ background: 'background',
64
+ });
65
+ // The request schema's documented ceiling for `input_references`.
66
+ const IMAGE_API_MAX_REFERENCES = 16;
67
+
68
+ function imageApiOutputsImage(row) {
69
+ const outputs = row.architecture?.output_modalities;
70
+ // Image-to-image-only models (upscalers, editors) need an input image;
71
+ // Studio also runs prompt-only, so they stay out.
72
+ return Array.isArray(outputs) && outputs.includes('image') && row.capabilities?.image_generation !== false;
73
+ }
74
+
75
+ function imageApiReferenceCap(row) {
76
+ const descriptor = row.supported_parameters?.input_references;
77
+ if (descriptor?.type === 'range' && Number.isFinite(Number(descriptor.max))) {
78
+ return Math.max(0, Math.min(IMAGE_API_MAX_REFERENCES, Math.trunc(Number(descriptor.max))));
79
+ }
80
+ const inputs = row.architecture?.input_modalities;
81
+ const acceptsImages = Array.isArray(inputs) && inputs.includes('image') && row.capabilities?.image_to_image !== false;
82
+ return acceptsImages ? IMAGE_API_MAX_REFERENCES : 0;
83
+ }
84
+
85
+ function imageApiControls(row) {
86
+ const controls = {};
87
+ for (const [param, control] of Object.entries(IMAGE_API_CONTROLS)) {
88
+ const descriptor = row.supported_parameters?.[param];
89
+ const values =
90
+ descriptor?.type === 'enum' && Array.isArray(descriptor.values)
91
+ ? [...new Set(descriptor.values.map((value) => String(value).trim()).filter(Boolean))]
92
+ : [];
93
+ if (values.length) controls[control] = values;
94
+ }
95
+ controls.maxReferences = imageApiReferenceCap(row);
96
+ return controls;
97
+ }
98
+
42
99
  function kindFor(lane, row, id) {
43
100
  if (row.deprecated === true || row.disabled === true) return null;
101
+ if (imageApiLane(lane)) return imageApiOutputsImage(row) ? 'image' : null;
44
102
  if (lane === 'openai-oauth') return imageToolSupported(row, id) ? 'image' : null;
45
103
  if (grokLane(lane)) {
46
104
  if (/^grok-imagine-image(?:-|$)/.test(id)) return 'image';
@@ -118,7 +176,9 @@ export function projectMediaModels(lane, rows) {
118
176
  const kind = kindFor(lane, row, id);
119
177
  if (!kind) continue;
120
178
  let controls = {};
121
- if (grokLane(lane) && kind === 'video') {
179
+ if (imageApiLane(lane)) {
180
+ controls = imageApiControls(row);
181
+ } else if (grokLane(lane) && kind === 'video') {
122
182
  controls = { resolution: ['480p', '720p'] };
123
183
  } else if (lane === 'gemini' && /^gemini-omni-/.test(id)) {
124
184
  controls = { resolution: [], durations: [], maxReferences: 3 };
@@ -181,6 +241,7 @@ async function providerSource(lane) {
181
241
  let auth;
182
242
  if (lane === 'gemini') auth = { token: resolveGeminiKey() };
183
243
  else if (lane === 'antigravity-oauth') auth = await resolveAntigravityAuth();
244
+ else if (imageApiLane(lane)) auth = resolveImageApiAuth(lane);
184
245
  else auth = await resolveXaiAuth(lane);
185
246
  // API-key and OAuth catalogs must not leak availability across credentials.
186
247
  // Persist only a one-way scope hash, never a key or a bearer. OAuth bearers
@@ -200,7 +261,7 @@ async function providerSource(lane) {
200
261
  }
201
262
 
202
263
  export async function fetchMediaModelRows({ lane, auth, fetchFn = fetch }) {
203
- const signal = AbortSignal.timeout(REQUEST_MS);
264
+ const signal = AbortSignal.timeout(mediaCatalogTimeoutMs(lane));
204
265
  if (lane === 'gemini') {
205
266
  const { fetchGeminiModelPages } = await import('../agent/orchestrator/providers/lib/gemini-model-catalog.mjs');
206
267
  return fetchGeminiModelPages(auth.token, async (url, init) => {
@@ -217,7 +278,8 @@ export async function fetchMediaModelRows({ lane, auth, fetchFn = fetch }) {
217
278
  return Object.entries(models).map(([id, row]) => ({ ...(row && typeof row === 'object' ? row : {}), id }));
218
279
  }
219
280
  const { getLlmDispatcher } = await import('../shared/llm/http-agent.mjs');
220
- const response = await fetchFn(`${auth.baseURL}/models`, {
281
+ // The Image API lists its generation models apart from the chat catalog.
282
+ const response = await fetchFn(`${auth.baseURL}/${imageApiLane(lane) ? 'images/models' : 'models'}`, {
221
283
  headers: { Authorization: `Bearer ${auth.token}` },
222
284
  redirect: 'error',
223
285
  signal,
@@ -60,6 +60,10 @@ async function runAdapter({ lane, kind, model, requestModel, prompt, options, re
60
60
  const adapter = await import('./adapters/codex-image.mjs');
61
61
  return await adapter.generateImage({ model: requestModel || model, prompt, options, references, signal });
62
62
  }
63
+ if (lane.id === 'openrouter' || lane.id === 'nano-gpt') {
64
+ const adapter = await import('./adapters/image-api.mjs');
65
+ return await adapter.generateImage({ lane: lane.id, model, prompt, options, references, signal });
66
+ }
63
67
  if (lane.id === 'antigravity-oauth') {
64
68
  const adapter = await import('./adapters/antigravity-image.mjs');
65
69
  return await adapter.generateImage({ model, prompt, options, references, signal });
@@ -16,6 +16,7 @@ import {
16
16
  hasGrokOAuthCredentials,
17
17
  hasOpenAIOAuthCredentials,
18
18
  } from '../agent/orchestrator/providers/oauth-credential-probes.mjs';
19
+ import { hasImageApiCredentials, imageApiLane } from './auth.mjs';
19
20
  import { loadMediaModels, projectMediaModels } from './catalog.mjs';
20
21
  import { catalogDiagnostic } from './catalog-errors.mjs';
21
22
  import { MEDIA_KINDS } from './tool-defs.mjs';
@@ -98,6 +99,20 @@ const LANES = Object.freeze([
98
99
  controls: GEMINI_IMAGE_CONTROLS,
99
100
  }),
100
101
  }),
102
+ // Gateway lanes on the normalized Image API: every model advertises its own
103
+ // parameters in the catalog, so the lane declares no controls of its own.
104
+ Object.freeze({
105
+ id: 'openrouter',
106
+ label: 'OpenRouter',
107
+ auth: Object.freeze({ type: 'api-key', provider: 'openrouter' }),
108
+ image: Object.freeze({ controls: Object.freeze({}) }),
109
+ }),
110
+ Object.freeze({
111
+ id: 'nano-gpt',
112
+ label: 'NanoGPT',
113
+ auth: Object.freeze({ type: 'api-key', provider: 'nano-gpt' }),
114
+ image: Object.freeze({ controls: Object.freeze({}) }),
115
+ }),
101
116
  ]);
102
117
 
103
118
  const LANE_BY_ID = new Map(LANES.map((lane) => [lane.id, lane]));
@@ -117,6 +132,7 @@ const OAUTH_PROBES = Object.freeze({
117
132
 
118
133
  function laneAuthenticated(lane) {
119
134
  try {
135
+ if (imageApiLane(lane.id)) return hasImageApiCredentials(lane.id);
120
136
  if (lane.auth.type === 'api-key') return hasAgentApiKey(lane.auth.provider);
121
137
  return OAUTH_PROBES[lane.auth.provider]?.() === true;
122
138
  } catch {
@@ -143,8 +159,23 @@ function laneKindView(lane, kind, models) {
143
159
  };
144
160
  }
145
161
 
162
+ const PENDING = Symbol('catalog-pending');
163
+
164
+ /** The catalog rows, or PENDING when they did not arrive within `budgetMs`
165
+ * (0 = wait). The request keeps running; the loader's singleflight lets the
166
+ * next listing join it instead of starting over. */
167
+ async function withinBudget(loading, budgetMs) {
168
+ if (!(budgetMs > 0)) return loading;
169
+ loading.catch(() => {});
170
+ let timer;
171
+ const expired = new Promise((resolve) => {
172
+ timer = setTimeout(resolve, budgetMs, PENDING);
173
+ });
174
+ return Promise.race([loading, expired]).finally(() => clearTimeout(timer));
175
+ }
176
+
146
177
  export function createMediaLaneCatalog({ loadModels = loadMediaModels, authenticated = laneAuthenticated } = {}) {
147
- async function laneView(lane) {
178
+ async function laneView(lane, budgetMs = 0) {
148
179
  const signedIn = authenticated(lane);
149
180
  const view = {
150
181
  id: lane.id,
@@ -161,7 +192,15 @@ export function createMediaLaneCatalog({ loadModels = loadMediaModels, authentic
161
192
  };
162
193
  if (!signedIn) return view;
163
194
  try {
164
- const rows = await loadModels(lane.id);
195
+ const rows = await withinBudget(
196
+ Promise.resolve().then(() => loadModels(lane.id)),
197
+ budgetMs
198
+ );
199
+ if (rows === PENDING) {
200
+ // Still loading, not empty: the caller shows that and asks again.
201
+ view.catalogPending = true;
202
+ return view;
203
+ }
165
204
  const models = projectMediaModels(lane.id, rows);
166
205
  if (rows.catalogWarning) view.catalogWarning = rows.catalogWarning;
167
206
  for (const kind of MEDIA_KINDS) {
@@ -187,9 +226,11 @@ export function createMediaLaneCatalog({ loadModels = loadMediaModels, authentic
187
226
  }
188
227
 
189
228
  return {
190
- /** Live catalog shared by Studio and the model-facing media tool. */
191
- async listMediaLanes() {
192
- return (await Promise.all(LANES.map(laneView))).filter(Boolean);
229
+ /** Live catalog shared by Studio and the model-facing media tool.
230
+ * `budgetMs` bounds the wait per lane: a lane whose catalog is slower
231
+ * answers `catalogPending` instead of holding every other lane. */
232
+ async listMediaLanes({ budgetMs = 0 } = {}) {
233
+ return (await Promise.all(LANES.map((lane) => laneView(lane, budgetMs)))).filter(Boolean);
193
234
  },
194
235
  async resolveMediaRequest({ lane: laneId, kind, model } = {}) {
195
236
  const kindName = String(kind || '').trim();
@@ -73,6 +73,23 @@ export function xaiUsage(usage) {
73
73
  return typeof ticks === 'number' && Number.isFinite(ticks) && ticks >= 0 ? { costUsd: ticks / TICKS_PER_USD } : null;
74
74
  }
75
75
 
76
+ /**
77
+ * Image API response usage (OpenRouter / NanoGPT `POST /images`). `usage.cost`
78
+ * is USD; NanoGPT's top-level `cost` counts only when `paymentSource` says USD
79
+ * (it may be billed in XNO).
80
+ */
81
+ export function imageApiUsage(body) {
82
+ const usage = body?.usage && typeof body.usage === 'object' ? body.usage : {};
83
+ const tokens = tokenUsage({
84
+ inputTokens: count(usage.prompt_tokens),
85
+ outputTokens: count(usage.completion_tokens),
86
+ outputImageTokens: count(usage.completion_tokens_details?.image_tokens),
87
+ });
88
+ const usd = String(body?.paymentSource || '').toUpperCase() === 'USD' ? body?.cost : undefined;
89
+ const cost = [usage.cost, usd].find((value) => typeof value === 'number' && Number.isFinite(value) && value >= 0);
90
+ return cost === undefined ? tokens : { ...tokens, costUsd: cost };
91
+ }
92
+
76
93
  /** True when the provider reported something billable for the request. */
77
94
  export function hasReportedUsage(usage) {
78
95
  if (!usage) return false;
@@ -14,10 +14,11 @@
14
14
  * what made the remote gallery slow.
15
15
  */
16
16
  import { spawn } from 'node:child_process';
17
- import { randomBytes } from 'node:crypto';
18
- import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
19
- import { dirname, join } from 'node:path';
17
+ import { existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
18
+ import { join } from 'node:path';
20
19
 
20
+ import { writeFileAtomicSync } from '../shared/atomic-file.mjs';
21
+ import { fileHasData } from '../shared/file-has-data.mjs';
21
22
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
22
23
 
23
24
  // maxEdge is the long edge in pixels; quality is the webp setting.
@@ -100,7 +101,14 @@ function cachedRendition(cacheDir, variant, id) {
100
101
  // a plain cache miss into a thrown media request.
101
102
  try {
102
103
  const info = statSync(path);
103
- if (info.isFile()) return { path, mime, bytes: info.size };
104
+ if (!info.isFile()) continue;
105
+ // A rendition a crash left empty or NUL-filled would be served forever;
106
+ // drop it so the miss below rebuilds it.
107
+ if (!fileHasData(path)) {
108
+ unlinkSync(path);
109
+ continue;
110
+ }
111
+ return { path, mime, bytes: info.size };
104
112
  } catch {
105
113
  /* absent or evicted mid-lookup */
106
114
  }
@@ -166,21 +174,13 @@ function writeRendition(cacheDir, variant, id, extension, buffer) {
166
174
  return null;
167
175
  }
168
176
  const path = renditionPath(cacheDir, variant, id, extension);
169
- mkdirSync(dirname(path), { recursive: true });
170
- // Stage-then-rename: a reader must never observe a half-written cache file.
171
- const staging = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
172
- writeFileSync(staging, buffer);
177
+ // Durable stage-then-rename: a reader never observes a half-written cache
178
+ // file, and a crash cannot publish one whose bytes are all NUL.
173
179
  try {
174
- renameSync(staging, path);
180
+ writeFileAtomicSync(path, buffer);
175
181
  } catch (error) {
176
182
  // Another process may have won the same cache publication race.
177
183
  if (!existsSync(path)) throw error;
178
- } finally {
179
- try {
180
- unlinkSync(staging);
181
- } catch {
182
- /* renamed or already removed */
183
- }
184
184
  }
185
185
  let bytes = buffer.length;
186
186
  try {
@@ -6,12 +6,12 @@
6
6
  * (newest first) and is written atomically under a file lock because the
7
7
  * desktop and CLI can generate concurrently.
8
8
  */
9
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, statSync } from 'node:fs';
9
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, statSync } from 'node:fs';
10
10
  import { dirname, join, resolve, sep } from 'node:path';
11
11
  import { randomUUID } from 'node:crypto';
12
12
  import { spawn } from 'node:child_process';
13
13
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
14
- import { writeJsonAtomicSync, withFileLockSync } from '../shared/atomic-file.mjs';
14
+ import { readJsonStateSync, updateJsonAtomicSync, writeFileAtomicSync } from '../shared/atomic-file.mjs';
15
15
  import { cacheRendition, ensureRendition, removeRenditions, renditionSpec } from './renditions.mjs';
16
16
 
17
17
  // Renderer transport is base64 over IPC: refuse to inline anything larger.
@@ -50,10 +50,6 @@ function indexPath() {
50
50
  return join(mediaDir(), 'index.json');
51
51
  }
52
52
 
53
- function indexLockPath() {
54
- return `${indexPath()}.lock`;
55
- }
56
-
57
53
  function ensureDirs({ migrate = false } = {}) {
58
54
  mkdirSync(assetsDir(), { recursive: true });
59
55
  if (migrate && !layoutMigrationChecked) {
@@ -89,42 +85,45 @@ function readIndex() {
89
85
  indexCacheHits += 1;
90
86
  return indexCacheAssets;
91
87
  }
92
- let raw;
93
- try {
94
- raw = readFileSync(path, 'utf8');
95
- } catch (error) {
96
- // A missing index file is a genuinely empty catalog (first run). Every
97
- // OTHER read failure is not: answering with an empty list made the next
98
- // save/delete write that emptiness back over every existing asset entry.
99
- if (error?.code === 'ENOENT') return rememberIndex(path, signature, []);
100
- throw error;
101
- }
102
- const parsed = JSON.parse(raw);
103
- if (!parsed || !Array.isArray(parsed.assets)) {
104
- throw new Error(`media index is malformed: ${path}`);
105
- }
106
- indexDiskReads += 1;
107
- return rememberIndex(path, signature, parsed.assets);
88
+ // A missing index is a genuinely empty catalog (first run). A DAMAGED one
89
+ // (empty, NUL-filled, truncated, wrong shape) is kept as index.json.corrupt-<ts>
90
+ // and the last good copy restored, so the gallery lists and saves keep
91
+ // working. Every other read failure (EACCES, EIO) still throws: answering
92
+ // with an empty list made the next save/delete write that emptiness back
93
+ // over every existing asset entry.
94
+ const assets = readJsonStateSync(path, { validate: catalogAssets, fallback: [] });
95
+ if (signature !== 'missing') indexDiskReads += 1;
96
+ return rememberIndex(path, signature, assets);
108
97
  }
109
98
 
110
- function writeIndex(assets) {
111
- const path = indexPath();
112
- writeJsonAtomicSync(path, { version: 2, assets });
113
- rememberIndex(path, indexSignature(path), assets);
99
+ function catalogAssets(parsed) {
100
+ if (!parsed || !Array.isArray(parsed.assets)) throw new Error(`media index is malformed: ${indexPath()}`);
101
+ return parsed.assets;
114
102
  }
115
103
 
116
104
  /**
117
105
  * The one catalog read-modify-write. Every mutation (migrate, save, duration
118
- * backfill, delete) holds index.lock across a fresh readIndex() and writes back
119
- * only what the callback returns, so a concurrent CLI/desktop generation can
120
- * never lose an entry — and a read that FAILS (rather than being absent)
121
- * propagates out of the lock instead of writing an empty catalog over it.
106
+ * backfill, delete) holds index.lock across a fresh read and writes back only
107
+ * what the callback returns, so a concurrent CLI/desktop generation can never
108
+ * lose an entry — and a read that FAILS (rather than being absent) propagates
109
+ * out of the lock instead of writing an empty catalog over it. The previous
110
+ * catalog is kept as index.json.bak.
122
111
  */
123
112
  function mutateIndex(mutate) {
124
- withFileLockSync(indexLockPath(), () => {
125
- const next = mutate(readIndex());
126
- if (next) writeIndex(next);
127
- });
113
+ // Sets a wrong-shaped catalog aside before the lock below takes the file.
114
+ readIndex();
115
+ let written = null;
116
+ updateJsonAtomicSync(
117
+ indexPath(),
118
+ (current) => {
119
+ const next = mutate(Array.isArray(current?.assets) ? current.assets : []);
120
+ if (!next) return undefined;
121
+ written = next;
122
+ return { version: 2, assets: next };
123
+ },
124
+ { backup: true }
125
+ );
126
+ if (written) rememberIndex(indexPath(), indexSignature(indexPath()), written);
128
127
  }
129
128
 
130
129
  export function mediaStoreCacheStats() {
@@ -228,7 +227,9 @@ export function saveMediaAsset({ kind, lane, model, prompt, options = {}, mime,
228
227
  const path = storedAssetPath(file);
229
228
  if (!path) throw new Error('invalid media asset path');
230
229
  mkdirSync(dirname(path), { recursive: true });
231
- writeFileSync(path, bytes);
230
+ // Durable before the index names it: a crash must not leave a listed asset
231
+ // whose bytes are all NUL.
232
+ writeFileAtomicSync(path, bytes);
232
233
  entry.file = file;
233
234
  // The gallery is NOT capped (user). An entry-count trim used to unlink the
234
235
  // oldest files here, which is data loss nobody asked for: a generated asset
@@ -28,6 +28,7 @@ const IMAGE_MIME = Object.freeze({
28
28
  '.gif': 'image/gif',
29
29
  });
30
30
  const MAX_REFERENCE_BYTES = 12 * 1024 * 1024;
31
+ const GATEWAY_IMAGE_LANES = new Set(['openrouter', 'nano-gpt']);
31
32
 
32
33
  let graph = null;
33
34
  async function mediaGraph(deps) {
@@ -228,7 +229,13 @@ export function buildOptions(
228
229
  if (resolution) {
229
230
  validateChoice(controls, 'resolution', resolution, 'resolution');
230
231
  options.resolution = resolution;
231
- } else if (kind === 'image' && allowed(controls, 'resolution')?.includes('2k')) {
232
+ } else if (
233
+ kind === 'image' &&
234
+ !GATEWAY_IMAGE_LANES.has(lane.id) &&
235
+ allowed(controls, 'resolution')?.includes('2k')
236
+ ) {
237
+ // Gateway models bill per resolution tier: an omitted resolution keeps
238
+ // the provider's default there instead of silently upscaling.
232
239
  options.resolution = '2k';
233
240
  }
234
241
  if (quality) {
@@ -1,4 +1,4 @@
1
- import fs from 'node:fs';
1
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
2
2
 
3
3
  const BACKLOG_WARN_COOLDOWN_MS = 10 * 60_000;
4
4
  const BACKLOG_WARN_FAILURES = 5;
@@ -16,7 +16,13 @@ export function createCycleHealthLedger({ cycleStateFile, log }) {
16
16
 
17
17
  function write() {
18
18
  try {
19
- fs.writeFileSync(cycleStateFile, JSON.stringify({ running, backlog, cycles: health, updatedAt: Date.now() }));
19
+ // Rewritten on every cycle event and read by a statusline that treats an
20
+ // unparsable file as "nothing to show", so no fsync; rename keeps it untorn.
21
+ writeJsonAtomicSync(
22
+ cycleStateFile,
23
+ { running, backlog, cycles: health, updatedAt: Date.now() },
24
+ { compact: true, fsync: false }
25
+ );
20
26
  } catch {
21
27
  /* best-effort; statusline just shows nothing */
22
28
  }
@@ -1,10 +1,12 @@
1
1
  // Embedding model files in the transformers.js cache layout
2
2
  // (<cache>/<model id>/<path>), so copies that library already downloaded are
3
3
  // reused; a missing file is fetched from the Hugging Face Hub once.
4
- import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
4
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { Readable } from 'node:stream';
7
7
  import { pipeline } from 'node:stream/promises';
8
+ import { parseJsonText } from '../../shared/atomic-file.mjs';
9
+ import { fileHasData } from '../../shared/file-has-data.mjs';
8
10
 
9
11
  const HUB = 'https://huggingface.co';
10
12
 
@@ -17,7 +19,8 @@ async function download(modelId, file, target, { optional = false } = {}) {
17
19
  mkdirSync(dirname(target), { recursive: true });
18
20
  const partial = `${target}.${process.pid}.part`;
19
21
  try {
20
- await pipeline(Readable.fromWeb(res.body), createWriteStream(partial));
22
+ // flush: fsync before close, so the renamed file cannot be all NUL after a crash.
23
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(partial, { flush: true }));
21
24
  renameSync(partial, target);
22
25
  } catch (error) {
23
26
  rmSync(partial, { force: true });
@@ -26,19 +29,39 @@ async function download(modelId, file, target, { optional = false } = {}) {
26
29
  return true;
27
30
  }
28
31
 
29
- /** Local path of `file`, downloading it first when absent. */
32
+ // A cached file that holds no data (empty, NUL-filled by a crash) or, for
33
+ // JSON, does not parse is not present: it is fetched again.
34
+ function cachedFileUsable(target) {
35
+ if (!fileHasData(target)) return false;
36
+ if (!target.endsWith('.json')) return true;
37
+ try {
38
+ parseJsonText(readFileSync(target, 'utf8'));
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /** Local path of `file`, downloading it first when absent or damaged. */
30
46
  export async function ensureModelFile(cacheDir, modelId, file) {
31
47
  const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
32
- if (!existsSync(target)) await download(modelId, file, target);
48
+ if (!cachedFileUsable(target)) await download(modelId, file, target);
33
49
  return target;
34
50
  }
35
51
 
36
52
  /** Local path of an ONNX graph; a graph published with external weights gets them alongside. */
37
53
  export async function ensureModelGraph(cacheDir, modelId, file) {
38
54
  const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
39
- if (!existsSync(target)) {
40
- await download(modelId, file, target);
41
- await download(modelId, `${file}_data`, `${target}_data`, { optional: true });
55
+ const weights = `${target}_data`;
56
+ const graphPresent = fileHasData(target);
57
+ // The weights are checked on their own: an external-weights file a crash
58
+ // left empty or NUL-filled is fetched again even when the graph is intact.
59
+ // They are fetched before the graph, so a cached graph implies its weights
60
+ // finished downloading; a weights file that is absent stays absent (graphs
61
+ // without external data have none).
62
+ if (!graphPresent || (existsSync(weights) && !fileHasData(weights))) {
63
+ await download(modelId, `${file}_data`, weights, { optional: true });
42
64
  }
65
+ if (!graphPresent) await download(modelId, file, target);
43
66
  return target;
44
67
  }
@@ -2,10 +2,10 @@
2
2
  // One incremental read of a transcript file: the bytes past the persisted
3
3
  // offset, split into complete JSONL lines, stored as conversation rows. The
4
4
  // persisted boundary only advances past lines that were fully consumed (parsed
5
- // + inserted or intentionally skipped); a malformed trailing line (mid-write
6
- // partial) or a transient insert error stops the walk and leaves the boundary
7
- // untouched so the next sweep retries from the same position instead of
8
- // silently consuming the line forever.
5
+ // + inserted or intentionally skipped); a transient insert error stops the walk
6
+ // and leaves the boundary untouched so the next sweep retries from the same
7
+ // position instead of silently consuming the line forever. A complete line
8
+ // that is not JSON (crash damage) is skipped, never a permanent stop.
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { cwdFromTranscriptPath, transcriptRowFromLine } from './transcript-rows.mjs';
@@ -73,8 +73,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
73
73
  const boundary = { bytes: tail.bytes, lineIndex: tail.lineIndex, generation: tail.generation };
74
74
  let index = tail.lineIndex;
75
75
  let count = 0;
76
- for (const { line, consumedBytes } of completeLines(tail.text)) {
77
- if (!line) {
76
+ for (const { line: rawLine, consumedBytes } of completeLines(tail.text)) {
77
+ // A crash leaves a run of NUL bytes where data never reached disk, and the
78
+ // resumed writer appends after it. A raw NUL is never valid JSON, so drop
79
+ // them and keep the record that follows.
80
+ const line = rawLine.includes('\0') ? rawLine.replaceAll('\0', '') : rawLine;
81
+ if (!line.trim()) {
78
82
  boundary.bytes += consumedBytes;
79
83
  continue;
80
84
  }
@@ -82,7 +86,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
82
86
  try {
83
87
  parsed = JSON.parse(line);
84
88
  } catch {
85
- break; // malformed line: do not advance past it; retry on next sweep
89
+ // The line is complete (a half-written one has no newline yet and is not
90
+ // yielded), so it can never become valid: skip it for good, or it would
91
+ // block every line after it.
92
+ log(`[transcript-watch] skipped a damaged line in ${path.basename(transcriptPath)}\n`);
93
+ boundary.bytes += consumedBytes;
94
+ continue;
86
95
  }
87
96
  index += 1;
88
97
  const row = transcriptRowFromLine(parsed);
@@ -86,11 +86,23 @@ export function parseCycle1LineFormat(raw) {
86
86
  const chunks = [];
87
87
  for (const line of lines) {
88
88
  const parts = line.split('|');
89
- if (parts.length < 4 || !/^\d+(?:\s*,\s*\d+)*$/.test(parts[0].trim())) {
89
+ // Models often write a run as `1-8` (sessions 2026-10-22: 32 of 71 lines);
90
+ // an ascending range expands to its indexes, everything else stays invalid.
91
+ if (parts.length < 4 || !/^\d+(?:\s*-\s*\d+)?(?:\s*,\s*\d+(?:\s*-\s*\d+)?)*$/.test(parts[0].trim())) {
90
92
  chunks.push({ _idxList: [], _parseError: 'invalid_line_format' });
91
93
  continue;
92
94
  }
93
- const indexes = parts[0].split(',').map((value) => Number(value.trim()));
95
+ const indexes = [];
96
+ for (const token of parts[0].split(',')) {
97
+ const [from, to = from] = token.split('-').map((value) => Number(value.trim()));
98
+ // Past the safe-integer range `n + 1 === n`, so the expansion would never end.
99
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || to < from || to - from >= 1000) {
100
+ indexes.length = 0;
101
+ indexes.push(0);
102
+ break;
103
+ }
104
+ for (let n = from; n <= to; n += 1) indexes.push(n);
105
+ }
94
106
  chunks.push({
95
107
  _idxList: indexes,
96
108
  element: parts[1].trim(),
@@ -26,6 +26,7 @@ import { readdir, readFile, rm, stat } from 'node:fs/promises';
26
26
  import { join } from 'node:path';
27
27
  import { execFile, spawn, spawnSync } from 'node:child_process';
28
28
  import { createConnection, createServer } from 'node:net';
29
+ import { holdsNoData, writeFileAtomicSync } from '../../../shared/atomic-file.mjs';
29
30
  import { isPidAlive } from '../../../shared/pid-liveness.mjs';
30
31
  import { runChild } from './run-child.mjs';
31
32
 
@@ -180,9 +181,12 @@ function stripPlatformInvalidLines(conf) {
180
181
  function ensureConfV2(pgdataDir) {
181
182
  const confPath = join(pgdataDir, 'postgresql.conf');
182
183
  if (!existsSync(confPath)) return false;
183
- const original = readFileSync(confPath, 'utf8');
184
+ // A conf a crash left empty or NUL-filled cannot be parsed by PostgreSQL; the
185
+ // defaults plus the overrides appended below are a working config.
186
+ const text = readFileSync(confPath, 'utf8');
187
+ const original = holdsNoData(text) ? '' : text;
184
188
  let conf = stripPlatformInvalidLines(original);
185
- let changed = conf !== original;
189
+ let changed = conf !== text;
186
190
  if (!conf.includes(MIXDOG_CONF_V2_MARKER)) {
187
191
  conf += buildV2Block();
188
192
  changed = true;
@@ -196,7 +200,7 @@ function ensureConfV2(pgdataDir) {
196
200
  changed = true;
197
201
  }
198
202
  if (!changed) return false;
199
- writeFileSync(confPath, conf);
203
+ writeFileAtomicSync(confPath, conf, { backup: true });
200
204
  return true;
201
205
  }
202
206
 
@@ -350,7 +354,7 @@ async function initClusterIfNeeded({ runtimeDir, pgdataDir, env }) {
350
354
  const confPath = join(pgdataDir, 'postgresql.conf');
351
355
  try {
352
356
  const existing = readFileSync(confPath, 'utf8');
353
- writeFileSync(confPath, existing + freshClusterConfAppend());
357
+ writeFileAtomicSync(confPath, existing + freshClusterConfAppend());
354
358
  } catch (e) {
355
359
  __mixdogMemoryLog(`[pg-process] postgresql.conf append failed: ${e?.message}\n`);
356
360
  }