mixdog 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  3. package/scripts/gate-local.mjs +12 -3
  4. package/scripts/latest-guard.mjs +21 -9
  5. package/scripts/lib/isolated-test-env.mjs +10 -5
  6. package/scripts/lib/run-node-tests.mjs +37 -37
  7. package/scripts/lib/scoped-temp.d.mts +16 -0
  8. package/scripts/lib/scoped-temp.mjs +123 -0
  9. package/scripts/lib/stale-temp.mjs +44 -0
  10. package/scripts/post-release-check.mjs +1 -6
  11. package/scripts/pptx-kit-index.mjs +17 -4
  12. package/scripts/suite-health.mjs +10 -1
  13. package/scripts/test-data-dir.mjs +10 -7
  14. package/src/app.mjs +4 -0
  15. package/src/defaults/shipped-history.json +5 -0
  16. package/src/defaults/skills/browser-use/SKILL.md +6 -4
  17. package/src/defaults/skills/computer-use/SKILL.md +4 -2
  18. package/src/lib/rules-builder.cjs +29 -12
  19. package/src/output-styles/common.md +2 -2
  20. package/src/output-styles/simple.md +1 -1
  21. package/src/rules/agent/AGENT.md +5 -4
  22. package/src/rules/lead/LEAD.md +10 -5
  23. package/src/rules/shared/10-tool-workflow.md +3 -1
  24. package/src/rules/shared/45-destructive.md +0 -2
  25. package/src/rules/shared/50-execution.md +2 -0
  26. package/src/rules/shared/70-delivery.md +7 -9
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
  28. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  36. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  37. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
  47. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  49. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  50. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  51. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  62. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  63. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  64. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  65. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
  68. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  76. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  77. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
  78. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  79. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  80. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  81. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  82. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  83. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  84. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
  85. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
  86. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  87. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
  89. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  90. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  92. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  93. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  97. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  99. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
  102. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
  103. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
  104. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
  105. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
  106. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  107. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  108. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  110. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  111. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  112. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  115. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  116. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  118. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  121. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  123. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
  125. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
  126. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  136. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  137. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
  138. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  139. package/src/runtime/channels/lib/whisper-port.mjs +10 -1
  140. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  141. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  142. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  143. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  144. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  145. package/src/runtime/local-provider/server-process.mjs +2 -1
  146. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  147. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  148. package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
  149. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  150. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  151. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  152. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  153. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  154. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  155. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  156. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  157. package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
  158. package/src/runtime/office/quality/design-review.mjs +1 -5
  159. package/src/runtime/office/quality/document-brief.mjs +1 -5
  160. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  161. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  162. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  163. package/src/runtime/shared/defaults-separation.mjs +1 -4
  164. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  165. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  166. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  167. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  168. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  169. package/src/runtime/shared/llm/cost.mjs +5 -1
  170. package/src/runtime/shared/loopback-listener.mjs +25 -2
  171. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  172. package/src/runtime/shared/model-route-chain.mjs +22 -4
  173. package/src/runtime/shared/open-url.mjs +10 -0
  174. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  175. package/src/runtime/shared/session-scratch.mjs +37 -0
  176. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  177. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  178. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  179. package/src/runtime/shared/tool-surface.mjs +3 -0
  180. package/src/runtime/shared/tool-work-units.mjs +2 -0
  181. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  182. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  183. package/src/runtime/shared/worktrees/git.mjs +15 -0
  184. package/src/runtime/shared/worktrees/manager.mjs +87 -38
  185. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  186. package/src/runtime/visualize-bridge/client.mjs +2 -0
  187. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  188. package/src/session-runtime/agent-task-sessions.mjs +152 -63
  189. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  190. package/src/session-runtime/boot/tools.mjs +5 -1
  191. package/src/session-runtime/bot-status.mjs +12 -6
  192. package/src/session-runtime/bot-target.mjs +38 -0
  193. package/src/session-runtime/channel-config-api.mjs +6 -1
  194. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  195. package/src/session-runtime/conversation-import/discovery.mjs +271 -25
  196. package/src/session-runtime/conversation-import/service.mjs +26 -5
  197. package/src/session-runtime/handoff-attachments.mjs +82 -31
  198. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  199. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  200. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  201. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
  202. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  203. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  204. package/src/session-runtime/model-route/set-route.mjs +11 -2
  205. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  206. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  207. package/src/session-runtime/provider-models.mjs +4 -2
  208. package/src/session-runtime/resource-recall-api.mjs +6 -1
  209. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  210. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  211. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  212. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  213. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  214. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  215. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  216. package/src/session-runtime/services/provider-admin.mjs +2 -1
  217. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  218. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  219. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
  220. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  221. package/src/session-runtime/workspace/transitions.mjs +25 -0
  222. package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
  223. package/src/standalone/daemon-session-control.mjs +18 -2
  224. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  225. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  226. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  227. package/src/tui/app/app-view/floating-panel.jsx +7 -4
  228. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  229. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  230. package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
  231. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  232. package/src/tui/app/provider-setup-picker.mjs +9 -5
  233. package/src/tui/components/QuestionPanel.jsx +13 -4
  234. package/src/tui/dist/index.mjs +75 -28
  235. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  236. package/src/ui/statusline-agents.mjs +1 -3
