mixdog 1.2.4 → 1.2.6

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 (292) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/mobile-release.mjs +5 -4
  5. package/scripts/provider-pingpong-live.mjs +9 -9
  6. package/src/agents/advisor/AGENT.md +4 -4
  7. package/src/agents/advisor/agent.json +1 -1
  8. package/src/agents/reviewer/AGENT.md +2 -3
  9. package/src/agents/reviewer/agent.json +1 -1
  10. package/src/defaults/shipped-history.json +11 -0
  11. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  12. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  13. package/src/defaults/skills/visualize/SKILL.md +3 -2
  14. package/src/lib/rules-builder.cjs +1 -1
  15. package/src/output-styles/common.md +22 -5
  16. package/src/output-styles/detailed.md +7 -5
  17. package/src/output-styles/minimal.md +7 -3
  18. package/src/output-styles/simple.md +2 -3
  19. package/src/rules/agent/AGENT.md +3 -1
  20. package/src/rules/lead/LEAD.md +10 -7
  21. package/src/rules/shared/10-tool-workflow.md +2 -1
  22. package/src/rules/shared/60-verification.md +4 -5
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  28. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  29. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  39. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  40. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  41. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  42. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  43. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  45. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  68. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  73. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  74. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  75. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  76. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  77. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  78. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  79. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  81. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  84. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  85. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  86. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  87. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  88. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  89. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  90. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  91. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  92. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  93. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  94. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  95. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  98. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
  99. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  100. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  101. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  102. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  103. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  104. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  105. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  106. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  107. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  116. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  118. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  125. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  126. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  135. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  141. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  142. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  144. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  145. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  146. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  147. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  148. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  153. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  154. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  155. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  156. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  157. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  158. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  160. package/src/runtime/attachments/office-extract.mjs +11 -9
  161. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  162. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  163. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  164. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  165. package/src/runtime/office/core/tabular.mjs +1 -1
  166. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  167. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  168. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  169. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  170. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  171. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  172. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  173. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  174. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  175. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  176. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  178. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  179. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  180. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  181. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  182. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  183. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  184. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  185. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  186. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  187. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  188. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  189. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  190. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  191. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  192. package/src/runtime/shared/atomic-file.mjs +22 -13
  193. package/src/runtime/shared/background-tasks.mjs +60 -20
  194. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  195. package/src/runtime/shared/config.mjs +0 -10
  196. package/src/runtime/shared/file-lock.mjs +7 -1
  197. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  198. package/src/runtime/shared/file-permissions.d.mts +19 -4
  199. package/src/runtime/shared/file-permissions.mjs +310 -4
  200. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  201. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  202. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  203. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  204. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  205. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  206. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  207. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  208. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  209. package/src/runtime/shared/notify-trace.mjs +8 -1
  210. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  211. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  212. package/src/runtime/shared/process-runner.mjs +38 -0
  213. package/src/runtime/shared/provider-accounts.mjs +2 -1
  214. package/src/runtime/shared/provider-api-key.mjs +0 -1
  215. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  216. package/src/runtime/shared/resource-admission.mjs +60 -18
  217. package/src/runtime/shared/run-child.mjs +84 -0
  218. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  219. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  220. package/src/runtime/shared/singleton-owner.mjs +126 -42
  221. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  222. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  223. package/src/runtime/shared/user-data-guard.mjs +28 -9
  224. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  225. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  226. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  227. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  228. package/src/runtime/tidy/runners/biome.mjs +3 -1
  229. package/src/runtime/tidy/shadow.mjs +2 -1
  230. package/src/runtime/web-search/index.mjs +5 -7
  231. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  232. package/src/runtime/web-search/tool-defs.mjs +6 -1
  233. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  234. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  235. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  236. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  237. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  238. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  239. package/src/session-runtime/lifecycle-api.mjs +3 -1
  240. package/src/session-runtime/provider-auth/login.mjs +10 -6
  241. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  242. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  243. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  244. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  245. package/src/session-runtime/provider-models.mjs +76 -10
  246. package/src/session-runtime/provider-readiness.mjs +4 -0
  247. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  248. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  249. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  250. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  251. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  252. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  254. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  255. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  256. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  257. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  258. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  259. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  260. package/src/session-runtime/services/agent-tool.mjs +20 -0
  261. package/src/session-runtime/services/provider-admin.mjs +92 -20
  262. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  263. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  264. package/src/session-runtime/turn/session-ops.mjs +2 -2
  265. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  266. package/src/session-runtime/usage-stats-api.mjs +6 -1
  267. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  268. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  269. package/src/standalone/daemon-owner.mjs +117 -25
  270. package/src/standalone/daemon.mjs +43 -13
  271. package/src/standalone/desktop-service-registry.mjs +32 -5
  272. package/src/standalone/session-call-cache.mjs +6 -0
  273. package/src/standalone/session-protocol.mjs +1 -1
  274. package/src/standalone/session-service/retention/busy.mjs +7 -2
  275. package/src/standalone/session-transport/routes.mjs +7 -6
  276. package/src/tui/app/model-options.mjs +39 -2
  277. package/src/tui/app/model-picker.mjs +16 -2
  278. package/src/tui/app/provider-setup-picker.mjs +1 -2
  279. package/src/tui/dist/index.mjs +436 -123
  280. package/src/tui/session/notice-text.mjs +0 -1
  281. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  282. package/src/tui/session/session-api/integrations.mjs +1 -5
  283. package/src/tui/session/session-flow/drain.mjs +8 -0
  284. package/src/tui/session/session-flow/steering.mjs +46 -0
  285. package/src/tui/session/session-flow.mjs +2 -0
  286. package/src/tui/session/turn-stream.mjs +33 -0
  287. package/src/tui/session/turn.mjs +30 -3
  288. package/src/ui/statusline.mjs +1 -6
  289. package/src/ui/streaming-markdown-heal.mjs +94 -25
  290. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  291. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  292. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -4,6 +4,7 @@
