mixdog 1.3.1 → 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 (377) hide show
  1. package/README.md +8 -4
  2. package/package.json +2 -2
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/src/defaults/shipped-history.json +5 -1
  5. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  6. package/src/defaults/skills/setup/SKILL.md +23 -1
  7. package/src/defaults/skills/setup/references/actions.md +50 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  9. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  10. package/src/defaults/skills/visualize/SKILL.md +9 -6
  11. package/src/defaults/skills/visualize/references/craft.md +8 -1
  12. package/src/help.mjs +1 -1
  13. package/src/lib/keychain-cjs.cjs +4 -1
  14. package/src/lib/rules-builder.cjs +14 -7
  15. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  16. package/src/rules/agent-mode/MIXY.md +6 -8
  17. package/src/rules/lead/LEAD.md +2 -2
  18. package/src/rules/shared/10-tool-workflow.md +4 -0
  19. package/src/rules/shared/20-research.md +2 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  24. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  31. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  33. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  34. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  36. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  39. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  42. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  47. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  48. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  49. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  50. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  59. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  61. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  66. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  67. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  69. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -5
  70. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  71. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  73. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  75. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  76. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  77. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +17 -2
  78. package/src/runtime/agent/orchestrator/session/agent-room.mjs +133 -38
  79. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  80. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  82. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  84. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  85. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  88. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  91. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  92. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +136 -32
  93. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  94. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  95. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  96. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  97. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  98. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  99. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  100. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  101. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  102. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  103. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  104. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  105. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  108. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  109. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  110. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  113. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  114. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  115. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  116. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  117. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  121. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  123. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -8
  124. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  125. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  126. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  127. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  128. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  129. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  130. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  131. package/src/runtime/agent/orchestrator/session/routine-result.mjs +120 -9
  132. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  133. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  135. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  136. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  137. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  138. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  139. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  140. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  141. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  142. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  143. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  144. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  145. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  146. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  147. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  149. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  150. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  151. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  152. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  154. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  156. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  157. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  158. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  159. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  160. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  161. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  162. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  163. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  164. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  165. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  166. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  167. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  168. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  169. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  170. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  171. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  173. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  174. package/src/runtime/attachments/limits.mjs +5 -1
  175. package/src/runtime/attachments/office-extract.mjs +3 -6
  176. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  177. package/src/runtime/attachments/store.mjs +49 -30
  178. package/src/runtime/browser-bridge/client.mjs +73 -20
  179. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  180. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  181. package/src/runtime/channels/lib/state-file.mjs +15 -15
  182. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  183. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  184. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  185. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  186. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  187. package/src/runtime/effort-judge/judge-client.mjs +5 -2
  188. package/src/runtime/effort-judge/model-install.mjs +16 -8
  189. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  190. package/src/runtime/local-provider/catalog.mjs +5 -2
  191. package/src/runtime/local-provider/registered-models.mjs +64 -27
  192. package/src/runtime/local-provider/server.mjs +2 -3
  193. package/src/runtime/media/adapters/image-api.mjs +89 -0
  194. package/src/runtime/media/auth.mjs +64 -1
  195. package/src/runtime/media/catalog.mjs +66 -4
  196. package/src/runtime/media/jobs.mjs +4 -0
  197. package/src/runtime/media/lanes.mjs +46 -5
  198. package/src/runtime/media/media-usage.mjs +17 -0
  199. package/src/runtime/media/renditions.mjs +15 -15
  200. package/src/runtime/media/store.mjs +36 -35
  201. package/src/runtime/media/tool.mjs +8 -1
  202. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  203. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  204. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  205. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  206. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  207. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  208. package/src/runtime/office/core/journal.mjs +7 -14
  209. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  210. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  211. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  212. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  213. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  214. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  215. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  216. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  217. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  218. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  219. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  220. package/src/runtime/shared/agent-store.mjs +24 -7
  221. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  222. package/src/runtime/shared/atomic-file.d.mts +50 -0
  223. package/src/runtime/shared/atomic-file.mjs +394 -132
  224. package/src/runtime/shared/background-tasks.mjs +7 -4
  225. package/src/runtime/shared/bot-hops.mjs +30 -0
  226. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  227. package/src/runtime/shared/bounded-download.mjs +4 -2
  228. package/src/runtime/shared/config.mjs +39 -9
  229. package/src/runtime/shared/defaults-separation.mjs +6 -2
  230. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  231. package/src/runtime/shared/err-text.mjs +4 -0
  232. package/src/runtime/shared/error-presentation.mjs +22 -4
  233. package/src/runtime/shared/file-has-data.mjs +28 -0
  234. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  235. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  236. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  237. package/src/runtime/shared/projects.mjs +50 -41
  238. package/src/runtime/shared/provider-accounts.mjs +17 -12
  239. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  240. package/src/runtime/shared/routine-trigger.mjs +14 -6
  241. package/src/runtime/shared/run-child.d.mts +3 -0
  242. package/src/runtime/shared/run-child.mjs +24 -3
  243. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  244. package/src/runtime/shared/singleton-owner.mjs +7 -6
  245. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  246. package/src/runtime/shared/staged-update.mjs +4 -2
  247. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  248. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  249. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  250. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  251. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  252. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  253. package/src/runtime/shared/user-cwd.mjs +9 -4
  254. package/src/runtime/shared/user-data-guard.mjs +41 -42
  255. package/src/runtime/shared/utf8-cut.mjs +10 -0
  256. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  257. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  258. package/src/runtime/tidy/core-install.mjs +26 -4
  259. package/src/runtime/tidy/install.mjs +91 -23
  260. package/src/runtime/web-search/lib/config.mjs +4 -16
  261. package/src/session-runtime/agent-home-delivery.mjs +555 -199
  262. package/src/session-runtime/agent-room-delivery.mjs +143 -36
  263. package/src/session-runtime/agent-task-sessions.mjs +667 -165
  264. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  265. package/src/session-runtime/boot/apis.mjs +45 -24
  266. package/src/session-runtime/boot/config.mjs +1 -0
  267. package/src/session-runtime/boot/facade.mjs +11 -1
  268. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  269. package/src/session-runtime/boot/providers.mjs +2 -2
  270. package/src/session-runtime/boot/shared.mjs +12 -6
  271. package/src/session-runtime/boot/tools.mjs +2 -2
  272. package/src/session-runtime/bot-status.mjs +432 -0
  273. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  274. package/src/session-runtime/config-lifecycle.mjs +60 -55
  275. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  276. package/src/session-runtime/held-requests.mjs +36 -33
  277. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  278. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  279. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +19 -4
  280. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  281. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  282. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  283. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  284. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  285. package/src/session-runtime/media-api.mjs +5 -2
  286. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  287. package/src/session-runtime/model-route/set-route.mjs +61 -1
  288. package/src/session-runtime/owned-automation-run.mjs +8 -1
  289. package/src/session-runtime/room-member-gate.mjs +16 -3
  290. package/src/session-runtime/route-resolution.mjs +16 -8
  291. package/src/session-runtime/route-state.mjs +1 -59
  292. package/src/session-runtime/routine-home-turn.mjs +5 -3
  293. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  294. package/src/session-runtime/runtime-paths.mjs +11 -5
  295. package/src/session-runtime/runtime-tunables.mjs +6 -0
  296. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  297. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  298. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  299. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  300. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  301. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  302. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  303. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  304. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  305. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  306. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  307. package/src/session-runtime/services/agent-tool.mjs +6 -0
  308. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  309. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  310. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  311. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  312. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  313. package/src/session-runtime/services/provider-admin.mjs +14 -4
  314. package/src/session-runtime/services/schedule-access.mjs +33 -4
  315. package/src/session-runtime/services/seeds.mjs +4 -3
  316. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  317. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  318. package/src/session-runtime/session-create/materialize.mjs +2 -3
  319. package/src/session-runtime/session-create/session-options.mjs +15 -2
  320. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  321. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  322. package/src/session-runtime/setup-tool/executor.mjs +8 -2
  323. package/src/session-runtime/setup-tool/settings-contract.mjs +31 -1
  324. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  325. package/src/session-runtime/skills-api.mjs +12 -5
  326. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  327. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  328. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  329. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  330. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  331. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  332. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  333. package/src/session-runtime/tool-policy-refresh.mjs +47 -369
  334. package/src/session-runtime/tool-surface.mjs +5 -3
  335. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  336. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  337. package/src/session-runtime/turn/turn-run.mjs +2 -1
  338. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  339. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +519 -333
  340. package/src/session-runtime/workflow-agents-api/agent-route.mjs +46 -46
  341. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  342. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  343. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  344. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  345. package/src/standalone/daemon-agent-control.mjs +4 -1
  346. package/src/standalone/daemon-session-control.mjs +51 -10
  347. package/src/standalone/daemon.mjs +28 -10
  348. package/src/standalone/local-session-runtime.mjs +22 -14
  349. package/src/standalone/session-client.mjs +7 -2
  350. package/src/standalone/session-protocol.mjs +7 -0
  351. package/src/standalone/session-service.mjs +11 -0
  352. package/src/standalone/session-state-projection.mjs +19 -9
  353. package/src/standalone/session-tool-output.mjs +35 -9
  354. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  355. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  356. package/src/tui/app/slash-commands.mjs +1 -1
  357. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  358. package/src/tui/dist/index.mjs +529 -333
  359. package/src/tui/prompt-history-store.mjs +16 -21
  360. package/src/tui/session/context-state.mjs +4 -0
  361. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  362. package/src/tui/session/session-api/media.mjs +2 -1
  363. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  364. package/src/tui/session/session-api/routes-write.mjs +203 -0
  365. package/src/tui/session/session-api/routes.mjs +23 -247
  366. package/src/tui/session/session-api/settings.mjs +14 -1
  367. package/src/tui/session/session-flow/drain.mjs +28 -3
  368. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  369. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  370. package/src/tui/session/session-flow/steering.mjs +8 -3
  371. package/src/tui/session/tool-approval.mjs +185 -20
  372. package/src/tui/session/transcript-intake.mjs +2 -0
  373. package/src/tui/session/turn.mjs +23 -14
  374. package/src/tui/transcript-tool-failures.mjs +8 -2
  375. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  376. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  377. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -0,0 +1,30 @@