@@ -66,15 +66,20 @@ export function createAgentExecute({
66
66
  });
67
67
  }
68
68
  // Targetless status is the same compact overview as list.
69
- return renderResult({
70
- // Idle workers stay open for follow-ups; the model must see them to close them.
71
- workers: views.list({
72
- scanSessions: registry.wantsSessionScan(args),
73
- context: scopedContext,
74
- includeIdle: true,
75
- }),
76
- jobs: views.listJobs(scopedContext),
77
- });
69
+ return renderResult(
70
+ {
71
+ // Idle workers stay open for follow-ups; the roster counts them and
72
+ // lists a bounded tail (or the `tag` matches) so they can be closed.
73
+ workers: views.list({
74
+ scanSessions: registry.wantsSessionScan(args),
75
+ context: scopedContext,
76
+ includeIdle: true,
77
+ includeClosed: true,
78
+ }),
79
+ jobs: views.listJobs(scopedContext),
80
+ },
81
+ { listFilter: clean(args.tag) }
82
+ );
78
83
  case 'read':
79
84
  return await readJob(args, scopedContext);
80
85
  case 'cleanup':
@@ -27,8 +27,12 @@ export function isActiveWorkerRow(row = {}) {
27
27
 
28
28
  // A finished worker that is still open: it takes follow-ups and can be closed.
29
29
  function isOpenIdleRow(row = {}) {
30
- const status = String(row.status ?? '').trim().toLowerCase();
31
- const stage = String(row.stage ?? '').trim().toLowerCase();
30
+ const status = String(row.status ?? '')
31
+ .trim()
32
+ .toLowerCase();
33
+ const stage = String(row.stage ?? '')
34
+ .trim()
35
+ .toLowerCase();
32
36
  if (CANCELLED_WORKER_STATUS.test(stage) || CANCELLED_WORKER_STATUS.test(status)) return false;
33
37
  return (status || stage) === 'idle';
34
38
  }
@@ -86,7 +90,13 @@ function agentRow({ tag, session, runtime, progress, now, indexRow = null }) {
86
90
  }
87
91
 
88
92
  export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntries, progress }) {
89
- return function list({ scanSessions = false, context = {}, includeTerminal = false, includeIdle = false } = {}) {
93
+ return function list({
94
+ scanSessions = false,
95
+ context = {},
96
+ includeTerminal = false,
97
+ includeIdle = false,
98
+ includeClosed = false,
99
+ } = {}) {
90
100
  refreshTagsFromSessions({ scanSessions, context });
91
101
  const now = Date.now();
92
102
  const rows = [];
@@ -98,7 +108,13 @@ export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntr
98
108
  // 2 s) would otherwise read every worker it ever ran, whole transcripts
99
109
  // on first sight, synchronously on the daemon loop.
100
110
  const state = rowState(session, runtime);
101
- if (!includeTerminal && !isActiveWorkerRow(state) && !(includeIdle && isOpenIdleRow(state))) continue;
111
+ if (
112
+ !includeTerminal &&
113
+ !isActiveWorkerRow(state) &&
114
+ !(includeIdle && isOpenIdleRow(state)) &&
115
+ !(includeClosed && state.status === 'closed')
116
+ )
117
+ continue;
102
118
  rows.push(
103
119
  agentRow({
104
120
  tag,
@@ -1,9 +1,10 @@
1
1
  // One-off model selection for a spawn the user explicitly pinned to a model.
2
2
  // The agent's preset route stays the default; this only runs when `model` is
3
- // passed. The query is matched against the live provider catalog — exact id,
4
- // then exact display name, then every query word — and a catalog miss retries
5
- // once on a refreshed catalog. One match runs; several or none fail with
6
- // candidates instead of substituting another model.
3
+ // passed. The query is matched against the quick/cached provider catalog —
4
+ // literal id, then normalized id, then exact display name, then every query
5
+ // word — and a catalog miss retries once on a catalog refreshed for the
6
+ // requested provider (concurrent misses share that refresh). One match runs;
7
+ // several or none fail with candidates instead of substituting another model.
7
8
  import { clean } from './helpers.mjs';
8
9
 
9
10
  const CANDIDATE_LIMIT = 8;
@@ -20,7 +21,7 @@ const terms = (value) =>
20
21
  .filter(Boolean);
21
22
  const label = (row) => `${row.provider}/${row.id}`;
22
23
 
23
- function providerRows(rows, provider) {
24
+ export function providerRows(rows, provider) {
24
25
  if (!provider) return rows;
25
26
  const wanted = norm(provider);
26
27
  const exact = rows.filter((row) => norm(row.provider) === wanted);
@@ -33,6 +34,8 @@ function matchModels(rows, query) {
33
34
  // Nothing to match on ("???"): `[].every` would accept every row.
34
35
  if (!words.length) return [];
35
36
  const tiers = [
37
+ // A literal id wins over its normalized look-alikes (`foo-bar` vs `foo_bar`).
38
+ (row) => row.id === query,
36
39
  (row) => norm(row.id) === wanted,
37
40
  (row) => norm(row.display) === wanted,
38
41
  (row) => words.every((word) => `${norm(row.id)} ${norm(row.display)}`.includes(word)),
@@ -49,6 +52,34 @@ function candidates(rows) {
49
52
  return rows.length > CANDIDATE_LIMIT ? `${shown}, … (${rows.length} total)` : shown;
50
53
  }
51
54
 
55
+ // A quick read answered from the loaded catalog marks every row complete.
56
+ // Otherwise a hit stands only when it is a literal id of the exact provider the
57
+ // caller named, which the full catalog would pick the same way.
58
+ function quickAnswerHolds(rows, hits, model, provider) {
59
+ if (rows.length > 0 && rows.every((row) => row.catalogComplete === true)) return true;
60
+ const wanted = norm(provider);
61
+ return hits.length > 0 && wanted !== '' && hits.every((row) => row.id === model && norm(row.provider) === wanted);
62
+ }
63
+
64
+ // The forced refresh in flight per catalog reader and provider scope: concurrent
65
+ // misses share it instead of each refreshing.
66
+ const refreshing = new WeakMap();
67
+
68
+ // One forced refresh, scoped to the requested provider when one is given.
69
+ function refreshedRows(listProviderModels, provider) {
70
+ if (!refreshing.has(listProviderModels)) refreshing.set(listProviderModels, new Map());
71
+ const inflight = refreshing.get(listProviderModels);
72
+ const scope = norm(provider);
73
+ if (!inflight.has(scope)) {
74
+ const request = listProviderModels({ force: true, ...(provider ? { provider } : {}) });
75
+ inflight.set(
76
+ scope,
77
+ request.finally(() => inflight.delete(scope))
78
+ );
79
+ }
80
+ return inflight.get(scope);
81
+ }
82
+
52
83
  /** Resolve args.provider/args.model to one exact catalog route. Without a
53
84
  * catalog (headless/daemon surfaces) exact provider + model pass through. */
54
85
  export async function resolveRequestedModel(args, listProviderModels) {
@@ -64,10 +95,20 @@ export async function resolveRequestedModel(args, listProviderModels) {
64
95
  if (!terms(model).length) throw new Error(`agent spawn: model "${model}" for provider "${provider}" not found`);
65
96
  return args;
66
97
  }
67
- let pool = providerRows(await listProviderModels(), provider);
98
+ // The quick/cached catalog answers first. A loaded catalog is final, so its
99
+ // hit never waits on a provider; an incomplete one (cold or partial) may
100
+ // lack the model that wins, or one that makes the name ambiguous, so only a
101
+ // literal id inside an exact, explicitly named provider is taken from it.
102
+ // Anything else waits for the full catalog.
103
+ const quickRows = await listProviderModels({ quick: true });
104
+ let pool = providerRows(quickRows, provider);
68
105
  let hits = matchModels(pool, model);
106
+ if (!quickAnswerHolds(quickRows, hits, model, provider)) {
107
+ pool = providerRows(await listProviderModels(), provider);
108
+ hits = matchModels(pool, model);
109
+ }
69
110
  if (!hits.length) {
70
- pool = providerRows(await listProviderModels({ force: true }), provider);
111
+ pool = providerRows(await refreshedRows(listProviderModels, provider), provider);
71
112
  hits = matchModels(pool, model);
72
113
  }
73
114
  if (hits.length === 1) return { ...args, provider: hits[0].provider, model: hits[0].id };
@@ -1,5 +1,7 @@
1
1
  // Pure result-rendering and finish-classification helpers.
2
2
  import { oneLine } from '../../../runtime/shared/clean.mjs';
3
+ import { isActiveWorkerRow } from './job-views/agent-list.mjs';
4
+ import { isTerminalJobStatus } from './job-views/session-progress.mjs';
3
5
  import { compactIso, elapsedFromStamps, stripFinalAnswerWrapper } from './helpers.mjs';
4
6
 
5
7
  // A worker that gets truncated mid-synthesis or produces an empty terminal
@@ -32,25 +34,77 @@ export function abnormalEmptyFinishError(result, agent) {
32
34
  }
33
35
  }
34
36
 
35
- // The /agents roster: worker rows then task rows.
36
- function renderRoster(value) {
37
+ const ROSTER_TAIL = 5;
38
+ const ROSTER_MATCH_CAP = 25;
39
+
40
+ function workerLine(worker) {
41
+ const status = worker.status || 'idle';
42
+ const stage = worker.worker_stage || worker.stage;
43
+ const progress = oneLine(worker.last_progress);
44
+ const summary = progress.length > 60 ? `${progress.slice(0, 59)}…` : progress;
45
+ return `- ${worker.tag} ${status}${stage && stage !== status ? `/${stage}` : ''}${summary ? ` ${summary}` : ''}`;
46
+ }
47
+
48
+ function jobLine(job) {
49
+ const error = job.status === 'failed' ? oneLine(job.error) : '';
50
+ return `- ${job.task_id} ${job.type} ${job.status} ${job.tag || '-'}${error ? ` error=${error}` : ''}`;
51
+ }
52
+
53
+ const stamp = (row) => String(row.updatedAt || row.lastUsedAt || row.finishedAt || row.startedAt || '');
54
+
55
+ // Inactive rows: newest first (stable for rows without stamps).
56
+ const newestFirst = (rows) => rows.slice().sort((a, b) => stamp(b).localeCompare(stamp(a)));
57
+
58
+ // Inactive rows collapse to a count plus a short tail. A `tag` filter lists the
59
+ // matching inactive rows (bounded) so a specific one can still be found.
60
+ function inactiveSection(label, rows, render, tagOf, filter) {
61
+ if (rows.length === 0) return [];
62
+ const needle = filter.toLowerCase();
63
+ const matched = needle
64
+ ? rows.filter((row) =>
65
+ String(tagOf(row) ?? '')
66
+ .toLowerCase()
67
+ .includes(needle)
68
+ )
69
+ : null;
70
+ const shown = newestFirst(matched ?? rows).slice(0, matched ? ROSTER_MATCH_CAP : ROSTER_TAIL);
71
+ const shownFilter = filter.replace(/\s+/g, ' ');
72
+ const head = matched
73
+ ? `${label}: ${rows.length} (${matched.length} match tag "${shownFilter}", showing ${shown.length})`
74
+ : `${label}: ${rows.length} (latest ${shown.length}; pass tag=<text> to find one)`;
75
+ // Bulleted: a non-bullet line would make the list read as an authored response.
76
+ return [`- ${head}`, ...shown.map(render)];
77
+ }
78
+
79
+ // The /agents roster: active workers and running tasks in full; idle/closed
80
+ // workers and finished tasks as counts with a bounded tail.
81
+ function renderRoster(value, filter = '') {
37
82
  const workers = Array.isArray(value.workers) ? value.workers : [];
38
83
  const jobs = Array.isArray(value.jobs) ? value.jobs : [];
39
84
  const lines = [`agents: ${workers.length} · tasks: ${jobs.length}`];
85
+ const isActive = (worker) => Boolean(worker.awaitingUser) || isActiveWorkerRow(worker);
86
+ const isRunning = (job) => !isTerminalJobStatus(job.status);
40
87
  // Keep list bullets: agent cards distinguish these rows from authored responses.
41
- for (const worker of workers) {
42
- const status = worker.status || 'idle';
43
- const stage = worker.worker_stage || worker.stage;
44
- const progress = oneLine(worker.last_progress);
45
- const summary = progress.length > 60 ? `${progress.slice(0, 59)}…` : progress;
46
- lines.push(
47
- `- ${worker.tag} ${status}${stage && stage !== status ? `/${stage}` : ''}${summary ? ` ${summary}` : ''}`
48
- );
49
- }
50
- for (const job of jobs) {
51
- const error = job.status === 'failed' ? oneLine(job.error) : '';
52
- lines.push(`- ${job.task_id} ${job.type} ${job.status} ${job.tag || '-'}${error ? ` error=${error}` : ''}`);
53
- }
88
+ lines.push(...workers.filter(isActive).map(workerLine));
89
+ lines.push(...jobs.filter(isRunning).map(jobLine));
90
+ lines.push(
91
+ ...inactiveSection(
92
+ 'idle/closed agents',
93
+ workers.filter((w) => !isActive(w)),
94
+ workerLine,
95
+ (w) => w.tag,
96
+ filter
97
+ )
98
+ );
99
+ lines.push(
100
+ ...inactiveSection(
101
+ 'finished tasks',
102
+ jobs.filter((j) => !isRunning(j)),
103
+ jobLine,
104
+ (j) => j.tag || j.task_id,
105
+ filter
106
+ )
107
+ );
54
108
  if (workers.length === 0 && jobs.length === 0) lines.push('(no agents or tasks)');
55
109
  return lines.join('\n');
56
110
  }
@@ -151,11 +205,11 @@ function renderContent(value) {
151
205
  return `${header}\n${stripFinalAnswerWrapper(value.content)}`;
152
206
  }
153
207
 
154
- export function renderResult(value, { includeDiagnostics = false } = {}) {
208
+ export function renderResult(value, { includeDiagnostics = false, listFilter = '' } = {}) {
155
209
  if (value === undefined || value === null) return '';
156
210
  if (typeof value === 'string') return value;
157
211
  if (value && typeof value === 'object') {
158
- if (Array.isArray(value.workers) || Array.isArray(value.jobs)) return renderRoster(value);
212
+ if (Array.isArray(value.workers) || Array.isArray(value.jobs)) return renderRoster(value, listFilter);
159
213
  // close-flow results carry `closed` plus task_id: render the close outcome.
160
214
  // close-flow results carry `closed` plus task_id: render the close outcome.
161
215
  if (value.closed !== undefined) return renderClosed(value);
@@ -1,4 +1,5 @@
1
1
  import { configuredAgentRouteCandidates } from '../../../runtime/shared/agent-route-config.mjs';
2
+ import { executionFallbacks } from '../../../runtime/shared/model-route-chain.mjs';
2
3
  import {
3
4
  agentPresetName,
4
5
  clean,
@@ -29,8 +30,11 @@ export function resolveAgentSpawnPreset(config, args = {}) {
29
30
  const agentName = normalizeAgentName(args.agent);
30
31
  // The agent's stored route: config.agents, or a store bot's own folder.
31
32
  const candidates = !clean(args.preset) ? configuredAgentRouteCandidates(config, agentName) : [];
32
- const agentRoute = candidates.map((candidate) => normalizeAgentRoute(candidate)).find(Boolean) || null;
33
- const storedFallbacks = candidates.find((candidate) => normalizeAgentRoute(candidate))?.fallbacks;
33
+ const storedCandidate = candidates.find((candidate) => normalizeAgentRoute(candidate));
34
+ const agentRoute = storedCandidate ? normalizeAgentRoute(storedCandidate) : null;
35
+ const storedFallbacks = agentRoute
36
+ ? executionFallbacks({ ...agentRoute, fallbacks: storedCandidate.fallbacks })
37
+ : undefined;
34
38
  // Where the spawned session reads its fallback chain at each hop
35
39
  // (route-fallback-chain.mjs): this agent's route, else Main's.
36
40
  const fallbackSource = { agent: agentName };
@@ -28,7 +28,7 @@ export const AGENT_TOOL = {
28
28
  agentHidden: false,
29
29
  },
30
30
  description:
31
- 'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Wait for completion notifications; use status/read only for manual recovery. Close finished agents yourself.',
31
+ 'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Completion notifications arrive automatically; use status/read only for manual recovery. Close finished agents yourself.',
32
32
  inputSchema: {
33
33
  type: 'object',
34
34
  properties: {
@@ -36,21 +36,21 @@ export const AGENT_TOOL = {
36
36
  type: 'string',
37
37
  enum: ['spawn', 'send', 'list', 'close', 'cancel', 'status', 'read', 'cleanup'],
38
38
  description:
39
- 'Required action. New spawn requires agent and one of prompt/message/file; send requires tag and one of prompt/message/file; read/status/close/cancel require task_id or tag; list/cleanup require no target.',
39
+ 'Required action. New spawn requires agent and one of prompt/message/file; send requires tag and one of prompt/message/file; read/status/close/cancel require task_id or tag; list/cleanup require no target. list shows active agents and running tasks in full; idle/closed agents and finished tasks are counts plus a short latest tail (list with tag=<text> shows matching ones: case-insensitive substring, at most 25 per section).',
40
40
  },
41
- task_id: { type: 'string', description: 'Task ID returned by spawn/send; target for read/status/close/cancel.' },
41
+ task_id: { type: 'string', description: 'Task ID returned by spawn/send.' },
42
42
  agent: {
43
43
  type: 'string',
44
- description: 'Workflow agent id. Required for a new spawn; same-tag reuse can inherit it.',
44
+ description: 'Workflow agent id; same-tag reuse can inherit it.',
45
45
  },
46
46
  tag: {
47
47
  type: 'string',
48
48
  description:
49
- 'Stable scope handle. Reuse the same tag for the same scope; never derive a variant by appending a number or suffix. send requires it, while spawn can generate one when omitted.',
49
+ 'Stable scope handle; never derive a variant by appending a number or suffix. spawn generates one when omitted.',
50
50
  },
51
- prompt: { type: 'string', description: 'Spawn task brief; also accepted for send.' },
52
- message: { type: 'string', description: 'Send follow-up; also accepted for spawn.' },
53
- file: { type: 'string', description: 'Path to task text; alternative to prompt/message for spawn/send.' },
51
+ prompt: { type: 'string', description: 'Spawn task brief.' },
52
+ message: { type: 'string', description: 'Send follow-up.' },
53
+ file: { type: 'string', description: 'Path to task text; used instead of prompt/message.' },
54
54
  cwd: { type: 'string', description: 'Project directory for a new or respawned agent.' },
55
55
  context: { type: 'string', description: 'Extra context for the spawn/send turn.' },
56
56
  attachments: {
@@ -63,7 +63,7 @@ export const AGENT_TOOL = {
63
63
  model: {
64
64
  type: 'string',
65
65
  description:
66
- "Spawn only, and only when the user explicitly names a model: runs this one spawn on it instead of the agent's preset route, which stays the default (omit otherwise). Id, display name, or words of it; ambiguous or unknown names fail with candidates. The agent still supplies role and permissions.",
66
+ "Spawn only, and only when the user explicitly names a model: runs this one spawn on it instead of the agent's preset route. Id, display name, or words of it; ambiguous or unknown names fail with candidates. The agent still supplies role and permissions.",
67
67
  },
68
68
  provider: { type: 'string', description: 'Narrows model to one provider; requires model.' },
69
69
  effort: { type: 'string', description: 'Reasoning effort for an explicit model.' },
@@ -1179,7 +1179,8 @@ export async function saveCustomProvider(cfgMod, rawInput) {
1179
1179
  export async function saveCustomProviderKey(cfgMod, rawId, rawKey) {
1180
1180
  requireLocalProviderManagement();
1181
1181
  const id = String(rawId || '').trim();
1182
- if (typeof rawKey !== 'string' || rawKey.length > CUSTOM_API_KEY_MAX) throw new TypeError('custom provider apiKey is invalid');
1182
+ if (typeof rawKey !== 'string' || rawKey.length > CUSTOM_API_KEY_MAX)
1183
+ throw new TypeError('custom provider apiKey is invalid');
1183
1184
  const key = rawKey.trim();
1184
1185
  if (!key) throw new Error('API key is required');
1185
1186
  const takesStoredKey = () => {
@@ -135,6 +135,7 @@ export function createPolicyStage({
135
135
  : _buildBaseRules({
136
136
  omitTools: [...denied, unusedModelEditToolName(getRoute()?.model)],
137
137
  allowTools: schemaAllowedTools,
138
+ ownerIsAgent,
138
139
  provider: getRoute()?.provider,
139
140
  model: getRoute()?.model,
140
141
  });
@@ -19,7 +19,11 @@ import {
19
19
  writeJsonAtomicSync,
20
20
  } from '../../runtime/shared/atomic-file.mjs';
21
21
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
22
- import { agentEntryFile, agentEntryPath, parseManifestText } from '../../runtime/agent/orchestrator/runtime-core/workflow/agents.mjs';
22
+ import {
23
+ agentEntryFile,
24
+ agentEntryPath,
25
+ parseManifestText,
26
+ } from '../../runtime/agent/orchestrator/runtime-core/workflow/agents.mjs';
23
27
  import { hasOwn } from '../../runtime/shared/object.mjs';
24
28
  import { serializeFrontmatterDoc } from '../../runtime/shared/markdown-frontmatter.mjs';
25
29
  import { isHiddenAgent } from '../../runtime/agent/orchestrator/internal-agents.mjs';
@@ -22,7 +22,11 @@ import {
22
22
 
23
23
  /** Mixy's runtime id (agent-home.mjs MIXY_AGENT_ID). */
24
24
  const MIXY_RUNTIME_ID = 'lead';
25
- import { routeChain, validateRouteChainProviders } from '../../runtime/shared/model-route-chain.mjs';
25
+ import {
26
+ canonicalRouteProvider,
27
+ routeChain,
28
+ validateRouteChainProviders,
29
+ } from '../../runtime/shared/model-route-chain.mjs';
26
30
 
27
31
  // Agent route persistence. Route-scope isolation: an agent route owns its
28
32
  // model AND its effort/fast, stored in config.agents[<id>]. The shared
@@ -183,18 +187,25 @@ export function createAgentRouteApi(deps) {
183
187
  }
184
188
  if (!clean(requested.provider)) return clearOverride(id);
185
189
  if (!clean(requested.model)) throw new Error('agent route requires provider and model');
190
+ // An explicit list is validated; the stored one may hold a fallback on the
191
+ // picked model's provider, which stays saved untouched and inactive.
192
+ const explicitFallbacks = hasOwn(requested, 'fallbacks');
186
193
  const chain = routeChain({
187
194
  ...requested,
188
- fallbacks: hasOwn(requested, 'fallbacks') ? requested.fallbacks : stored.fallbacks,
195
+ fallbacks: explicitFallbacks ? requested.fallbacks : stored.fallbacks,
189
196
  });
190
- validateRouteChainProviders(chain);
197
+ validateRouteChainProviders(chain, { allowDuplicates: !explicitFallbacks });
191
198
  const storedChain = routeChain(stored);
192
199
  const resolved = [];
200
+ const usedProviders = new Set();
193
201
  for (const entry of chain) {
202
+ const provider = canonicalRouteProvider(entry.provider);
203
+ const inactive = usedProviders.has(provider);
204
+ usedProviders.add(provider);
194
205
  const previous = storedChain.find(
195
206
  (candidate) => candidate.provider === entry.provider && candidate.model === entry.model
196
207
  );
197
- resolved.push(await resolveChainEntry(deps, entry, previous || {}));
208
+ resolved.push(inactive ? entry : await resolveChainEntry(deps, entry, previous || {}));
198
209
  }
199
210
  const [primary, ...fallbacks] = resolved;
200
211
  const routeToSave = normalizeWorkflowRoute({ ...primary, fallbacks });
@@ -20,7 +20,7 @@ import { resolveDataDir } from './shared.mjs';
20
20
  function applyWorkflowSlots(workflowInput, draft) {
21
21
  for (const slot of WORKFLOW_ROUTE_SLOTS) {
22
22
  if (!hasOwn(workflowInput, slot)) continue;
23
- const normalized = normalizeWorkflowRoute(workflowInput[slot]);
23
+ const normalized = normalizeWorkflowRoute(workflowInput[slot], {}, { strict: true });
24
24
  if (slot === 'lead') {
25
25
  if (!normalized) continue;
26
26
  draft.presets = upsertWorkflowPreset(draft.presets, 'lead', normalized);
@@ -40,7 +40,7 @@ function applyAgentRouteInput(agentInput, agentRoutes, configurableIds) {
40
40
  const nextAgents = { ...agentRoutes };
41
41
  for (const id of configurableIds) {
42
42
  if (!hasOwn(agentInput, id)) continue;
43
- const routeToSave = normalizeWorkflowRoute(agentInput[id]);
43
+ const routeToSave = normalizeWorkflowRoute(agentInput[id], {}, { strict: true });
44
44
  if (routeToSave) nextAgents[id] = routeToSave;
45
45
  else delete nextAgents[id];
46
46
  }
@@ -67,7 +67,7 @@ export function createOnboardingApi(deps) {
67
67
  const config = getConfig();
68
68
  const nextConfig = { ...config };
69
69
  const defaultRoute = hasOwn(payload, 'defaultRoute')
70
- ? normalizeWorkflowRoute(payload.defaultRoute, getRoute())
70
+ ? normalizeWorkflowRoute(payload.defaultRoute, getRoute(), { strict: true })
71
71
  : null;
72
72
  const draft = {
73
73
  presets: Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [],
@@ -752,6 +752,30 @@ export function createWorkspaceTransitions({
752
752
  return maintenance.pruneWorktrees({ ...(await maintenanceScope()), repoCommonDir });
753
753
  }
754
754
 
755
+ /**
756
+ * Remove managed worktree `entryId` whose owner is gone (listed `ownerLive:
757
+ * false, removable: true`), on an explicit request: no owner lane exists to
758
+ * do it. No stored or live session may still use the folder; the manager
759
+ * reads the retention again under the repository lock (an owner that came
760
+ * back refuses) and keeps its guards: dirty or unmerged work needs `force`,
761
+ * other sessions' references and retained recovery data refuse, and only
762
+ * the managed checkout is deleted.
763
+ */
764
+ async function removeOrphanWorktree({ entryId, force = false } = {}) {
765
+ const row = (await listWorktrees()).find((item) => item.id === entryId);
766
+ if (!row) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
767
+ const removed = await withManagementFolders([row.path], true, async () => {
768
+ const { liveSessionIds, listSessionIds, manager: backendApi } = await maintenanceScope();
769
+ return backendApi.removeOrphanedWorktree({
770
+ entryId,
771
+ force: force === true,
772
+ liveSessionIds,
773
+ retainedSessionIds: await listSessionIds(),
774
+ });
775
+ });
776
+ return { removed, entryId, path: row.path };
777
+ }
778
+
755
779
  /** After `sessionId` was deleted: autoCleanup removes its clean, merged worktree. */
756
780
  async function cleanupDeletedSession(sessionId) {
757
781
  // Its snapshots go with it below; no turn cleanup keeps retrying them.
@@ -1219,6 +1243,7 @@ export function createWorkspaceTransitions({
1219
1243
  switchProject,
1220
1244
  listWorktrees,
1221
1245
  pruneWorktrees,
1246
+ removeOrphanWorktree,
1222
1247
  cleanupDeletedSession,
1223
1248
  onWorktreeSetup,
1224
1249
  dispose,
@@ -27,12 +27,7 @@ const loadManager = () => import('../../runtime/shared/worktrees/manager.mjs');
27
27
  * @param {((args: object, context: object) => Promise<any>)|null} [deps.executeSessionControl] Cross-session lane.
28
28
  * @param {object|null} [deps.manager] Managed-worktree backend override (tests).
29
29
  */
30
- export function createWorktreeToolHost({
31
- getSession,
32
- getWorkspaceApi,
33
- executeSessionControl = null,
34
- manager = null,
35
- }) {
30
+ export function createWorktreeToolHost({ getSession, getWorkspaceApi, executeSessionControl = null, manager = null }) {
36
31
  const backend = () => (manager ? Promise.resolve(manager) : loadManager());
37
32
 
38
33
  function workspaceApi() {
@@ -102,7 +97,11 @@ export function createWorktreeToolHost({
102
97
  'The project folder has no checked-out branch to return to (detached HEAD).'
103
98
  );
104
99
  }
105
- return lane.call('selectBranch', { branch: projectBranch, expectedRevision: workspace.revision, txId: request.txId });
100
+ return lane.call('selectBranch', {
101
+ branch: projectBranch,
102
+ expectedRevision: workspace.revision,
103
+ txId: request.txId,
104
+ });
106
105
  }
107
106
 
108
107
  /** The desktop's "merge": into the confirmed merge target, then the cleanup autoCleanup allows. */
@@ -116,6 +115,15 @@ export function createWorktreeToolHost({
116
115
  });
117
116
  }
118
117
 
118
+ /**
119
+ * The listed worktree matching `predicate` whose owner is gone and that the
120
+ * list reports removable, or null: no owner lane can remove it.
121
+ */
122
+ async function orphan(predicate) {
123
+ const rows = await workspaceApi().listWorktrees();
124
+ return rows.find((row) => predicate(row) && row.ownerLive === false && row.removable === true) ?? null;
125
+ }
126
+
119
127
  /**
120
128
  * `git worktree remove <path>`: the managed removal of whichever owner
121
129
  * holds it — this session's snapshot, or the worktree a session retains
@@ -129,6 +137,9 @@ export function createWorktreeToolHost({
129
137
  );
130
138
  }
131
139
  if (entry.kind === 'snapshot') return workspaceApi().removeSnapshot({ path: entry.path, force: request.force });
140
+ if (entry.ownerSessionId !== callerSessionId && (await orphan((row) => row.id === entry.id))) {
141
+ return workspaceApi().removeOrphanWorktree({ entryId: entry.id, force: request.force === true });
142
+ }
132
143
  if (!entry.ownerSessionId) {
133
144
  throw new Error(
134
145
  `git worktree remove: no session retains ${entry.path}; git worktree prune removes it once it is clean and merged`
@@ -161,8 +172,14 @@ export function createWorktreeToolHost({
161
172
  return back(lane(), request);
162
173
  case 'merge':
163
174
  return merge(lane());
164
- case 'remove':
165
- return lane().call('removeWorktree', { force: request.force === true });
175
+ case 'remove': {
176
+ const force = request.force === true;
177
+ if (request.sessionId && request.sessionId !== callerSessionId) {
178
+ const gone = await orphan((row) => row.ownerSessionId === request.sessionId);
179
+ if (gone) return workspaceApi().removeOrphanWorktree({ entryId: gone.id, force });
180
+ }
181
+ return lane().call('removeWorktree', { force });
182
+ }
166
183
  case 'list':
167
184
  return workspaceApi().listWorktrees({ includeSnapshots: true });
168
185
  case 'prune':
@@ -7,6 +7,7 @@ import {
7
7
  import { createAgentTaskSessions, taskSessionPendingWork } from '../session-runtime/agent-task-sessions.mjs';
8
8
  import { createBotStatus, scanCatalog, scopedBot } from '../session-runtime/bot-status.mjs';
9
9
  import { handoffContent } from '../session-runtime/handoff-attachments.mjs';
10
+ import { BOT_TARGET_ACTIONS, isBotShaped, unknownBotError } from '../session-runtime/bot-target.mjs';
10
11
  import { checkEffort, readModelCatalog } from '../session-runtime/task-session-route.mjs';
11
12
  import { assertBotChainTarget, botChainOf } from '../runtime/shared/bot-hops.mjs';
12
13
  import { agentIdOfHomeSession } from '../runtime/agent/orchestrator/session/agent-home.mjs';
@@ -82,7 +83,8 @@ async function storedTitle(sessionId) {
82
83
  export function createSessionControl({
83
84
  getSessionService,
84
85
  agentTasks = createAgentTaskSessions({ getSessionService }),
85
- botStatus = createBotStatus({ getSessionService }),
86
+ listBots,
87
+ botStatus = createBotStatus({ getSessionService, listBots }),
86
88
  // Agent work a session handed off and still waits on (agent-task-sessions.mjs).
87
89
  pendingWork = taskSessionPendingWork,
88
90
  liveWork = storedLiveWork,
@@ -92,7 +94,7 @@ export function createSessionControl({
92
94
  const worktreeControl = createWorktreeControl(getSessionService);
93
95
  const routeControl = createRouteControl(getSessionService);
94
96
 
95
- return async function executeSessionControl(args = {}, context = {}) {
97
+ async function control(args = {}, context = {}) {
96
98
  const action = args.action;
97
99
  const caller = requestCaller(action, context);
98
100
  // `callerAgentId`: set only for an attended agent conversation (workspace-tools.mjs).
@@ -263,5 +265,19 @@ export function createSessionControl({
263
265
  agentsWorking: (await pendingWork(sessionId)) === true,
264
266
  quotaResume: snapshot.quotaResume ?? null,
265
267
  };
268
+ }
269
+
270
+ return async function executeSessionControl(args = {}, context = {}) {
271
+ try {
272
+ return await control(args, context);
273
+ } catch (error) {
274
+ // A bot id is resolved to its home chat before this (workspace-tools.mjs),
275
+ // so a bot-shaped id the session service found no session for is no bot
276
+ // either: say which ids are.
277
+ const id = typeof args.session_id === 'string' ? args.session_id.trim() : '';
278
+ const missing = [`${id} is not available`, `${id} is unavailable`].some((text) => error?.message?.includes(text));
279
+ if (!BOT_TARGET_ACTIONS.has(args.action) || !isBotShaped(id) || !missing) throw error;
280
+ throw await unknownBotError(id, listBots, error);
281
+ }
266
282
  };
267
283
  }