4
4
  import { refreshKeychainStatus, retryKeychain } from '../../runtime/shared/config.mjs';
5
5
  import { keychainIssueFor } from '../services/provider-admin.mjs';
6
6
  import { modelProxyClientMode } from '../../runtime/shared/model-proxy-mode.mjs';
7
+ import { providerCatalogFailure } from '../../runtime/agent/orchestrator/providers/catalog-failures.mjs';
7
8
 
8
9
  // The keyring alert must reflect the live state (it can lock or unlock while a
9
10
  // cached setup is served), so overlay it on every ready read.
@@ -14,6 +15,22 @@ function withLiveKeychain(setup) {
14
15
  return keychain ? { ...rest, keychain } : rest;
15
16
  }
16
17
 
18
+ // A provider whose model list failed says why on its row (`catalogError`), so a
19
+ // view never shows its empty list as if nothing were wrong. Overlaid on every
20
+ // read like the keyring alert: the cached setup predates the failure.
21
+ export function withCatalogErrors(setup) {
22
+ if (!setup || typeof setup !== 'object') return setup;
23
+ const out = { ...setup };
24
+ for (const group of ['api', 'oauth', 'local']) {
25
+ if (!Array.isArray(setup[group])) continue;
26
+ out[group] = setup[group].map((row) => {
27
+ const { message, errorKind } = providerCatalogFailure(row?.id) ?? {};
28
+ return message ? { ...row, catalogError: { message, errorKind } } : row;
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+
17
34
  export function createSetupApi({
18
35
  awaitKeychainPrewarm,
19
36
  isKeychainPrewarmReady,
@@ -41,7 +58,7 @@ export function createSetupApi({
41
58
  void Promise.resolve(awaitKeychainPrewarm())
42
59
  .then(() => cachedProviderSetup({}))
43
60
  .catch(() => {});
44
- return { ...quick, pendingSecrets: !isKeychainPrewarmReady() };
61
+ return withCatalogErrors({ ...quick, pendingSecrets: !isKeychainPrewarmReady() });
45
62
  }
46
63
  await awaitKeychainPrewarm();
47
64
  // A keyring the user has since unlocked: rebuild the setup that was cached
@@ -53,7 +70,7 @@ export function createSetupApi({
53
70
  // `recheckKeychain` (window focus) skips the snapshot TTL.
54
71
  const changed = await refreshKeychainStatus({ force: options?.recheckKeychain === true });
55
72
  if (changed) reloadFullConfig();
56
- return withLiveKeychain(await cachedProviderSetup({ force: force || recovered || changed }));
73
+ return withCatalogErrors(withLiveKeychain(await cachedProviderSetup({ force: force || recovered || changed })));
57
74
  },
58
75
  async getUsageDashboard(options = {}) {
59
76
  return await getUsageDashboard(options);
@@ -1,52 +1,128 @@
1
1
  // Provider registries are process-global. Share raw catalog rows across
2
2
  // sessions, while each session owns its saved-settings projection.
3
- let sharedCatalogRevision = -1;
3
+ //
4
+ // One slow or offline provider must not hold, or force a reload of, everyone
5
+ // else's rows. Each provider's model list runs through catalog-deadline.mjs:
6
+ // bounded per provider (a timed-out list becomes an error entry and is
7
+ // aborted where listModels honors `{ signal }`), and single-flight per
8
+ // provider until the underlying work settles. When some but not all providers
9
+ // answer, the answered entries are kept for the catalog state, and each
10
+ // failed entry carries a process-wide `retryAt`: non-forced reads by any
11
+ // session reuse the partial answer and ask a missing provider again only once
12
+ // its backoff passed. Forced reads ask every provider. A load where every
13
+ // provider failed is never kept, so it stays immediately retryable. A
14
+ // provider whose previous list ignored its deadline and is still running is
15
+ // not asked at all (catalog-deadline.mjs): it gets a timed-out entry at
16
+ // once, with the same backoff, until that list settles.
17
+ //
18
+ // The catalog state is the registry revision plus a local epoch that
19
+ // invalidateSharedProviderCatalog() bumps on auth/account changes, so rows
20
+ // listed under the previous account are never served again.
21
+ import {
22
+ boundedProviderCatalogOp,
23
+ bumpProviderCatalogGeneration,
24
+ PROVIDER_CATALOG_TIMEOUT_MS,
25
+ } from '../runtime/agent/orchestrator/providers/catalog-deadline.mjs';
26
+
27
+ export { PROVIDER_CATALOG_TIMEOUT_MS };
28
+
29
+ /** Backoff before a provider missing from a partial catalog is asked again. */
30
+ export const PROVIDER_CATALOG_RETRY_MS = 30_000;
31
+
32
+ let catalogEpoch = 0;
33
+ let sharedCatalogKey = null;
4
34
  let sharedCatalogEntries = null;
35
+ let sharedPartialKey = null;
36
+ let sharedPartialEntries = null;
5
37
  let sharedCatalogPromise = null;
6
- let sharedCatalogPromiseRevision = -1;
38
+ let sharedCatalogPromiseKey = null;
39
+ let sharedCatalogPromiseForced = false;
7
40
 
8
41
  export function catalogRevision(registry) {
9
42
  const value = Number(registry?.providerCatalogRevision?.());
10
43
  return Number.isFinite(value) ? value : 0;
11
44
  }
12
45
 
13
- export async function sharedProviderCatalog(registry) {
14
- const revision = catalogRevision(registry);
15
- if (sharedCatalogRevision === revision && Array.isArray(sharedCatalogEntries)) {
46
+ const catalogKey = (registry) => `${catalogRevision(registry)}:${catalogEpoch}`;
47
+
48
+ /** Drops every shared entry (complete or partial) and opens a new epoch: the
49
+ * next read asks every provider again. Called on auth/account changes. */
50
+ export function invalidateSharedProviderCatalog() {
51
+ catalogEpoch += 1;
52
+ // Pending refreshes and lists of the previous account, of either kind, must
53
+ // not block or fail the new account's work.
54
+ bumpProviderCatalogGeneration();
55
+ sharedCatalogKey = null;
56
+ sharedCatalogEntries = null;
57
+ sharedPartialKey = null;
58
+ sharedPartialEntries = null;
59
+ }
60
+
61
+ const answered = (entry) => !Object.hasOwn(entry, 'error');
62
+
63
+ async function providerEntry(name, provider, timeoutMs, epoch) {
64
+ if (typeof provider?.listModels !== 'function') return { name, models: [], ms: 0 };
65
+ const startedAt = performance.now();
66
+ const outcome = await boundedProviderCatalogOp({
67
+ name,
68
+ provider,
69
+ kind: 'list',
70
+ run: (signal) => provider.listModels({ signal }),
71
+ timeoutMs,
72
+ epoch,
73
+ });
74
+ const ms = performance.now() - startedAt;
75
+ if (outcome.error) {
76
+ return { name, models: [], error: outcome.error, ms, retryAt: Date.now() + PROVIDER_CATALOG_RETRY_MS };
77
+ }
78
+ return { name, models: Array.isArray(outcome.value) ? outcome.value : [], ms };
79
+ }
80
+
81
+ export async function sharedProviderCatalog(registry, { force = false, timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS } = {}) {
82
+ const key = catalogKey(registry);
83
+ if (!force && sharedCatalogKey === key && Array.isArray(sharedCatalogEntries)) {
16
84
  return sharedCatalogEntries;
17
85
  }
18
- if (sharedCatalogPromise && sharedCatalogPromiseRevision === revision) {
86
+ if (sharedCatalogPromise && sharedCatalogPromiseKey === key && (sharedCatalogPromiseForced || !force)) {
19
87
  return await sharedCatalogPromise;
20
88
  }
89
+ const kept = new Map();
90
+ if (!force && sharedPartialKey === key) {
91
+ const now = Date.now();
92
+ for (const entry of sharedPartialEntries) {
93
+ if (answered(entry) || now < entry.retryAt) kept.set(entry.name, entry);
94
+ }
95
+ }
21
96
  const providers = [...registry.getAllProviders()];
22
- sharedCatalogPromiseRevision = revision;
97
+ // Every missing provider is still inside its backoff: the partial answer is
98
+ // the answer, without asking anyone.
99
+ if (providers.length > 0 && providers.every(([name]) => kept.has(name))) {
100
+ return providers.map(([name]) => kept.get(name));
101
+ }
102
+ // A pending list from an older catalog state (revision or account epoch) is
103
+ // not joined: its answer may describe the previous catalog.
104
+ const epoch = key;
105
+ sharedCatalogPromiseKey = key;
106
+ sharedCatalogPromiseForced = force;
23
107
  const request = Promise.all(
24
- providers.map(async ([name, provider]) => {
25
- if (typeof provider?.listModels !== 'function') return { name, models: [], ms: 0 };
26
- const startedAt = performance.now();
27
- try {
28
- const models = await provider.listModels();
29
- return { name, models: Array.isArray(models) ? models : [], ms: performance.now() - startedAt };
30
- } catch (error) {
31
- return { name, models: [], error, ms: performance.now() - startedAt };
32
- }
33
- })
108
+ providers.map(([name, provider]) => kept.get(name) || providerEntry(name, provider, timeoutMs, epoch))
34
109
  )
35
110
  .then((entries) => {
36
- if (
37
- sharedCatalogPromise === request &&
38
- catalogRevision(registry) === revision &&
39
- entries.every((entry) => !Object.hasOwn(entry, 'error'))
40
- ) {
41
- sharedCatalogRevision = revision;
42
- sharedCatalogEntries = entries;
111
+ if (sharedCatalogPromise === request && catalogKey(registry) === key) {
112
+ const complete = entries.every(answered);
113
+ const partial = !complete && entries.some(answered);
114
+ sharedCatalogKey = complete ? key : null;
115
+ sharedCatalogEntries = complete ? entries : null;
116
+ sharedPartialKey = partial ? key : null;
117
+ sharedPartialEntries = partial ? entries : null;
43
118
  }
44
119
  return entries;
45
120
  })
46
121
  .finally(() => {
47
122
  if (sharedCatalogPromise === request) {
48
123
  sharedCatalogPromise = null;
49
- sharedCatalogPromiseRevision = -1;
124
+ sharedCatalogPromiseKey = null;
125
+ sharedCatalogPromiseForced = false;
50
126
  }
51
127
  });
52
128
  sharedCatalogPromise = request;
@@ -17,6 +17,7 @@ export function createCatalogLoader({
17
17
  ensureFullConfig,
18
18
  awaitKeychainPrewarm,
19
19
  ensureProvidersReady,
20
+ providerTimeoutMs,
20
21
  }) {
21
22
  function rowsForEntry({ name, models, error, ms }) {
22
23
  const out = [];
@@ -76,7 +77,10 @@ export function createCatalogLoader({
76
77
  }
77
78
 
78
79
  /** `request` ({ seq }) identifies the caller's load; `request.complete`
79
- * reports whether every provider answered. */
80
+ * reports whether every provider answered, `request.missing` names the
81
+ * providers that failed or timed out, `request.partial` marks a load
82
+ * where some but not all providers answered, and `request.retryAt` is when
83
+ * the first missing provider may be asked again. */
80
84
  return async function loadProviderModelsFresh({ forceRefresh = false, loadSecrets = true, request = null } = {}) {
81
85
  const startedAt = performance.now();
82
86
  profile('load:start', { forceRefresh, loadSecrets });
@@ -93,8 +97,15 @@ export function createCatalogLoader({
93
97
  // forward only for the still-current request, never for a superseded load.
94
98
  if (ownsLoad) request.seq = caches.providerModelsLoadSeq;
95
99
  const seq = request?.seq ?? caches.providerModelsLoadSeq;
96
- const catalogEntries = await sharedProviderCatalog(reg());
97
- if (request) request.complete = catalogEntries.every((entry) => !Object.hasOwn(entry, 'error'));
100
+ const catalogEntries = await sharedProviderCatalog(reg(), { force: forceRefresh, timeoutMs: providerTimeoutMs });
101
+ if (request) {
102
+ const failed = catalogEntries.filter((entry) => Object.hasOwn(entry, 'error'));
103
+ request.missing = failed.map((entry) => entry.name);
104
+ request.complete = failed.length === 0;
105
+ request.partial = !request.complete && failed.length < catalogEntries.length;
106
+ // The shared, process-wide backoff of the soonest missing provider.
107
+ request.retryAt = request.partial ? Math.min(...failed.map((entry) => entry.retryAt)) : null;
108
+ }
98
109
  const results = [];
99
110
  const seen = new Set();
100
111
  for (const row of catalogEntries.flatMap(rowsForEntry)) {
@@ -69,6 +69,10 @@ export function hydrateProviderModelRow(cfg, row) {
69
69
  savedModelParameters: saved.modelParameters || {},
70
70
  savedContextPercent: Number(saved.contextPercent) || undefined,
71
71
  savedContextTokens: Number(saved.contextTokens) || undefined,
72
+ // Model management: an explicit /model visibility choice and the favorite
73
+ // mark (absent = the picker's default listing, not a favorite).
74
+ ...(typeof saved.hidden === 'boolean' ? { savedHidden: saved.hidden } : {}),
75
+ ...(saved.favorite === true ? { favorite: true } : {}),
72
76
  };
73
77
  }
74
78
 
@@ -10,6 +10,7 @@
10
10
  // row-hydration — saved model settings layered onto catalog rows
11
11
  // web-search-catalog — the native web-search model list
12
12
  // catalog-load — one authoritative provider catalog load
13
+ import { PROVIDER_CATALOG_RETRY_MS } from './provider-catalog-cache.mjs';
13
14
  import { createCatalogLoader } from './provider-models/catalog-load.mjs';
14
15
  import { createModelMetaIndex, modelMetaKey } from './provider-models/model-meta.mjs';
15
16
  import { createRowHydration } from './provider-models/row-hydration.mjs';
@@ -19,6 +20,25 @@ const PROVIDER_MODELS_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(
19
20
  String(process.env.MIXDOG_PROVIDER_MODELS_PROFILE || process.env.MIXDOG_BOOT_PROFILE || '')
20
21
  );
21
22
 
23
+ /** How long a partial catalog (some provider failed or timed out) is served
24
+ * as-is before a read retries the missing providers in the background. The
25
+ * backoff is process-wide (provider-catalog-cache.mjs), not per session. */
26
+ export const PARTIAL_CATALOG_RETRY_MS = PROVIDER_CATALOG_RETRY_MS;
27
+
28
+ /** Rows of a partial answer say so explicitly, on quick and full reads alike:
29
+ * `catalogPartial`, the `catalogMissing` providers and `catalogRetryAt`
30
+ * (epoch ms) after which a reader should ask again. Complete answers carry
31
+ * none of these. */
32
+ function withCatalogState(models, partial) {
33
+ if (!partial) return models;
34
+ return models.map((row) => ({
35
+ ...row,
36
+ catalogPartial: true,
37
+ catalogMissing: [...partial.missing],
38
+ catalogRetryAt: partial.retryAt,
39
+ }));
40
+ }
41
+
22
42
  export function createProviderModels({
23
43
  caches,
24
44
  modelMetaByRoute,
@@ -37,6 +57,8 @@ export function createProviderModels({
37
57
  scheduleProviderModelWarmup,
38
58
  // Quick-row helpers wired in after createQuickModelRows resolves.
39
59
  quickHelpers,
60
+ // Per-provider model-list bound; defaults to PROVIDER_CATALOG_TIMEOUT_MS.
61
+ providerCatalogTimeoutMs,
40
62
  }) {
41
63
  const config = () => getConfig();
42
64
  const route = () => getRoute();
@@ -66,17 +88,56 @@ export function createProviderModels({
66
88
  collectProviderModels,
67
89
  ...readiness,
68
90
  });
69
- const loadProviderModelsFresh = createCatalogLoader({ caches, config, reg, meta, rows, profile, ...readiness });
91
+ const loadProviderModelsFresh = createCatalogLoader({
92
+ caches,
93
+ config,
94
+ reg,
95
+ meta,
96
+ rows,
97
+ profile,
98
+ providerTimeoutMs: providerCatalogTimeoutMs,
99
+ ...readiness,
100
+ });
101
+
102
+ // Partial-catalog state of a loaded rows array, for readers that joined the
103
+ // shared load instead of reading the adopted cache.
104
+ const partialLoads = new WeakMap();
105
+ const partialOf = (request) => (request.partial ? { missing: request.missing, retryAt: request.retryAt } : null);
70
106
 
71
107
  function adoptProviderModelCache(models, request, loadSecrets = true) {
72
108
  const revision = meta.syncCatalogRevision();
73
- // No-secrets and failed loads may be partial. Keep them retryable rather
74
- // than turning a transient failure into an authoritative empty catalog.
75
- if (request.seq === caches.providerModelsLoadSeq && loadSecrets && request.complete) {
76
- caches.providerModelsCache = { models, at: Date.now(), revision };
109
+ const partial = partialOf(request);
110
+ if (partial) partialLoads.set(models, partial);
111
+ if (request.seq !== caches.providerModelsLoadSeq || !loadSecrets) return;
112
+ const at = Date.now();
113
+ if (request.complete) {
114
+ caches.providerModelsCache = { models, at, revision };
115
+ } else if (partial) {
116
+ // Some providers answered. Serving that answer beats reloading every
117
+ // provider on each read while one is down; it stays marked partial and
118
+ // its missing providers are retried once the shared backoff passes. A
119
+ // no-secrets, empty or all-failed load stays uncached and retryable
120
+ // rather than becoming an authoritative empty catalog.
121
+ caches.providerModelsCache = { models, at, revision, partial: true, ...partial };
77
122
  }
78
123
  }
79
124
 
125
+ /** Background refill of a partial catalog: only its missing providers are
126
+ * asked again (provider-catalog-cache keeps the ones that answered, and
127
+ * holds back providers still inside the shared backoff), and readers keep
128
+ * the partial rows until the refill lands. */
129
+ function refillPartialCatalog(cached) {
130
+ if (!cached.partial || Date.now() < cached.retryAt || caches.providerModelsPromise) return;
131
+ profile('partial:retry', { missing: cached.missing.join(',') });
132
+ startSharedLoad({
133
+ loadSecrets: true,
134
+ onFailed: (err) => {
135
+ profile('partial:retry-failed', { error: err instanceof Error ? err.message : String(err) });
136
+ return cached.models;
137
+ },
138
+ });
139
+ }
140
+
80
141
  /** The shared in-flight load every concurrent reader joins. `onFailed`, when
81
142
  * given, is part of the shared promise so joiners see its value too;
82
143
  * `caches.providerModelsPromise` is cleared once it settles so a later read
@@ -97,13 +158,17 @@ export function createProviderModels({
97
158
 
98
159
  async function collectProviderModels({ force = false, quick = false } = {}) {
99
160
  meta.syncCatalogRevision();
100
- if (!force && Array.isArray(caches.providerModelsCache.models)) {
101
- const models = rows.providerModelsFromCacheRows(caches.providerModelsCache.models);
161
+ const cached = caches.providerModelsCache;
162
+ if (!force && Array.isArray(cached.models)) {
163
+ refillPartialCatalog(cached);
164
+ const models = rows.providerModelsFromCacheRows(cached.models);
102
165
  // A quick read answered from the loaded catalog already IS the full
103
166
  // catalog. Saying so lets a caller skip the full read it would otherwise
104
167
  // follow up with (a phone paid the whole catalog twice at boot). Only
105
168
  // this answer carries the mark: a full read is complete by definition,
106
- // and an unmarked quick answer keeps meaning "follow up".
169
+ // and an unmarked quick answer keeps meaning "follow up". A partial
170
+ // catalog is never marked complete; it carries its partial state.
171
+ if (cached.partial) return withCatalogState(models, cached);
107
172
  return quick ? models.map((row) => ({ ...row, catalogComplete: true })) : models;
108
173
  }
109
174
  if (!force && quick) {
@@ -117,10 +182,11 @@ export function createProviderModels({
117
182
  const request = { seq: ++caches.providerModelsLoadSeq };
118
183
  const models = await loadProviderModelsFresh({ forceRefresh: true, loadSecrets: true, request });
119
184
  adoptProviderModelCache(models, request);
120
- return rows.providerModelsFromCacheRows(models);
185
+ return withCatalogState(rows.providerModelsFromCacheRows(models), partialOf(request));
121
186
  }
122
187
  if (!caches.providerModelsPromise) startSharedLoad({ loadSecrets: true });
123
- return rows.providerModelsFromCacheRows(await caches.providerModelsPromise);
188
+ const loaded = await caches.providerModelsPromise;
189
+ return withCatalogState(rows.providerModelsFromCacheRows(loaded), partialLoads.get(loaded));
124
190
  }
125
191
 
126
192
  function warmProviderModelCache({ loadSecrets = false } = {}) {
@@ -5,6 +5,7 @@
5
5
  // registry import and the provider-models factory resolve after this gate is
6
6
  // created, while ensureProvidersReady only reaches them on demand.
7
7
  import { bootProfile } from './boot-profile.mjs';
8
+ import { invalidateSharedProviderCatalog } from './provider-catalog-cache.mjs';
8
9
  import { providerInitCacheKey } from './provider-init-key.mjs';
9
10
 
10
11
  const KEYCHAIN_PREWARM_WAIT_MS = 5000;
@@ -52,6 +53,9 @@ export function createProviderReadiness({ rt, keychain, getReg, getWarmProviderM
52
53
  const providerInitPromises = new Map();
53
54
 
54
55
  function invalidateProviderCaches(options = {}) {
56
+ // Auth/account/config changes alter what providers list: the process-wide
57
+ // catalog (partial rows and retry backoffs included) starts a new epoch.
58
+ invalidateSharedProviderCatalog();
55
59
  Object.assign(providerModelCaches, emptyModelCaches());
56
60
  providerModelCaches.providerModelsLoadSeq += 1;
57
61
  Object.assign(providerUsageCaches, emptyUsageCaches());
@@ -8,6 +8,7 @@ import {
8
8
  import { agentScope, clean, clearAgentStatuslineRoute } from './helpers.mjs';
9
9
  import { isLeadPoolAgent } from './worker-rows.mjs';
10
10
  import { createCloseAll } from './close-flow/close-all.mjs';
11
+ import { closeManagedSession } from '../../lifecycle/shared.mjs';
11
12
 
12
13
  const CLOSE_REASON = 'cancelled by agent close';
13
14
 
@@ -68,7 +69,7 @@ export function createCloseFlow({ mgr, registry, views }) {
68
69
  // below is an idempotent no-op fallback once terminal.
69
70
  // Closing a runtime cancels its work; only explicit session deletion
70
71
  // may tombstone the conversation.
71
- const ok = await mgr.closeSession(sessionId, 'cli-agent-close', { tombstone: false });
72
+ const ok = await closeManagedSession(mgr, sessionId, 'cli-agent-close', { tombstone: false });
72
73
  if (task?.taskId) cancelBackgroundTask(task.taskId, CLOSE_REASON);
73
74
  // Drop the row only once the cancellation is DURABLY recorded: a failed
74
75
  // close that still forgot the row would leave no cancelled state behind.
@@ -76,13 +76,21 @@ export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntr
76
76
  return function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
77
77
  refreshTagsFromSessions({ scanSessions, context });
78
78
  const now = Date.now();
79
- const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session, indexRow }) =>
79
+ const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session, indexRow, loaded }) =>
80
80
  agentRow({
81
81
  tag,
82
82
  session,
83
83
  indexRow,
84
84
  runtime: mgr.getSessionRuntime?.(session.id),
85
- progress: progress.sessionProgressExtras(session.id, session.agent || null, now),
85
+ // A tag-bound entry is the getSession answer itself; an index row or
86
+ // a scanned summary is only a projection and still reads the session.
87
+ progress: progress.sessionProgressExtras(
88
+ session.id,
89
+ session.agent || null,
90
+ now,
91
+ null,
92
+ loaded ? { loaded: session } : {}
93
+ ),
86
94
  now,
87
95
  })
88
96
  );
@@ -58,9 +58,16 @@ export function liveAgentStatusRow(row) {
58
58
  }
59
59
 
60
60
  export function createSessionProgress({ mgr }) {
61
- function sessionProgressExtras(sessionId, role, now = Date.now(), taskStatus = null) {
61
+ /** `loaded`: the caller's own getSession(sessionId) answer from this same
62
+ * read, so the session is not loaded a second time. */
63
+ function sessionProgressExtras(sessionId, role, now = Date.now(), taskStatus = null, { loaded } = {}) {
62
64
  if (!sessionId) return {};
63
- const session = mgr.getSession(sessionId);
65
+ // Only the session's status is read here: the manager's stamp-validated
66
+ // status lookup spares a whole-transcript read per idle worker per pulse.
67
+ let sessionStatus;
68
+ if (loaded !== undefined) sessionStatus = loaded?.status || null;
69
+ else if (typeof mgr.loadSessionStatus === 'function') sessionStatus = mgr.loadSessionStatus(sessionId) || null;
70
+ else sessionStatus = mgr.getSession(sessionId)?.status || null;
64
71
  const runtime = mgr.getSessionRuntime?.(sessionId) || null;
65
72
  const snapshot =
66
73
  typeof mgr.getSessionProgressSnapshot === 'function' ? mgr.getSessionProgressSnapshot(sessionId) : null;
@@ -72,8 +79,8 @@ export function createSessionProgress({ mgr }) {
72
79
  typeof mgr.getSessionPendingMessageDepth === 'function' ? mgr.getSessionPendingMessageDepth(sessionId) : null;
73
80
  return buildAgentTaskProgressFields({
74
81
  now,
75
- sessionStatus: session?.status || null,
76
- runtimeStage: runtime?.stage || snapshot?.stage || session?.status || null,
82
+ sessionStatus,
83
+ runtimeStage: runtime?.stage || snapshot?.stage || sessionStatus,
77
84
  snapshot,
78
85
  runtime,
79
86
  policy,
@@ -96,6 +103,8 @@ export function createSessionProgress({ mgr }) {
96
103
  stage: progress.worker_stage || runtime?.stage || status,
97
104
  // Additive: present only while the worker's turn waits on a user approval.
98
105
  ...(runtime?.awaitingUser ? { awaitingUser: runtime.awaitingUser } : {}),
106
+ // Additive: present only while the worker's turn sits in `task wait`.
107
+ ...(runtime?.taskWaits > 0 ? { awaitingTask: 'wait' } : {}),
99
108
  clientHostPid: session.clientHostPid || null,
100
109
  lastStreamDeltaAt: runtime?.lastStreamDeltaAt ? new Date(runtime.lastStreamDeltaAt).toISOString() : null,
101
110
  ...progress,
@@ -9,7 +9,7 @@ import {
9
9
  import { clean } from './helpers.mjs';
10
10
  import { createEarlyCompletionNotice } from './notify/early-completion.mjs';
11
11
 
12
- export function createNotify(mgr, { notifySessionCompletion } = {}) {
12
+ export function createNotify(mgr, { notifySessionCompletion, onWorkerCompletionQueued = null } = {}) {
13
13
  function enqueueCompletionMessage(sessionId, text, meta = {}) {
14
14
  const target = clean(sessionId);
15
15
  if (!target || typeof mgr.enqueuePendingMessage !== 'function') return false;
@@ -28,8 +28,8 @@ export function createNotify(mgr, { notifySessionCompletion } = {}) {
28
28
  logDuplicateSkip('notify-enqueue', { executionId: meta?.execution_id, text: visible });
29
29
  return true;
30
30
  }
31
- // Mark this as a deferred completion/task notification so a later session
32
- // resume drops it rather than replaying it out-of-order (owner decision).
31
+ // Mark this as a completion/task notification: delivery dedupes it by
32
+ // execution id, and it stays queued (durably) until the model gets it.
33
33
  return Boolean(
34
34
  mgr.enqueuePendingMessage(
35
35
  target,
@@ -67,7 +67,18 @@ export function createNotify(mgr, { notifySessionCompletion } = {}) {
67
67
  // appear as a second Lead-level completion card. Agent task completion
68
68
  // has its own owner delivery path below; only that terminal handoff
69
69
  // crosses the parent boundary.
70
- return workerId ? enqueueCompletionMessage(workerId, text, meta) : false;
70
+ if (!workerId) return false;
71
+ const queued = enqueueCompletionMessage(workerId, text, meta);
72
+ // A busy worker takes the completion at its next tool boundary; an idle
73
+ // one is woken so the result reaches it now, not at its next message.
74
+ if (queued && typeof onWorkerCompletionQueued === 'function') {
75
+ setImmediate(() => {
76
+ try {
77
+ onWorkerCompletionQueued(workerId);
78
+ } catch {}
79
+ });
80
+ }
81
+ return queued;
71
82
  };
72
83
  }
73
84