1
+ // Bot-to-bot chain budget. Every send a bot makes carries `hop` (stored in the
2
+ // transcript meta of the message it delivers, and in the report that answers
3
+ // it). A turn knows the hop of the submission that started it (`turnHop`,
4
+ // stamped on the session when the turn is prepared); a send made in it goes
5
+ // out at turnHop + 1. A turn started by the user or by a routine starts at 0;
6
+ // a runtime notification keeps the hop of the work it belongs to.
7
+ export const MAX_BOT_HOPS = 3;
8
+ export const BOT_HOP_LIMIT_ERROR = 'session: Bot-to-bot chain limit reached; ask the user before continuing.';
9
+
10
+ const hopOf = (value) => (Number.isInteger(value) && value > 0 ? value : 0);
11
+
12
+ /** The hop a turn starts at: its submission's own, else (a runtime
13
+ * notification, `runtimeSource`) the hop the session was already at, else 0. */
14
+ export function startingTurnHop(transcriptMeta, runtimeSource, previous) {
15
+ return hopOf(transcriptMeta?.hop) || (runtimeSource ? hopOf(previous) : 0);
16
+ }
17
+
18
+ /** Stamp the starting hop on `session` for the turn about to run. */
19
+ export function stampTurnHop(session, transcriptMeta, runtimeSource) {
20
+ session.turnHop = startingTurnHop(transcriptMeta, runtimeSource, session.turnHop);
21
+ }
22
+
23
+ /** A bot's message steered into the running turn raises the turn's hop. */
24
+ export function raiseTurnHop(session, transcriptMeta) {
25
+ const hop = hopOf(transcriptMeta?.hop);
26
+ if (session && hop > hopOf(session.turnHop)) session.turnHop = hop;
27
+ }
28
+
29
+ /** The hop of the turn `session` is in. */
30
+ export const turnHopOf = (session) => hopOf(session?.turnHop);
@@ -83,7 +83,7 @@ function writeJson(file, value) {
83
83
 
84
84
  // The definition file a bot's kind is read from: AGENT.md, or the archived
85
85
  // copy of a deleted bot (its chat is kept, so it moves with the others).
86
- function definitionFile(dir) {
86
+ export function definitionFile(dir) {
87
87
  if (existsSync(join(dir, 'AGENT.md'))) return join(dir, 'AGENT.md');
88
88
  if (existsSync(join(dir, DELETED_MARKER)) && existsSync(join(dir, ARCHIVED_FILE))) return join(dir, ARCHIVED_FILE);
89
89
  return '';
@@ -55,7 +55,7 @@ export async function readResponseBuffer(response, { maxBytes, label = 'download
55
55
  export async function streamResponseToFile(
56
56
  response,
57
57
  destPath,
58
- { maxBytes, expectedBytes = 0, label = 'download', onProgress = null } = {}
58
+ { maxBytes, expectedBytes = 0, label = 'download', onProgress = null, signal = undefined } = {}
59
59
  ) {
60
60
  const { maximum, expected } = downloadBounds(maxBytes, expectedBytes, label);
61
61
  const advertised = responseByteLength(response, maximum, label);
@@ -94,7 +94,9 @@ export async function streamResponseToFile(
94
94
  });
95
95
 
96
96
  try {
97
- await pipeline(response.body, guard, createWriteStream(destPath));
97
+ // flush: the bytes reach disk before the caller installs the file, so a
98
+ // crash cannot leave an installed binary that is all NUL.
99
+ await pipeline(response.body, guard, createWriteStream(destPath, { flush: true }), { signal });
98
100
  return downloaded;
99
101
  } catch (error) {
100
102
  try {
@@ -2,12 +2,14 @@
2
2
  * Unified config reader/writer.
3
3
  * Single file: mixdog-config.json with sections such as channels, agent, and memory.
4
4
  */
5
- import { readFileSync, statSync, mkdirSync, existsSync, watch as fsWatch } from 'node:fs';
6
- import { join, dirname } from 'node:path';
5
+ import { readFileSync, readdirSync, statSync, mkdirSync, existsSync, watch as fsWatch } from 'node:fs';
6
+ import { basename, join, dirname } from 'node:path';
7
7
  import { createRequire } from 'node:module';
8
8
  import { resolvePluginData } from './plugin-paths.mjs';
9
9
  import { hasOwn, isPlainObject } from './object.mjs';
10
10
  import {
11
+ parseJsonText,
12
+ quarantinePath,
11
13
  renameWithRetrySync,
12
14
  writeJsonAtomicSync,
13
15
  writeJsonAtomicAsync,
@@ -226,7 +228,7 @@ function readConfigFile() {
226
228
  const raw = readConfigRawCached();
227
229
  if (raw == null) return null;
228
230
  try {
229
- return JSON.parse(raw);
231
+ return parseJsonText(raw);
230
232
  } catch (err) {
231
233
  // Quarantine a malformed mixdog-config.json so the next boot starts fresh
232
234
  // instead of looping on a broken file. A cached raw string that fails to
@@ -346,21 +348,47 @@ function readAll() {
346
348
  }
347
349
 
348
350
  function quarantineMalformedConfig(parseErr) {
349
- const corrupt = `${configPath()}.corrupt-${Date.now()}`;
351
+ const corrupt = quarantinePath(configPath());
352
+ let keptAside = false;
350
353
  try {
351
354
  if (existsSync(configPath())) renameWithRetrySync(configPath(), corrupt);
355
+ keptAside = existsSync(corrupt);
352
356
  } catch {}
353
357
  process.stderr.write(
354
358
  `[config] mixdog-config.json is malformed (${parseErr.message}). Renamed to ${corrupt}. Restore it or delete to start fresh.\n`
355
359
  );
360
+ return keptAside;
356
361
  }
357
362
 
358
- function restoreAllForRmWOrThrow(reason) {
363
+ // A damaged config that was kept aside (its bytes preserved as
364
+ // mixdog-config.json.corrupt-<ts>) is not a lost config: those bytes are the
365
+ // safety net, so with no backup to restore a save may start from an empty
366
+ // config. Every save also snapshots a backup, so a stale copy from an earlier
367
+ // incident never reaches this point while a backup of a later config exists.
368
+ function hasQuarantinedCopy() {
369
+ const dir = dirname(configPath());
370
+ const prefix = `${basename(configPath())}.corrupt-`;
371
+ try {
372
+ return readdirSync(dir).some((name) => name.startsWith(prefix));
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ // `keptAside`: the unusable config's bytes are preserved on disk, so with no
379
+ // backup to restore a save starts from an empty config instead of refusing.
380
+ function restoreAllForRmWOrThrow(reason, keptAside = false) {
359
381
  const restored = loadLatestMixdogConfigFromBackup(resolvePluginData(), { fresh: true });
360
382
  if (restored && isPlainObject(restored)) {
361
383
  process.stderr.write(`[config] RMW read: restored mixdog-config.json from latest user-data backup (${reason})\n`);
362
384
  return restored;
363
385
  }
386
+ if (keptAside) {
387
+ process.stderr.write(
388
+ `[config] RMW read: mixdog-config.json is unusable (${reason}); its bytes are kept aside and no backup exists; starting empty\n`
389
+ );
390
+ return {};
391
+ }
364
392
  const legacy = findLegacyUserDataEvidence(resolvePluginData(), { fresh: true });
365
393
  if (legacy) {
366
394
  const err = new Error(
@@ -381,6 +409,9 @@ function restoreAllForRmWOrThrow(reason) {
381
409
  * Read-modify-write baseline. ENOENT on a never-initialized data dir → {}.
382
410
  * Malformed on-disk config (or missing config after prior init) → restore
383
411
  * from backup or throw; never silently collapse to a one-section overwrite.
412
+ * The exception is a damaged config whose bytes were kept aside as
413
+ * .corrupt-<ts>: with no backup to restore, the save starts from an empty
414
+ * config. A config that cannot be READ (I/O error) still throws.
384
415
  */
385
416
  function readAllForRmW() {
386
417
  let raw;
@@ -389,7 +420,7 @@ function readAllForRmW() {
389
420
  } catch (err) {
390
421
  if (err.code === 'ENOENT') {
391
422
  if (isUserDataRecoveryExpected(resolvePluginData(), { fresh: true })) {
392
- return restoreAllForRmWOrThrow('config file missing after user-data was initialized');
423
+ return restoreAllForRmWOrThrow('config file missing after user-data was initialized', hasQuarantinedCopy());
393
424
  }
394
425
  return {};
395
426
  }
@@ -397,12 +428,11 @@ function readAllForRmW() {
397
428
  throw err;
398
429
  }
399
430
  try {
400
- const parsed = JSON.parse(raw);
431
+ const parsed = parseJsonText(raw);
401
432
  if (!isPlainObject(parsed)) throw new SyntaxError('config root must be a JSON object');
402
433
  return parsed;
403
434
  } catch (parseErr) {
404
- quarantineMalformedConfig(parseErr);
405
- return restoreAllForRmWOrThrow(parseErr.message);
435
+ return restoreAllForRmWOrThrow(parseErr.message, quarantineMalformedConfig(parseErr));
406
436
  }
407
437
  }
408
438
 
@@ -11,9 +11,10 @@
11
11
  * Left untouched on purpose: `desktop`, credentials, onboarding, detection and
12
12
  * install flags, caches, and keys that are only ever hand-edited.
13
13
  */
14
- import { constants, copyFileSync, existsSync, mkdirSync } from 'node:fs';
14
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { isDeepStrictEqual } from 'node:util';
17
+ import { writeFileAtomicSync } from './atomic-file.mjs';
17
18
  import { packDefinitionFromDir } from './shipped-definitions.mjs';
18
19
  import { resolvePluginData } from './plugin-paths.mjs';
19
20
  import { hasOwn, isPlainObject } from './object.mjs';
@@ -231,7 +232,10 @@ export function backupConfigBeforeDefaultsSeparation({ dataDir = resolvePluginDa
231
232
  throw err;
232
233
  }
233
234
  const target = join(dir, 'mixdog-config.json');
234
- copyFileSync(source, target, constants.COPYFILE_EXCL);
235
+ // Written through an fsynced handle (a plain copy can leave this backup NUL
236
+ // exactly when a crash makes it needed); the folder was claimed above, so
237
+ // nothing can exist at `target`.
238
+ writeFileAtomicSync(target, readFileSync(source), { mode: statSync(source).mode & 0o777 });
235
239
  return target;
236
240
  }
237
241
  }
@@ -0,0 +1,74 @@
1
+ // Delegated approvals: a sub-agent's approval request (tui/session/tool-approval.mjs)
2
+ // is also shown in the top-level conversation the user watches — the root bot's
3
+ // home chat, or the coding Lead's session (human-gate/gate.mjs approvalSurface).
4
+ // It stays ONE request: the sub-agent's queue owns it and its waiter; the
5
+ // top-level session's queue holds a copy whose answer is forwarded to that
6
+ // owner, and the copy is withdrawn as soon as the owner settles (answered in
7
+ // either place, timed out, its turn ended). Like held-request-host.mjs this
8
+ // registry is process-wide and dependency-free.
9
+
10
+ const KEY = Symbol.for('mixdog.delegatedApprovalHost.v1');
11
+
12
+ function registry() {
13
+ globalThis[KEY] ??= { queues: new Set(), pending: new Map(), opener: null };
14
+ return globalThis[KEY];
15
+ }
16
+
17
+ /** `queue`: `{ sessionId() → string, disposed() → boolean, offer(delegated),
18
+ * withdraw(id, result) }` — one session runtime's approval queue. */
19
+ export function registerApprovalQueue(queue) {
20
+ registry().queues.add(queue);
21
+ return () => registry().queues.delete(queue);
22
+ }
23
+
24
+ /** `open(sessionId) → Promise`: brings a session's runtime up (the daemon's
25
+ * session service), so a top-level chat that is not loaded can show the copy. */
26
+ export function registerDelegatedApprovalOpener(open) {
27
+ registry().opener = open;
28
+ return () => {
29
+ if (registry().opener === open) registry().opener = null;
30
+ };
31
+ }
32
+
33
+ function liveQueuesOf(sessionId) {
34
+ const { queues } = registry();
35
+ const live = [];
36
+ for (const queue of queues) {
37
+ if (queue.disposed()) queues.delete(queue);
38
+ else if (queue.sessionId() === sessionId) live.push(queue);
39
+ }
40
+ return live;
41
+ }
42
+
43
+ function offerTo(sessionId, delegated) {
44
+ const live = liveQueuesOf(sessionId);
45
+ for (const queue of live) queue.offer(delegated);
46
+ return live.length > 0;
47
+ }
48
+
49
+ /** Show `delegated` (`{ id, sessionId, request, answer(...finishArgs) → boolean }`,
50
+ * `sessionId` being the top-level conversation) there, now or once it loads. */
51
+ export function delegateApproval(delegated) {
52
+ const { pending, opener } = registry();
53
+ pending.set(delegated.id, delegated);
54
+ if (offerTo(delegated.sessionId, delegated) || typeof opener !== 'function') return;
55
+ void Promise.resolve()
56
+ .then(() => opener(delegated.sessionId))
57
+ .then(() => {
58
+ if (pending.get(delegated.id) === delegated) offerTo(delegated.sessionId, delegated);
59
+ })
60
+ .catch(() => {});
61
+ }
62
+
63
+ /** The owner settled `id`: every copy goes away. `result` (`{ approved, device }`)
64
+ * lets the copy say who answered. */
65
+ export function withdrawDelegatedApproval(id, result = null) {
66
+ const { pending, queues } = registry();
67
+ if (!pending.delete(id)) return;
68
+ for (const queue of queues) queue.withdraw(id, result);
69
+ }
70
+
71
+ /** The pending delegated approvals a top-level session should show. */
72
+ export function delegatedApprovalsFor(sessionId) {
73
+ return [...registry().pending.values()].filter((delegated) => delegated.sessionId === sessionId);
74
+ }
@@ -257,6 +257,10 @@ export function presentErrorText(error, options = {}) {
257
257
  if (code === 'TOOL_SURFACE_EXCEEDS_CONTEXT' || /\(TOOL_SURFACE_EXCEEDS_CONTEXT\)/.test(text)) {
258
258
  return stripErrorPrefix(text);
259
259
  }
260
+ // Likewise a latest message that cannot fit names its size and what would
261
+ // help; a wrapper's prefix ("agent compact failed (...): ") is dropped.
262
+ const latestTurn = /The latest message cannot fit this model \(LATEST_TURN_EXCEEDS_CONTEXT\)[\s\S]*$/.exec(text);
263
+ if (latestTurn) return latestTurn[0].replace(' (LATEST_TURN_EXCEEDS_CONTEXT)', '');
260
264
  const jsonError = extractJsonError(text);
261
265
  if (jsonError) text = jsonError;
262
266
  const embeddedError = extractEmbeddedError(text);
@@ -3,10 +3,14 @@ import { transportErrorText } from './transport-error-text.mjs';
3
3
 
4
4
  const DETAILS_LIMIT = 12_000;
5
5
  const SUMMARY_LIMIT = 180;
6
+ // "<wrapper>: The latest message cannot fit this model (CODE): <sizes>. <cause and remedy>"
7
+ const LATEST_TURN_RE =
8
+ /The latest message cannot fit this model \(LATEST_TURN_EXCEEDS_CONTEXT\)(: it needs about [\d,]+ tokens, but the [^.]* window leaves [\d,]+ for it\.)([\s\S]*)$/;
6
9
 
7
10
  /** Diagnostic text must be safe before it reaches a view, copy button or log. */
8
11
  export function safeErrorDetails(value) {
9
12
  let text = String(value ?? '')
13
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: strips ANSI escape sequences (ESC) from diagnostic text
10
14
  .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
11
15
  .replace(/\r\n?/g, '\n')
12
16
  .replace(/\b((?:https?|ssh):\/\/)[^/\s@]+@/gi, '$1[redacted]@')
@@ -135,7 +139,9 @@ function resetLabel(resetAt) {
135
139
  const reset = new Date(resetAt);
136
140
  const pad = (n) => String(n).padStart(2, '0');
137
141
  const time = `${pad(reset.getHours())}:${pad(reset.getMinutes())}`;
138
- return reset.toDateString() === new Date().toDateString() ? time : `${reset.getMonth() + 1}/${reset.getDate()} ${time}`;
142
+ return reset.toDateString() === new Date().toDateString()
143
+ ? time
144
+ : `${reset.getMonth() + 1}/${reset.getDate()} ${time}`;
139
145
  }
140
146
 
141
147
  // A refused request that names its model as unknown, retired or not offered
@@ -286,7 +292,9 @@ export function describeError(value) {
286
292
  if (
287
293
  value?.reauthRequired === true ||
288
294
  (!typed &&
289
- /invalid_grant|refresh token (?:expired|revoked|not available)|sign in again|sign-in expired|Reauth Required/i.test(raw))
295
+ /invalid_grant|refresh token (?:expired|revoked|not available)|sign in again|sign-in expired|Reauth Required/i.test(
296
+ raw
297
+ ))
290
298
  ) {
291
299
  kind = 'reauth';
292
300
  summary = 'Sign-in expired.';
@@ -294,7 +302,9 @@ export function describeError(value) {
294
302
  } else if (
295
303
  descriptor.kind === 'auth' ||
296
304
  (descriptor.kind === 'unknown' &&
297
- /\b(?:invalid|expired)\s+(?:api[ _-]?key|access token|credentials?)\b|Provider authentication failed/i.test(clean))
305
+ /\b(?:invalid|expired)\s+(?:api[ _-]?key|access token|credentials?)\b|Provider authentication failed/i.test(
306
+ clean
307
+ ))
298
308
  ) {
299
309
  kind = 'authentication';
300
310
  summary = 'Provider authentication failed';
@@ -331,6 +341,13 @@ export function describeError(value) {
331
341
  kind = 'not-found';
332
342
  summary = firstReadableLine(clean) || NOT_FOUND_SUMMARY;
333
343
  recovery = MODEL_UNAVAILABLE_COPY[2];
344
+ } else if (LATEST_TURN_RE.test(clean)) {
345
+ // The latest message cannot fit the window (compact/runner.mjs): the
346
+ // sizes are the headline, the cause and remedy the next step.
347
+ const [, headline, rest] = LATEST_TURN_RE.exec(clean);
348
+ kind = 'latest-turn-size';
349
+ summary = `The latest message cannot fit this model${headline}`;
350
+ recovery = rest.trim();
334
351
  } else if (
335
352
  descriptor.kind === 'unknown' &&
336
353
  /payload too large|request (?:body )?too large|The request exceeds the size limit/i.test(clean)
@@ -366,6 +383,7 @@ export function describeError(value) {
366
383
  }
367
384
  summary = summary.replace(/\s+/g, ' ').trim() || 'Something went wrong.';
368
385
  if (summary.length > SUMMARY_LIMIT) summary = `${summary.slice(0, SUMMARY_LIMIT - 1).trimEnd()}…`;
386
+ const fingerprintStatus = kind === 'reauth' ? '' : status || '';
369
387
  return {
370
388
  kind,
371
389
  summary,
@@ -378,6 +396,6 @@ export function describeError(value) {
378
396
  details,
379
397
  // Known structured failures ignore volatile indexes; unknown failures
380
398
  // retain their diagnostic identity rather than merging unrelated work.
381
- fingerprint: kind === 'unknown' ? details : `${kind}:${kind === 'reauth' ? '' : status || ''}`,
399
+ fingerprint: kind === 'unknown' ? details : `${kind}:${fingerprintStatus}`,
382
400
  };
383
401
  }
@@ -0,0 +1,28 @@
1
+ import { closeSync, openSync, readSync } from 'node:fs';
2
+ import { holdsNoData } from './atomic-file.mjs';
3
+
4
+ const PROBE_BYTES = 4096;
5
+
6
+ /**
7
+ * Install check for a downloaded binary or model: false when `path` is absent
8
+ * or its first bytes hold no data (empty, or NUL-filled by a crash that
9
+ * committed the size but not the bytes). Cheaper than hashing a multi-GB file
10
+ * on every use. A file that exists but cannot be opened right now (locked by a
11
+ * scanner or by the process running it) still counts as present.
12
+ */
13
+ export function fileHasData(path) {
14
+ let fd;
15
+ try {
16
+ fd = openSync(path, 'r');
17
+ const head = Buffer.alloc(PROBE_BYTES);
18
+ return !holdsNoData(head.subarray(0, readSync(fd, head, 0, PROBE_BYTES, 0)));
19
+ } catch (err) {
20
+ return err?.code !== 'ENOENT' && err?.code !== 'ENOTDIR';
21
+ } finally {
22
+ if (fd !== undefined) {
23
+ try {
24
+ closeSync(fd);
25
+ } catch {}
26
+ }
27
+ }
28
+ }
@@ -114,7 +114,7 @@ export async function importUsageHistory(ledger, dataDir, { now = Date.now(), re
114
114
  // send's diagnostic copy cannot also enter as historical usage.
115
115
  const until = Number(ledger.get('liveSince')) || now;
116
116
  if (Number(ledger.get('importedThrough')) >= until) return { skipped: true };
117
- const result = { inserted: 0, sourceRows: 0, unrecordableRows: 0, files: [], traceRows: 0 };
117
+ const result = { inserted: 0, sourceRows: 0, unrecordableRows: 0, tornTraceLines: 0, files: [], traceRows: 0 };
118
118
  const accept = (rows) => {
119
119
  const normalized = [];
120
120
  for (const raw of rows) {
@@ -140,15 +140,23 @@ export async function importUsageHistory(ledger, dataDir, { now = Date.now(), re
140
140
  const original = await readFile(path, 'utf8');
141
141
  const lines = original.split(/\r?\n/);
142
142
  const rows = [];
143
+ let torn = 0;
143
144
  for (let i = 0; i < lines.length; i++) {
144
145
  if (!lines[i].trim()) continue;
145
146
  let row;
146
147
  try {
147
148
  row = JSON.parse(lines[i]);
148
149
  } catch {
149
- throw new Error(`Incomplete or invalid trace record: ${path}:${i + 1}`);
150
+ // An append a crash tore (cut short, or NUL-filled) is no record: skip
151
+ // it, like the gateway store below, instead of failing every import.
152
+ torn += 1;
153
+ continue;
150
154
  }
151
- if (row.kind === 'usage_raw' || row.kind === 'usage') rows.push(row);
155
+ if (row?.kind === 'usage_raw' || row?.kind === 'usage') rows.push(row);
156
+ }
157
+ if (torn > 0) {
158
+ process.stderr.write(`[usage-ledger] warning: ${path} has ${torn} torn trace line(s); skipped\n`);
159
+ result.tornTraceLines += torn;
152
160
  }
153
161
  accept(rows);
154
162
  result.files.push({ path, sha256: createHash('sha256').update(original).digest('hex'), rows: rows.length });
@@ -160,29 +168,40 @@ export async function importUsageHistory(ledger, dataDir, { now = Date.now(), re
160
168
  // The JSONL store supersedes the legacy JSON file (it is migrated from it).
161
169
  const gatewayJsonl = await optionalFile(join(dataDir, 'gateway-usage.local.jsonl'));
162
170
  const gateway = gatewayJsonl === null ? await optionalFile(join(dataDir, 'gateway-usage.local.json')) : null;
163
- if (gatewayJsonl !== null || gateway)
171
+ // A missing file is absence. Empty, unparsable, or wrong-shaped JSON is not:
172
+ // skip only that legacy import and leave importedThrough unset so a repair
173
+ // is retried instead of short-circuiting forever.
174
+ let legacyReady = true;
175
+ let gatewayEvents = null;
176
+ if (gatewayJsonl !== null) {
177
+ gatewayEvents = gatewayJsonl.split('\n').flatMap((line) => {
178
+ try {
179
+ return line.trim() ? [JSON.parse(line)] : [];
180
+ } catch {
181
+ return []; // torn append
182
+ }
183
+ });
184
+ } else if (gateway !== null) {
185
+ try {
186
+ const events = JSON.parse(gateway)?.events;
187
+ gatewayEvents = Array.isArray(events) ? events : [];
188
+ } catch (error) {
189
+ legacyReady = false;
190
+ process.stderr.write(
191
+ `[usage-ledger] warning: legacy gateway usage file gateway-usage.local.json is empty or corrupt (${error.message}); skipping its import\n`
192
+ );
193
+ }
194
+ }
195
+ if (gatewayEvents)
164
196
  accept(
165
- (gatewayJsonl !== null
166
- ? gatewayJsonl.split('\n').flatMap((line) => {
167
- try {
168
- return line.trim() ? [JSON.parse(line)] : [];
169
- } catch {
170
- return []; // torn append
171
- }
172
- })
173
- : JSON.parse(gateway).events || []
174
- ).map((event) => ({
197
+ gatewayEvents.map((event) => ({
175
198
  ...event,
176
199
  kind: 'usage',
177
200
  session_id: event.sessionId,
178
201
  }))
179
202
  );
180
- // A missing file is absence. Empty, unparsable, or wrong-shaped JSON is not:
181
- // skip only this legacy import and leave importedThrough unset so a repair
182
- // is retried instead of short-circuiting forever.
183
203
  const legacyPath = join(dataDir, 'usage-rollup.local.json');
184
204
  const legacy = await optionalFile(legacyPath);
185
- let legacyReady = true;
186
205
  if (legacy !== null) {
187
206
  let days;
188
207
  try {
@@ -0,0 +1,54 @@
1
+ // Which Lead sessions still wait on agent work they handed off: the rule the
2
+ // desktop turn notifier applies to the agent pool (apps/desktop
3
+ // shared/agent-activity.ts isActiveDesktopAgentEntry /
4
+ // isCancelUnconfirmedDesktopAgentEntry, main/push-turn-events.ts
5
+ // sessionsWithPendingWork), ported for the runtime so a bot's task-session
6
+ // report waits for the same final answer the desktop notifies. Pool rows are
7
+ // listStoredAgentWorkersAsync rows (`sessionId`, root `ownerSessionId`,
8
+ // `status`, `stage`); nested descendants keep the Lead as their owner.
9
+
10
+ const TERMINAL_STATUS = /idle|done|complete|success|closed|error|fail|cancel|killed|timeout/i;
11
+ const ACTIVE_STATUS =
12
+ /^(?:connecting|requesting|streaming|tool[-_\s]?running|running|queued|pending|starting|resource_wait)$/i;
13
+ const CANCELLED_STATUS = /cancel|killed|aborted|interrupted/i;
14
+ const CANCEL_UNCONFIRMED_STATUS = /cancel[-_\s]?(?:unconfirmed|pending)|cancell?ing/i;
15
+
16
+ const record = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
17
+
18
+ function statusValues(value) {
19
+ const entry = record(value);
20
+ return [entry.stage, entry.status]
21
+ .map((status) => String(status || '').trim())
22
+ .filter((status, index, values) => Boolean(status) && values.indexOf(status) === index);
23
+ }
24
+
25
+ /** A running or queued row: `status` decides, `stage` only for a row without one. */
26
+ function isActiveAgentRow(value) {
27
+ const statuses = statusValues(value);
28
+ if (statuses.length === 0 || statuses.some((status) => CANCELLED_STATUS.test(status))) return false;
29
+ const lifecycle = String(record(value).status || '').trim() || statuses[0];
30
+ return !TERMINAL_STATUS.test(lifecycle) && ACTIVE_STATUS.test(lifecycle);
31
+ }
32
+
33
+ /** A cancel whose stop is not proven yet. */
34
+ function isCancelUnconfirmedAgentRow(value) {
35
+ return statusValues(value).some((status) => CANCEL_UNCONFIRMED_STATUS.test(status));
36
+ }
37
+
38
+ /** Agent work still in flight on one pool row. */
39
+ export function hasPendingAgentWork(value) {
40
+ return isActiveAgentRow(value) || isCancelUnconfirmedAgentRow(value);
41
+ }
42
+
43
+ /** The owner (Lead) session ids with a child row still in flight. The Lead's
44
+ * own row is skipped: its turn state comes from the session itself. */
45
+ export function sessionsWithPendingWork(rows) {
46
+ const pending = new Set();
47
+ for (const row of Array.isArray(rows) ? rows : []) {
48
+ const sessionId = String(row?.sessionId || '');
49
+ const ownerSessionId = String(row?.ownerSessionId || '') || sessionId;
50
+ if (!ownerSessionId || sessionId === ownerSessionId) continue;
51
+ if (hasPendingAgentWork(row)) pending.add(ownerSessionId);
52
+ }
53
+ return pending;
54
+ }
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  closeSync,
3
3
  existsSync,
4
- fsyncSync,
5
4
  mkdirSync,
6
5
  openSync,
7
6
  readFileSync,
@@ -15,7 +14,7 @@ import {
15
14
  import { join } from 'node:path';
16
15
  import { execFile } from 'node:child_process';
17
16
  import { isDeepStrictEqual, promisify } from 'node:util';
18
- import { withFileLock, withFileLockSync } from './atomic-file.mjs';
17
+ import { withFileLock, withFileLockSync, writeJsonAtomicSync } from './atomic-file.mjs';
19
18
  import { resolvePluginData } from './plugin-paths.mjs';
20
19
 
21
20
  const execFileAsync = promisify(execFile);
@@ -338,24 +337,21 @@ function markerOwned(active) {
338
337
  function writeMarker(active) {
339
338
  try {
340
339
  mkdirSync(active.markerDir, { recursive: true, mode: 0o700 });
341
- writeFileSync(
340
+ // Flushed through the handle that wrote it: reopening the file read-only to
341
+ // fsync it is refused on Windows (EPERM), which reported a failure after
342
+ // the marker was already written.
343
+ writeJsonAtomicSync(
342
344
  active.markerPath,
343
- `${JSON.stringify({
345
+ {
344
346
  version: 1,
345
347
  timestamp: new Date().toISOString(),
346
348
  pid: process.pid,
347
349
  ppid: process.ppid,
348
350
  token: active.token,
349
351
  processIdentity: active.processIdentity,
350
- })}\n`,
351
- { encoding: 'utf8', mode: 0o600 }
352
+ },
353
+ { compact: true, mode: 0o600 }
352
354
  );
353
- const fd = openSync(active.markerPath, 'r');
354
- try {
355
- fsyncSync(fd);
356
- } finally {
357
- closeSync(fd);
358
- }
359
355
  return true;
360
356
  } catch {
361
357
  return false;