mixdog 1.3.0 → 1.3.2

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 (423) hide show
  1. package/NOTICE.md +52 -9
  2. package/README.md +8 -4
  3. package/package.json +2 -2
  4. package/scripts/native-release-status.mjs +238 -0
  5. package/src/defaults/shipped-history.json +8 -1
  6. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  7. package/src/defaults/skills/setup/SKILL.md +23 -1
  8. package/src/defaults/skills/setup/references/actions.md +54 -4
  9. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  10. package/src/defaults/skills/setup-guide/SKILL.md +17 -10
  11. package/src/defaults/skills/visualize/SKILL.md +9 -6
  12. package/src/defaults/skills/visualize/references/craft.md +8 -1
  13. package/src/help.mjs +1 -1
  14. package/src/lib/keychain-cjs.cjs +4 -1
  15. package/src/lib/rules-builder.cjs +14 -7
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent-mode/AGENT_MODE.md +9 -6
  18. package/src/rules/agent-mode/MIXY.md +11 -6
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/rules/shared/10-tool-workflow.md +4 -0
  21. package/src/rules/shared/20-research.md +2 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  25. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  26. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  27. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  29. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  32. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  36. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  37. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  38. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  43. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  44. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
  45. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  46. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  47. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  49. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
  50. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  51. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  52. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  53. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  54. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  65. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  68. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  69. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  72. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
  74. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
  78. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  83. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  85. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
  86. package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
  87. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  88. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  90. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  91. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  92. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  93. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  94. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  96. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  97. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  99. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  100. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
  101. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  102. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  103. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  104. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  105. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  106. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  108. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  110. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
  111. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
  112. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  116. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  119. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  120. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  124. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  125. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  126. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  128. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  131. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  132. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  134. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
  135. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
  136. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  139. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  140. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  142. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  143. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  144. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  145. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
  146. package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
  147. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  148. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  149. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  150. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  151. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  152. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  153. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  154. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  155. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  156. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  157. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  158. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  159. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  160. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  161. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
  162. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
  163. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  164. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  167. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  168. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  169. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  170. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  171. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  175. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  176. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  177. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  178. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  179. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  180. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  181. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  182. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  183. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  184. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  185. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  186. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  187. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  188. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  189. package/src/runtime/attachments/limits.mjs +5 -1
  190. package/src/runtime/attachments/office-extract.mjs +3 -6
  191. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  192. package/src/runtime/attachments/store.mjs +55 -30
  193. package/src/runtime/browser-bridge/client.mjs +73 -20
  194. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  195. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  196. package/src/runtime/channels/lib/state-file.mjs +15 -15
  197. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  198. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  199. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  200. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  201. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  202. package/src/runtime/effort-judge/judge-client.mjs +6 -3
  203. package/src/runtime/effort-judge/model-install.mjs +16 -8
  204. package/src/runtime/effort-judge/model-manifest.json +7 -7
  205. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  206. package/src/runtime/local-provider/catalog.mjs +5 -2
  207. package/src/runtime/local-provider/registered-models.mjs +64 -27
  208. package/src/runtime/local-provider/server.mjs +2 -3
  209. package/src/runtime/media/adapters/image-api.mjs +89 -0
  210. package/src/runtime/media/auth.mjs +64 -1
  211. package/src/runtime/media/catalog.mjs +66 -4
  212. package/src/runtime/media/jobs.mjs +4 -0
  213. package/src/runtime/media/lanes.mjs +46 -5
  214. package/src/runtime/media/media-usage.mjs +17 -0
  215. package/src/runtime/media/renditions.mjs +15 -15
  216. package/src/runtime/media/store.mjs +36 -35
  217. package/src/runtime/media/tool.mjs +8 -1
  218. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  219. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  220. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  221. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  222. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  223. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  224. package/src/runtime/office/core/journal.mjs +7 -14
  225. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  226. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  227. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  228. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  229. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  230. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  231. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  232. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  233. package/src/runtime/shared/agent-kind.mjs +152 -0
  234. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  235. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  236. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  237. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  238. package/src/runtime/shared/agent-route-config.mjs +22 -8
  239. package/src/runtime/shared/agent-store.mjs +149 -0
  240. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  241. package/src/runtime/shared/atomic-file.d.mts +50 -0
  242. package/src/runtime/shared/atomic-file.mjs +394 -132
  243. package/src/runtime/shared/background-tasks.mjs +7 -4
  244. package/src/runtime/shared/bot-hops.mjs +30 -0
  245. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  246. package/src/runtime/shared/bounded-download.mjs +4 -2
  247. package/src/runtime/shared/config-patch.mjs +20 -1
  248. package/src/runtime/shared/config.mjs +39 -9
  249. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  250. package/src/runtime/shared/connector-catalog.mjs +2 -4
  251. package/src/runtime/shared/connector-triggers.mjs +2 -0
  252. package/src/runtime/shared/defaults-separation.mjs +6 -2
  253. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  254. package/src/runtime/shared/err-text.mjs +7 -1
  255. package/src/runtime/shared/error-presentation.mjs +113 -10
  256. package/src/runtime/shared/file-has-data.mjs +28 -0
  257. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  258. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  259. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  260. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  261. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  262. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  263. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  264. package/src/runtime/shared/plain-preview.mjs +22 -17
  265. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  266. package/src/runtime/shared/projects.mjs +50 -41
  267. package/src/runtime/shared/provider-accounts.mjs +17 -12
  268. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  269. package/src/runtime/shared/routine-trigger.mjs +14 -6
  270. package/src/runtime/shared/run-child.d.mts +3 -0
  271. package/src/runtime/shared/run-child.mjs +24 -3
  272. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  273. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  274. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  275. package/src/runtime/shared/singleton-owner.mjs +7 -6
  276. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  277. package/src/runtime/shared/staged-update.mjs +4 -2
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  279. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  280. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  281. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  282. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  283. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  284. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  285. package/src/runtime/shared/user-cwd.mjs +9 -4
  286. package/src/runtime/shared/user-data-guard.mjs +46 -44
  287. package/src/runtime/shared/utf8-cut.mjs +10 -0
  288. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  289. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  290. package/src/runtime/tidy/core-install.mjs +26 -4
  291. package/src/runtime/tidy/install.mjs +91 -23
  292. package/src/runtime/web-search/lib/config.mjs +4 -16
  293. package/src/session-runtime/agent-home-delivery.mjs +859 -136
  294. package/src/session-runtime/agent-room-delivery.mjs +167 -19
  295. package/src/session-runtime/agent-task-sessions.mjs +831 -69
  296. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  297. package/src/session-runtime/boot/apis.mjs +58 -24
  298. package/src/session-runtime/boot/config.mjs +1 -0
  299. package/src/session-runtime/boot/facade.mjs +11 -1
  300. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  301. package/src/session-runtime/boot/providers.mjs +16 -6
  302. package/src/session-runtime/boot/shared.mjs +12 -6
  303. package/src/session-runtime/boot/tools.mjs +2 -2
  304. package/src/session-runtime/bot-status.mjs +432 -0
  305. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  306. package/src/session-runtime/config-lifecycle.mjs +78 -32
  307. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  308. package/src/session-runtime/handoff-attachments.mjs +593 -0
  309. package/src/session-runtime/held-requests.mjs +36 -33
  310. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  311. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  312. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
  313. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  314. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  315. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  316. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  317. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  318. package/src/session-runtime/media-api.mjs +5 -2
  319. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  320. package/src/session-runtime/model-route/route-tuning.mjs +11 -2
  321. package/src/session-runtime/model-route/set-route.mjs +82 -2
  322. package/src/session-runtime/native-web-search.mjs +2 -2
  323. package/src/session-runtime/new-session-config.mjs +6 -1
  324. package/src/session-runtime/owned-automation-run.mjs +8 -1
  325. package/src/session-runtime/room-member-gate.mjs +16 -3
  326. package/src/session-runtime/route-resolution.mjs +21 -19
  327. package/src/session-runtime/route-state.mjs +1 -59
  328. package/src/session-runtime/routine-home-turn.mjs +5 -3
  329. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  330. package/src/session-runtime/runtime-paths.mjs +11 -5
  331. package/src/session-runtime/runtime-tunables.mjs +6 -0
  332. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  333. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  334. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  335. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  336. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  337. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  338. package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
  339. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  340. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  341. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
  342. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  343. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  344. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  345. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  346. package/src/session-runtime/services/agent-tool.mjs +6 -0
  347. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  348. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  349. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  350. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  351. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  352. package/src/session-runtime/services/provider-admin.mjs +14 -4
  353. package/src/session-runtime/services/schedule-access.mjs +33 -4
  354. package/src/session-runtime/services/seeds.mjs +4 -3
  355. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  356. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  357. package/src/session-runtime/session-create/materialize.mjs +2 -3
  358. package/src/session-runtime/session-create/session-options.mjs +15 -2
  359. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  360. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  361. package/src/session-runtime/setup-tool/executor.mjs +20 -3
  362. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  363. package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
  364. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  365. package/src/session-runtime/skills-api.mjs +12 -5
  366. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  367. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  368. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  369. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  370. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  371. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  372. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  373. package/src/session-runtime/tool-policy-refresh.mjs +47 -358
  374. package/src/session-runtime/tool-surface.mjs +5 -3
  375. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
  376. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  377. package/src/session-runtime/turn/turn-run.mjs +2 -1
  378. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  379. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
  380. package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
  381. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  382. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  383. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  384. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  385. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  386. package/src/standalone/daemon-agent-control.mjs +4 -1
  387. package/src/standalone/daemon-session-control.mjs +60 -11
  388. package/src/standalone/daemon.mjs +34 -4
  389. package/src/standalone/local-session-runtime.mjs +22 -14
  390. package/src/standalone/session-client.mjs +7 -2
  391. package/src/standalone/session-protocol.mjs +7 -0
  392. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  393. package/src/standalone/session-service/agent-tree.mjs +4 -3
  394. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  395. package/src/standalone/session-service.mjs +11 -0
  396. package/src/standalone/session-state-projection.mjs +19 -9
  397. package/src/standalone/session-tool-output.mjs +35 -9
  398. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  399. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  400. package/src/tui/app/slash-commands.mjs +1 -1
  401. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  402. package/src/tui/dist/index.mjs +599 -342
  403. package/src/tui/prompt-history-store.mjs +16 -21
  404. package/src/tui/session/context-state.mjs +4 -0
  405. package/src/tui/session/notice-surface.mjs +3 -4
  406. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  407. package/src/tui/session/session-api/media.mjs +2 -1
  408. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  409. package/src/tui/session/session-api/routes-write.mjs +203 -0
  410. package/src/tui/session/session-api/routes.mjs +23 -236
  411. package/src/tui/session/session-api/settings.mjs +14 -1
  412. package/src/tui/session/session-flow/drain.mjs +28 -3
  413. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  414. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  415. package/src/tui/session/session-flow/steering.mjs +8 -3
  416. package/src/tui/session/tool-approval.mjs +185 -20
  417. package/src/tui/session/transcript-intake.mjs +4 -0
  418. package/src/tui/session/turn-live-call.mjs +11 -3
  419. package/src/tui/session/turn.mjs +36 -17
  420. package/src/tui/transcript-tool-failures.mjs +8 -2
  421. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  422. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  423. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -1,5 +1,4 @@
1
- import { existsSync } from 'node:fs';
2
-
1
+ import { fileHasData } from '../shared/file-has-data.mjs';
3
2
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
4
3
  import {
5
4
  exactLocalProviderFile,
@@ -65,7 +64,7 @@ export async function ensureLocalProviderServer(modelId, { dataDir = resolvePlug
65
64
  const contextWindow = localContextSettings(entry, dataDir).contextWindow;
66
65
  const executable = localProviderRuntimeExecutable(dataDir);
67
66
  const weights = localProviderModelPath(entry, dataDir);
68
- if (!executable || !existsSync(executable)) {
67
+ if (!executable || !fileHasData(executable)) {
69
68
  throw new Error('[local-provider] runtime is not installed');
70
69
  }
71
70
  if (!exactLocalProviderFile(weights, entry.size)) {
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Normalized Image API adapter for the `openrouter` and `nano-gpt` lanes.
3
+ *
4
+ * Both gateways document the same contract: POST {base}/images with `model`,
5
+ * `prompt`, the model's advertised enum parameters and `input_references`
6
+ * (image_url parts; data URLs accepted), answering `data[].b64_json` (NanoGPT
7
+ * may answer a signed `url` instead) plus `usage`.
8
+ * https://openrouter.ai/docs/guides/overview/multimodal/image-generation
9
+ * https://docs.nano-gpt.com/api-reference/image-generation
10
+ */
11
+ import { resolveImageApiAuth } from '../auth.mjs';
12
+ import { timeoutSignal } from '../bounded-signal.mjs';
13
+ import { decodeBase64Media, downloadPublicMedia } from '../download.mjs';
14
+ import { mediaError } from '../lanes.mjs';
15
+ import { imageApiUsage, withReportedUsage } from '../media-usage.mjs';
16
+ import { upstreamError } from '../upstream-error.mjs';
17
+
18
+ const IMAGE_TIMEOUT_MS = 180_000;
19
+ const LABELS = Object.freeze({ openrouter: 'OpenRouter image', 'nano-gpt': 'NanoGPT image' });
20
+ // Request field per Studio option; values come from the model's own enums.
21
+ const PARAMS = Object.freeze({
22
+ aspectRatio: 'aspect_ratio',
23
+ resolution: 'resolution',
24
+ quality: 'quality',
25
+ background: 'background',
26
+ });
27
+
28
+ function referenceUrl(reference) {
29
+ const data = String(reference?.base64 || '');
30
+ return data.startsWith('data:') ? data : `data:${reference?.mime || 'image/png'};base64,${data}`;
31
+ }
32
+
33
+ /** The stored format comes from the bytes, not a label: only rasters the store keeps. */
34
+ function rasterMime(bytes) {
35
+ if (bytes.length >= 8 && bytes.readUInt32BE(0) === 0x89504e47) return 'image/png';
36
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg';
37
+ if (bytes.length >= 12 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP')
38
+ return 'image/webp';
39
+ return '';
40
+ }
41
+
42
+ export async function generateImage({ lane, model, prompt, options = {}, references = [], signal }) {
43
+ const label = LABELS[lane] || 'Image API';
44
+ const { baseURL, token } = resolveImageApiAuth(lane);
45
+ const body = { model, prompt, n: 1 };
46
+ for (const [option, param] of Object.entries(PARAMS)) {
47
+ const value = String(options?.[option] || '').trim();
48
+ if (value) body[param] = value;
49
+ }
50
+ if (references.length) {
51
+ body.input_references = references.map((reference) => ({
52
+ type: 'image_url',
53
+ image_url: { url: referenceUrl(reference) },
54
+ }));
55
+ }
56
+ let res;
57
+ try {
58
+ res = await fetch(`${baseURL}/images`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
61
+ body: JSON.stringify(body),
62
+ redirect: 'error',
63
+ signal: timeoutSignal(signal, IMAGE_TIMEOUT_MS),
64
+ });
65
+ } catch (error) {
66
+ if (error?.name === 'TimeoutError') throw mediaError(`${label} timed out`, 'MEDIA_TIMEOUT', 504);
67
+ throw error;
68
+ }
69
+ if (!res.ok) throw upstreamError(label, res.status, (await res.text().catch(() => '')).slice(0, 400));
70
+ const data = await res.json().catch(() => null);
71
+ const usage = imageApiUsage(data);
72
+ const entry = Array.isArray(data?.data) ? data.data[0] : null;
73
+ let bytes;
74
+ try {
75
+ if (entry?.b64_json) bytes = decodeBase64Media(entry.b64_json, label);
76
+ else if (entry?.url) bytes = await downloadPublicMedia(entry.url, { signal, label });
77
+ else throw mediaError(`${label} returned no image data`, 'MEDIA_EMPTY_RESULT', 502);
78
+ } catch (error) {
79
+ throw withReportedUsage(error, usage);
80
+ }
81
+ const mime = rasterMime(bytes);
82
+ if (!mime) {
83
+ throw withReportedUsage(
84
+ mediaError(`${label} returned an unsupported image format`, 'MEDIA_UPSTREAM_FAILED', 502),
85
+ usage
86
+ );
87
+ }
88
+ return { bytes, mime, revisedPrompt: entry.revised_prompt || null, usage };
89
+ }
@@ -5,11 +5,74 @@
5
5
  * paths) so the Studio never holds a second copy of a session. API-key lanes
6
6
  * read the same keychain-backed store the agent providers use.
7
7
  */
8
- import { getAgentApiKey } from '../shared/provider-api-key.mjs';
8
+ import { readSection } from '../shared/config.mjs';
9
+ import {
10
+ getAgentApiKey,
11
+ hasAgentApiKey,
12
+ hasProviderApiKey,
13
+ resolveProviderApiKey,
14
+ } from '../shared/provider-api-key.mjs';
9
15
 
10
16
  export { hasGrokOAuthCredentials } from '../agent/orchestrator/providers/oauth-credential-probes.mjs';
11
17
 
12
18
  const XAI_BASE_URL = 'https://api.x.ai/v1';
19
+ // Both gateways serve the same normalized Image API (GET /images/models,
20
+ // POST /images) under these documented bases.
21
+ const IMAGE_API_BASE_URL = Object.freeze({
22
+ openrouter: 'https://openrouter.ai/api/v1',
23
+ 'nano-gpt': 'https://api.nano-gpt.com/api/v1',
24
+ });
25
+
26
+ /** Lanes served by the normalized Image API adapter. */
27
+ export function imageApiLane(laneId) {
28
+ return Object.hasOwn(IMAGE_API_BASE_URL, laneId);
29
+ }
30
+
31
+ function nanoGptHost(baseURL) {
32
+ try {
33
+ const host = new URL(String(baseURL || '')).hostname.toLowerCase();
34
+ return host === 'nano-gpt.com' || host.endsWith('.nano-gpt.com');
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ // NanoGPT is not a built-in provider: it is connected as an OpenAI-compatible
41
+ // custom provider, whose key lives under that provider's own id (keychain or
42
+ // its configured environment variable). The first enabled, keyed endpoint on
43
+ // a NanoGPT host is the lane's credential.
44
+ function nanoGptProviders() {
45
+ const providers = readSection('agent')?.providers || {};
46
+ return Object.entries(providers).filter(
47
+ ([id, entry]) =>
48
+ id.startsWith('custom-') &&
49
+ entry?.type === 'custom' &&
50
+ entry.enabled !== false &&
51
+ entry.auth !== 'none' &&
52
+ nanoGptHost(entry.baseURL)
53
+ );
54
+ }
55
+
56
+ /** Presence-only probe for an Image API lane's key; no secret is read. */
57
+ export function hasImageApiCredentials(laneId) {
58
+ if (laneId === 'openrouter') return hasAgentApiKey('openrouter');
59
+ if (laneId === 'nano-gpt') return nanoGptProviders().some(([id, entry]) => hasProviderApiKey(id, entry));
60
+ return false;
61
+ }
62
+
63
+ /** Base URL + bearer for an Image API lane (OpenRouter key, or the connected NanoGPT custom provider's key). */
64
+ export function resolveImageApiAuth(laneId) {
65
+ let token = '';
66
+ if (laneId === 'openrouter') token = getAgentApiKey('openrouter') || '';
67
+ else if (laneId === 'nano-gpt') {
68
+ for (const [id, entry] of nanoGptProviders()) {
69
+ token = resolveProviderApiKey(id, entry);
70
+ if (token) break;
71
+ }
72
+ }
73
+ if (!imageApiLane(laneId) || !token) throw unauthenticated(`${laneId} API key is not configured`);
74
+ return { baseURL: IMAGE_API_BASE_URL[laneId], token };
75
+ }
13
76
 
14
77
  // Built here rather than with lanes.mjs's mediaError: importing it would close
15
78
  // an auth -> lanes -> catalog -> auth module cycle.
@@ -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
  }