mixdog 1.3.3 → 1.3.5

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 (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -33,10 +33,12 @@ import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.m
33
33
  import { collectTurnArtifacts, handoffContent, sessionRoots, turnSlice } from './handoff-attachments.mjs';
34
34
  import { roomMemberOfSession } from '../runtime/agent/orchestrator/session/agent-room.mjs';
35
35
  import { wrapAgentMessage } from '../runtime/shared/agent-message-envelope.mjs';
36
- import { sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
36
+ import { hasPendingAgentWork, sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
37
37
  import { agentTaskIdOfExecution } from './services/agent-tool/restart-notify.mjs';
38
+ import { botChainOf } from '../runtime/shared/bot-hops.mjs';
38
39
  import { worktreeError, worktreeNameError } from '../runtime/shared/worktrees/contract.mjs';
39
40
  import { samePath } from '../runtime/shared/worktrees/paths.mjs';
41
+ import { readModelCatalog, routeByDisplayName, withEffort } from './task-session-route.mjs';
40
42
  import { assertWorktreeMutationAllowed } from './services/worktree-access.mjs';
41
43
 
42
44
  const TITLE_LIMIT = 60;
@@ -158,10 +160,25 @@ function consumedTaskIds(message) {
158
160
  * submissionOutcome and when its end was recorded (0 when unknown); null
159
161
  * when no turn took runId. */
160
162
  export function latestTurnOutcome(messages, runId) {
163
+ const chain = turnChain(messages, runId);
164
+ return chain && { id: chain.id, outcome: chain.outcome, at: chain.at, ambiguous: chain.ambiguous };
165
+ }
166
+
167
+ /** latestTurnOutcome with what a report's wait needs besides: `taskIds`, the
168
+ * agent tasks the chain's turns started — the only tasks proven the chain's
169
+ * — and `busyIsOthers`: the session's last stored turn is one outside the
170
+ * chain that recorded no end yet, and a stored turn took the result of every
171
+ * one of `taskIds` (only those make a turn the chain's) — a busy session
172
+ * then runs that other turn, and no result of the chain can be on its way
173
+ * into a turn (woken by it, or steered into the running one) whose prompt or
174
+ * steer is not stored yet. */
175
+ function turnChain(messages, runId) {
161
176
  const list = Array.isArray(messages) ? messages : [];
162
177
  const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
163
178
  if (start < 0) return null;
164
179
  const started = new Set();
180
+ // The agent tasks whose results a stored turn took (as its prompt or steered in).
181
+ const taken = new Set();
165
182
  const startingCalls = new Set();
166
183
  let at = start;
167
184
  let id = runId;
@@ -186,6 +203,7 @@ export function latestTurnOutcome(messages, runId) {
186
203
  index > start && message?.role === 'user' && message.meta?.source === 'task-notification'
187
204
  ? consumedTaskIds(message)
188
205
  : null;
206
+ for (const taskId of delivered || []) if (taskId) taken.add(taskId);
189
207
  if (delivered && !inChain) {
190
208
  if (delivered.some((taskId) => taskId && started.has(taskId))) {
191
209
  inChain = true;
@@ -211,7 +229,32 @@ export function latestTurnOutcome(messages, runId) {
211
229
  }
212
230
  const turn = list.slice(at);
213
231
  const ended = turn.find((message) => message?.meta?.transcript?.completion);
214
- return { id, outcome: submissionOutcome(turn, id), at: Number(ended?.meta?.transcript?.at) || 0, ambiguous };
232
+ const last = turnId === id ? null : submissionOutcome(list.slice(turnAt), turnId);
233
+ return {
234
+ id,
235
+ outcome: submissionOutcome(turn, id),
236
+ at: Number(ended?.meta?.transcript?.at) || 0,
237
+ ambiguous,
238
+ taskIds: started,
239
+ busyIsOthers: last === 'open' && [...started].every((taskId) => taken.has(taskId)),
240
+ };
241
+ }
242
+
243
+ /** The agent task an owed result reports; null when it names none. */
244
+ function owedTaskId(result) {
245
+ const executionId = result?.meta?.execution_id;
246
+ return executionId ? agentTaskIdOfExecution(executionId) : null;
247
+ }
248
+
249
+ /** The `owed` results that may be the chain's of agent tasks `taskIds`: its
250
+ * tasks' and those naming no task (their provenance is missing); every one
251
+ * for a null `taskIds` (the whole session). */
252
+ function owedOf(owed, taskIds) {
253
+ if (!taskIds) return owed;
254
+ return owed.filter((result) => {
255
+ const taskId = owedTaskId(result);
256
+ return !taskId || taskIds.has(taskId);
257
+ });
215
258
  }
216
259
 
217
260
  /** Whether task session `sessionId` still waits on agent work it handed off,
@@ -221,16 +264,72 @@ export function latestTurnOutcome(messages, runId) {
221
264
  * `agentTaskOwners` (the sessions owning a queued, spawning or running agent
222
265
  * task — one still waiting for admission has no turn in flight, and its pool
223
266
  * row reads `unknown`). An agent task owned by an Agent session under it, at
224
- * any depth, counts too: every descendant's pool row names the Lead as owner. */
267
+ * any depth, counts too: every descendant's pool row names the Lead as owner.
268
+ * `taskIds` narrows it to one chain's agent tasks (chainPendingWork). */
225
269
  export function pendingWorkOf(
226
270
  sessionId,
227
- { agentWorking = new Set(), owed = [], pool = [], agentTaskOwners = new Set() }
271
+ {
272
+ agentWorking = new Set(),
273
+ owed = [],
274
+ pool = [],
275
+ agentTaskOwners = new Set(),
276
+ taskIds = null,
277
+ working = new Set(),
278
+ agentTasks = [],
279
+ }
228
280
  ) {
281
+ if (taskIds) return chainPendingWork(sessionId, taskIds, { owed, pool, agentTaskOwners, working, agentTasks });
229
282
  if (agentWorking.has(sessionId) || owed.length > 0 || agentTaskOwners.has(sessionId)) return true;
230
283
  if (sessionsWithPendingWork(pool).has(sessionId)) return true;
231
284
  return pool.some((row) => row?.ownerSessionId === sessionId && agentTaskOwners.has(String(row.sessionId || '')));
232
285
  }
233
286
 
287
+ /** The agent work of the chain of Lead `sessionId` whose own agent tasks are
288
+ * `taskIds`. `taskIds`: those tasks alone, the only ones proven the chain's
289
+ * (no record says which task started a child agent). `sessions`: the Agent
290
+ * sessions those tasks run in (`agentTasks` `sessionId`, a pool row's
291
+ * `taskId`) and every Agent session under one (pool `parentSessionId`),
292
+ * never the Lead — work there may be the chain's, so it only holds the
293
+ * report (it may be another's: a session the chain's agent ran in can be
294
+ * sent another task, which can spawn children of its own). */
295
+ export function chainClosure(sessionId, taskIds, { pool = [], agentTasks = [] } = {}) {
296
+ const owned = new Set(taskIds);
297
+ const sessions = new Set();
298
+ const join = (id) => {
299
+ if (!id || id === sessionId || sessions.has(id)) return false;
300
+ sessions.add(id);
301
+ return true;
302
+ };
303
+ for (const task of agentTasks) if (owned.has(task.taskId)) join(task.sessionId);
304
+ for (const row of pool) if (owned.has(String(row?.taskId || ''))) join(String(row.sessionId || ''));
305
+ for (let grown = true; grown; ) {
306
+ grown = false;
307
+ for (const row of pool) {
308
+ if (sessions.has(String(row?.parentSessionId || '')) && join(String(row.sessionId || ''))) grown = true;
309
+ }
310
+ }
311
+ return { taskIds: owned, sessions };
312
+ }
313
+
314
+ /** pendingWorkOf of the chain of agent tasks `taskIds` alone, with all its
315
+ * work (chainClosure): a task it owns or one of its Agent sessions runs
316
+ * (`agentTasks`: `{ taskId, sessionId, running }` of this process's agent
317
+ * tasks; a pool row's `taskId`) still queued or running, or its result owed
318
+ * (or an owed result naming no task), or one of those sessions with a turn
319
+ * in flight (`working`), owning a queued or running agent task, or pending
320
+ * on its pool row. Other work of the session never counts. */
321
+ function chainPendingWork(sessionId, taskIds, { owed, pool, agentTaskOwners, working, agentTasks }) {
322
+ const chain = chainClosure(sessionId, taskIds, { pool, agentTasks });
323
+ const sessionOf = new Map();
324
+ for (const row of pool) if (row?.taskId) sessionOf.set(String(row.taskId), String(row.sessionId || ''));
325
+ for (const task of agentTasks) if (task.sessionId) sessionOf.set(task.taskId, task.sessionId);
326
+ const ofChain = (taskId) => chain.taskIds.has(taskId) || chain.sessions.has(sessionOf.get(taskId));
327
+ if (owed.some((result) => !owedTaskId(result) || ofChain(owedTaskId(result)))) return true;
328
+ if (agentTasks.some((task) => task.running && ofChain(task.taskId))) return true;
329
+ for (const id of chain.sessions) if (working.has(id) || agentTaskOwners.has(id)) return true;
330
+ return pool.some((row) => chain.sessions.has(String(row?.sessionId || '')) && hasPendingAgentWork(row));
331
+ }
332
+
234
333
  /** The agent results queued for `sessionId` that no turn has taken yet (its
235
334
  * durable pending queue: an agent's completion, a restart failure): `{ text,
236
335
  * meta }` as the notification bus delivers them. */
@@ -255,22 +354,37 @@ export async function owedAgentResults(sessionId) {
255
354
  }
256
355
 
257
356
  /** pendingWorkOf over this process's state: the in-flight runtimes, the
258
- * durable pending queue, the agent pool and the background agent tasks. */
259
- export async function taskSessionPendingWork(sessionId, { owedResults = owedAgentResults } = {}) {
260
- const [{ inFlightSessionActivity }, { listStoredAgentWorkersAsync }, { activeBackgroundTaskSessionIds }] =
261
- await Promise.all([
262
- import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
263
- import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
264
- import('../runtime/shared/background-tasks.mjs'),
265
- ]);
357
+ * durable pending queue, the agent pool and the background agent tasks;
358
+ * `taskIds` narrows it to one chain's agent tasks. */
359
+ export async function taskSessionPendingWork(sessionId, { owedResults = owedAgentResults, taskIds = null } = {}) {
360
+ const [
361
+ { inFlightSessionActivity },
362
+ { listStoredAgentWorkersAsync },
363
+ { activeBackgroundTaskSessionIds, listBackgroundTasks },
364
+ ] = await Promise.all([
365
+ import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
366
+ import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
367
+ import('../runtime/shared/background-tasks.mjs'),
368
+ ]);
266
369
  const { working, agentWorking } = inFlightSessionActivity();
267
370
  const agentTaskOwners = activeBackgroundTaskSessionIds({ surface: 'agent' });
268
- if (pendingWorkOf(sessionId, { agentWorking, agentTaskOwners })) return true;
269
- if ((await owedResults(sessionId)).length) return true;
371
+ const agentTasks = taskIds ? agentTaskRows(listBackgroundTasks) : [];
372
+ const live = { agentWorking, agentTaskOwners, taskIds, working, agentTasks };
373
+ if (pendingWorkOf(sessionId, live)) return true;
374
+ const owed = await owedResults(sessionId);
375
+ if (pendingWorkOf(sessionId, { ...live, owed })) return true;
270
376
  const pool = await listStoredAgentWorkersAsync({ runningSessionIds: working });
271
- return pendingWorkOf(sessionId, { pool, agentTaskOwners });
377
+ return pendingWorkOf(sessionId, { ...live, owed, pool });
272
378
  }
273
379
 
380
+ /** This process's agent tasks as pendingWorkOf reads them. */
381
+ const agentTaskRows = (listBackgroundTasks) =>
382
+ listBackgroundTasks({ surface: 'agent' }).map((task) => ({
383
+ taskId: task.task_id,
384
+ sessionId: task.sessionId || null,
385
+ running: task.status === 'running',
386
+ }));
387
+
274
388
  // A task session idle with nothing pending must stay so this long before its
275
389
  // latest answer is final: a finished agent's result can start the Lead's next
276
390
  // turn right after (the desktop notifier's quiet period).
@@ -305,8 +419,9 @@ const LAST_USED_ROUTE_MAX_PAGES = 20;
305
419
  const roomFrom = (originSessionId) => (roomMemberOfSession(originSessionId) ? { from: originSessionId } : {});
306
420
 
307
421
  /** The route of `modelRef`: a provider/model[@effort…] ref (a superseded
308
- * model runs on its successor) or a preset name. */
309
- async function configModelRoute(modelRef) {
422
+ * model runs on its successor), a preset name, else whatever `byName` makes
423
+ * of it (a catalog display name; it throws when it names none). */
424
+ async function configModelRoute(modelRef, byName) {
310
425
  const [{ modelRouteFields, parseScheduleModelRef }, { migrateCodexRoute }, { loadConfig }] = await Promise.all([
311
426
  import('../runtime/shared/schedule-model-ref.mjs'),
312
427
  import('../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs'),
@@ -315,13 +430,12 @@ async function configModelRoute(modelRef) {
315
430
  const ref = parseScheduleModelRef(modelRef);
316
431
  if (ref && typeof ref === 'object') return modelRouteFields(migrateCodexRoute(ref));
317
432
  const preset = loadConfig({ secrets: false }).presets?.find((p) => p.id === ref || p.name === ref);
318
- if (!preset) throw new Error(`model "${ref}" is neither a provider/model route nor a known preset name`);
319
- return modelRouteFields(preset);
433
+ return preset ? modelRouteFields(preset) : byName(ref);
320
434
  }
321
435
 
322
436
  /** The Lead (Main) route of the config — what a desktop New task starts on.
323
437
  * Null while none is chosen. */
324
- async function configLeadRoute() {
438
+ export async function configLeadRoute() {
325
439
  const [{ modelRouteFields }, { resolveRoute }, { loadConfig }] = await Promise.all([
326
440
  import('../runtime/shared/schedule-model-ref.mjs'),
327
441
  import('./boot/shared.mjs'),
@@ -352,8 +466,12 @@ async function lastUsedSessionRoute(sessionService) {
352
466
  /** Wait for the final answer of the task turn `runId` of `sessionId` ended
353
467
  * completed: the end of the latest turn since it (a Lead that handed work
354
468
  * to agents ends its turn and answers in the turn their results wake),
355
- * once the session is idle with no pending agent work (pendingWork) for
356
- * `settleMs`. `immediate` is runId's own end, when this process saw it.
469
+ * once that chain has no turn running and no pending agent work
470
+ * (pendingWork of its agent tasks) for `settleMs`. Work of the session
471
+ * outside the chain (a turn the user typed, its agents) never holds it;
472
+ * without the provenance to tell (no stored turn, `ambiguous`) the whole
473
+ * session must be idle with none pending. `immediate` is runId's own end,
474
+ * when this process saw it.
357
475
  * `finish(status, text)` runs once; `check` re-evaluates (call it on every
358
476
  * state change of `runtime`), `stop` ends the wait. */
359
477
  function followTaskToFinal(
@@ -364,10 +482,10 @@ function followTaskToFinal(
364
482
  let pendingSince = 0;
365
483
  // Agent work still pending `pendingCapMs` after the chain's latest turn
366
484
  // ended: `progress(text)` reports that answer once, marked still in
367
- // progress, and the wait for the final one goes on.
368
- async function reportProgress() {
485
+ // progress, and the wait for the final one goes on. `latest`: the chain's
486
+ // latest turn (turnChain).
487
+ async function reportProgress(latest) {
369
488
  if (progressed || typeof progress !== 'function') return;
370
- const latest = latestTurnOutcome((await loadSession(sessionId))?.messages, runId);
371
489
  let end = latest?.outcome && latest.outcome !== 'open' ? latest.outcome : null;
372
490
  if (immediate && (!latest || latest.id === runId)) end = immediate;
373
491
  if (!end) return;
@@ -383,6 +501,7 @@ function followTaskToFinal(
383
501
  let candidateAt = 0;
384
502
  let timer = null;
385
503
  let wakes = 0;
504
+ let otherWakes = 0;
386
505
  let wokeAt = 0;
387
506
  const unwoken = (why) => {
388
507
  log(`${sessionId} could not take its agents' results after the restart: ${why}`);
@@ -391,23 +510,32 @@ function followTaskToFinal(
391
510
  // `wakeOwed` (a restarted watch): deliver the agent results queued for
392
511
  // the idle session through the notification bus so a turn takes them, at
393
512
  // most OWED_WAKE_TRIES times, a poll apart; null while that turn is owed.
394
- async function wakeForOwed() {
513
+ // Every owed result is delivered, but only those of the chain's agent tasks
514
+ // (owedOf `taskIds`) hold the report or fail it; deliveries of the others
515
+ // alone have a bound of their own (`otherWakes`).
516
+ async function wakeForOwed(taskIds) {
395
517
  const owed = await owedResults(sessionId);
396
518
  if (!owed.length) return undefined;
397
- if (Date.now() - wokeAt < pollMs) return null;
398
- if (wakes >= OWED_WAKE_TRIES) return unwoken(`still not taken after ${wakes} deliveries`);
399
- wakes += 1;
400
- wokeAt = Date.now();
401
- let delivered = false;
402
- try {
403
- if (typeof deliverCompletion === 'function') {
404
- const results = await Promise.all(owed.map(({ text, meta }) => deliverCompletion(sessionId, text, meta)));
405
- delivered = results.some((result) => result === true);
406
- }
407
- } catch (error) {
408
- return unwoken(error?.message || error);
519
+ const chain = new Set(owedOf(owed, taskIds));
520
+ const hold = chain.size ? null : undefined;
521
+ if (Date.now() - wokeAt < pollMs) return hold;
522
+ if (chain.size ? wakes >= OWED_WAKE_TRIES : otherWakes >= OWED_WAKE_TRIES) {
523
+ return chain.size ? unwoken(`still not taken after ${wakes} deliveries`) : undefined;
409
524
  }
410
- return delivered ? null : unwoken('no session runtime took the delivery');
525
+ if (chain.size) wakes += 1;
526
+ else otherWakes += 1;
527
+ wokeAt = Date.now();
528
+ const outcomes = await Promise.allSettled(
529
+ owed.map(
530
+ async ({ text, meta }) =>
531
+ typeof deliverCompletion === 'function' && (await deliverCompletion(sessionId, text, meta)) === true
532
+ )
533
+ );
534
+ if (!chain.size) return undefined;
535
+ const ofChain = outcomes.filter((_, index) => chain.has(owed[index]));
536
+ const failure = ofChain.find((outcome) => outcome.status === 'rejected');
537
+ if (failure) return unwoken(failure.reason?.message || failure.reason);
538
+ return ofChain.some((outcome) => outcome.value === true) ? null : unwoken('no session runtime took the delivery');
411
539
  }
412
540
  const arm = (ms) => {
413
541
  clearTimeout(timer);
@@ -421,21 +549,25 @@ function followTaskToFinal(
421
549
  const failed = (error) => log(`following ${sessionId} to its final answer failed: ${error?.message || error}`);
422
550
  // The end to report now, or null while the task still works.
423
551
  async function ready() {
424
- if (runtime?.getState?.()?.busy) return null;
425
- if (wakeOwed) {
426
- const woke = await wakeForOwed();
552
+ const stored = await loadSession(sessionId);
553
+ const latest = turnChain(stored?.messages, runId);
554
+ // The chain's agent tasks; null (the whole session) without its provenance.
555
+ const taskIds = latest && !latest.ambiguous ? latest.taskIds : null;
556
+ // A busy session may run the chain's turn unless that turn is provably another's.
557
+ const busy = Boolean(runtime?.getState?.()?.busy);
558
+ if (busy && !(taskIds && latest.busyIsOthers)) return null;
559
+ if (wakeOwed && !busy) {
560
+ const woke = await wakeForOwed(taskIds);
427
561
  if (woke !== undefined) return woke;
428
562
  }
429
- if (await pendingWork(sessionId)) {
430
- await reportProgress();
563
+ if (await pendingWork(sessionId, { taskIds })) {
564
+ await reportProgress(latest);
431
565
  return null;
432
566
  }
433
- const stored = await loadSession(sessionId);
434
567
  if (!stored && !immediate) return { key: 'gone', status: 'cancelled', text: GONE_TEXT };
435
- const latest = latestTurnOutcome(stored?.messages, runId);
436
568
  // Provenance the chain needs is missing: its answer may not be final.
437
569
  if (latest?.ambiguous) {
438
- await reportProgress();
570
+ await reportProgress(latest);
439
571
  return null;
440
572
  }
441
573
  if (immediate && (!latest || latest.id === runId)) return { key: runId, ...immediate };
@@ -635,9 +767,7 @@ async function rearmTaskWatch(
635
767
  ...(watch.hop > 0 ? { hop: watch.hop } : {}),
636
768
  ...roomFrom(watch.homeSessionId),
637
769
  watchId: watch.runId,
638
- ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT
639
- ? {}
640
- : await artifactFields(sessionId, turnId)),
770
+ ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT ? {} : await artifactFields(sessionId, turnId)),
641
771
  });
642
772
  if (!(await loadSession(sessionId))) return finish('cancelled', GONE_TEXT);
643
773
  let runtime;
@@ -737,6 +867,8 @@ export function createAgentTaskSessions({
737
867
  newTaskDefault = async () =>
738
868
  (await import('../runtime/agent/orchestrator/config.mjs')).loadConfig({ secrets: false }).worktree?.newTaskDefault,
739
869
  modelRoute = configModelRoute,
870
+ // The model menu's catalog rows, read through the calling session's runtime.
871
+ modelCatalog = (sessionId) => readModelCatalog(service(), sessionId),
740
872
  // The Lead (Main) route of the config — what a desktop New task starts on.
741
873
  leadRoute = configLeadRoute,
742
874
  // The model the user last used in a coding session.
@@ -796,18 +928,62 @@ export function createAgentTaskSessions({
796
928
  follow
797
929
  );
798
930
 
931
+ // Sends one turn answers settle together with the same `detail` object: a
932
+ // send steered into the turn while it ran, or queued and taken as one batch.
933
+ // That turn reports once, closing every one of them.
934
+ const settledTurns = new WeakMap();
935
+ let sendCount = 0;
936
+
937
+ /** Add send `member` (`{ seq, id, unwatch, settle }`) to the settlement
938
+ * `detail` of its turn, per reporting conversation. The sends of one turn
939
+ * settle in the same tick; after it the earliest (the turn's own, whose
940
+ * record holds the whole turn) settles with all their ids and the others
941
+ * stand down. */
942
+ function joinSettlement(detail, origin, member) {
943
+ const key = `${origin.agentId}\0${origin.sessionId}`;
944
+ let groups = settledTurns.get(detail);
945
+ if (!groups) {
946
+ groups = new Map();
947
+ settledTurns.set(detail, groups);
948
+ }
949
+ let members = groups.get(key);
950
+ if (!members) {
951
+ members = [];
952
+ groups.set(key, members);
953
+ const settling = members;
954
+ void Promise.resolve()
955
+ .then(() => {
956
+ const [first, ...others] = settling.sort((a, b) => a.seq - b.seq);
957
+ for (const other of others) other.unwatch();
958
+ first.settle(settling.map((entry) => entry.id));
959
+ })
960
+ .catch((error) => log(`settling the sends of one turn failed: ${error?.message || error}`));
961
+ }
962
+ members.push(member);
963
+ }
964
+
799
965
  /** Submit `prompt` as the origin agent's turn; its end (and a wait for the
800
966
  * user in it) reports to the agent's home. Resolves on intake. `submit`
801
967
  * takes the turn in another way than a prompt to `sessionId` (a message
802
968
  * into a group room, whose member answers in `sessionId`): given `{ id,
803
969
  * onSettled }`, it resolves `{ accepted }`. */
804
- async function agentTurn(sessionId, prompt, origin, id = `atask_${randomUUID()}`, beforeReport = null, submit = null) {
970
+ async function agentTurn(
971
+ sessionId,
972
+ prompt,
973
+ origin,
974
+ id = `atask_${randomUUID()}`,
975
+ beforeReport = null,
976
+ submit = null
977
+ ) {
805
978
  const sessions = service();
806
979
  const target = origin.target === 'session' ? { target: 'session' } : {};
807
980
  const { notify, noReport } = await watchTurn({ watches, owner: processToken, log }, sessionId, origin, id);
808
981
  const dropWatch = () =>
809
982
  watches.remove(id).catch((error) => log(`dropping the watch of ${sessionId} failed: ${error?.message || error}`));
810
983
  let runtime = null;
984
+ // The sends this turn's end closes: this one, plus any that its turn
985
+ // answered too (joinSettlement).
986
+ let sendIds = [id];
811
987
  // A report that cannot be queued is retried in this process; when every try
812
988
  // fails the durable watch stays and restart recovery delivers it (the queue
813
989
  // dedupes by runId, so a retry never posts twice).
@@ -828,7 +1004,11 @@ export function createAgentTaskSessions({
828
1004
  // may receive them (reportOrigin `textOnly`).
829
1005
  ...(status === 'blocked' || status === 'working'
830
1006
  ? {}
831
- : { watchId: id, ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)) }),
1007
+ : {
1008
+ watchId: id,
1009
+ ...(sendIds.length > 1 ? { watchIds: sendIds } : {}),
1010
+ ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)),
1011
+ }),
832
1012
  })
833
1013
  )
834
1014
  .catch((error) => log(`report of ${sessionId} failed: ${error?.message || error}`));
@@ -836,6 +1016,31 @@ export function createAgentTaskSessions({
836
1016
  // Set once the turn ended completed: the report waits for the final answer.
837
1017
  let follow = null;
838
1018
  let result;
1019
+ /** This send ends the turn `detail` for the sends `answered` (joinSettlement). */
1020
+ const settleTurn = (detail, answered) => {
1021
+ sendIds = answered;
1022
+ const status = turnStatus(detail);
1023
+ const text = detail?.result?.content || detail?.error || '';
1024
+ // A failed or cancelled turn reports at once; a completed one
1025
+ // reports the task's final answer (followToFinal).
1026
+ if (status !== 'completed') {
1027
+ unwatch();
1028
+ void report(status, text);
1029
+ return;
1030
+ }
1031
+ follow = followToFinal({
1032
+ sessionId,
1033
+ runId: id,
1034
+ runtime,
1035
+ immediate: { status, text },
1036
+ finish: async (endStatus, endText) => {
1037
+ unwatch();
1038
+ await report(endStatus, endText);
1039
+ },
1040
+ progress: (progressText) => report('working', progressText),
1041
+ });
1042
+ follow.check();
1043
+ };
839
1044
  try {
840
1045
  if (notify) {
841
1046
  runtime = await sessions.materializeSession(sessionId);
@@ -854,6 +1059,9 @@ export function createAgentTaskSessions({
854
1059
  ? wrapAgentMessage(prompt, { name: origin.envelopeName, agentId: origin.agentId })
855
1060
  : prompt;
856
1061
  const take = submit ?? ((options) => sessions.submitSession({ sessionId, prompt: sent, options }));
1062
+ // Submission order: the earliest of the sends one turn answers reports for them.
1063
+ sendCount += 1;
1064
+ const seq = sendCount;
857
1065
  result = await take({
858
1066
  id,
859
1067
  mode: 'prompt',
@@ -864,6 +1072,8 @@ export function createAgentTaskSessions({
864
1072
  sender: 'lead',
865
1073
  ...(origin.envelopeName ? { fromAgent: origin.agentId } : {}),
866
1074
  ...(origin.hop > 0 ? { hop: origin.hop } : {}),
1075
+ // The bots that handed this work on, the sender last (bot-hops.mjs).
1076
+ ...(origin.botChain?.length ? { botChain: origin.botChain } : {}),
867
1077
  },
868
1078
  ...(notify
869
1079
  ? {
@@ -871,29 +1081,13 @@ export function createAgentTaskSessions({
871
1081
  // id (one turn for several sends, or a fresh turn for one cut
872
1082
  // short): the watch follows that turn, recorded before it runs.
873
1083
  follow: (submissionId) => watches.follow?.(id, submissionId),
874
- onSettled: (detail) => {
875
- const status = turnStatus(detail);
876
- const text = detail?.result?.content || detail?.error || '';
877
- // A failed or cancelled turn reports at once; a completed one
878
- // reports the task's final answer (followToFinal).
879
- if (status !== 'completed') {
880
- unwatch();
881
- void report(status, text);
882
- return;
883
- }
884
- follow = followToFinal({
885
- sessionId,
886
- runId: id,
887
- runtime,
888
- immediate: { status, text },
889
- finish: async (endStatus, endText) => {
890
- unwatch();
891
- await report(endStatus, endText);
892
- },
893
- progress: (progressText) => report('working', progressText),
894
- });
895
- follow.check();
896
- },
1084
+ onSettled: (detail) =>
1085
+ joinSettlement(detail, origin, {
1086
+ seq,
1087
+ id,
1088
+ unwatch: () => unwatch(),
1089
+ settle: (answered) => settleTurn(detail, answered),
1090
+ }),
897
1091
  }
898
1092
  : {}),
899
1093
  });
@@ -909,6 +1103,8 @@ export function createAgentTaskSessions({
909
1103
  }
910
1104
  return {
911
1105
  accepted,
1106
+ // Names this send; its terminal report carries it back (routine.sendId).
1107
+ ...(accepted && notify ? { sendId: id } : {}),
912
1108
  reportsBack: accepted && notify,
913
1109
  ...(accepted && !notify ? { reportsBackReason: noReport } : {}),
914
1110
  };
@@ -954,7 +1150,15 @@ export function createAgentTaskSessions({
954
1150
  * caller is refused and the New task default keeps it in the Project folder. */
955
1151
  async function create(
956
1152
  args = {},
957
- { callerSessionId, callerAgentId, callerCwd, callerHop = 0, callerScope, attachmentParts = [] } = {}
1153
+ {
1154
+ callerSessionId,
1155
+ callerAgentId,
1156
+ callerCwd,
1157
+ callerHop = 0,
1158
+ callerBotChain = [],
1159
+ callerScope,
1160
+ attachmentParts = [],
1161
+ } = {}
958
1162
  ) {
959
1163
  if (!callerAgentId) throw new Error('session: create is only for an agent conversation');
960
1164
  const brief = typeof args.brief === 'string' ? args.brief.trim() : '';
@@ -984,10 +1188,18 @@ export function createAgentTaskSessions({
984
1188
  // A task session runs in coding mode under Lead: an explicit model wins,
985
1189
  // else the Lead route, else the last-used model. Never a model-less
986
1190
  // session — the composer would show no model and its first turn would fail.
987
- const route = clean(args.model)
988
- ? await modelRoute(clean(args.model))
1191
+ // The catalog is read only when a display name or an effort needs it.
1192
+ let catalog = null;
1193
+ const catalogRows = async () => (catalog ??= await modelCatalog(callerSessionId));
1194
+ let route = clean(args.model)
1195
+ ? await modelRoute(clean(args.model), async (text) =>
1196
+ routeByDisplayName(text, await catalogRows(), args.effort !== undefined)
1197
+ )
989
1198
  : (await leadRoute()) || (await lastUsedRoute(service()));
990
1199
  if (!route) throw new Error('session: no model to start the task session on. Choose a Lead model first.');
1200
+ // An explicit effort is the setting the menu's effort pick makes (session.effort,
1201
+ // Auto effort off); it wins over one a model ref or name carried.
1202
+ if (args.effort !== undefined) route = withEffort(route, args.effort, await catalogRows());
991
1203
  const sessions = service();
992
1204
  // Where it starts: the `worktree` option, else the user's New task default
993
1205
  // for a caller that may provision one. Decided before the session exists,
@@ -1052,10 +1264,11 @@ export function createAgentTaskSessions({
1052
1264
  });
1053
1265
  // The brief carries the files the agent forwarded (workspace-tools.mjs).
1054
1266
  const prompt = handoffContent(brief, attachmentParts);
1267
+ const botChain = botChainOf(callerBotChain);
1055
1268
  const { accepted, reportsBack, reportsBackReason } = await agentTurn(
1056
1269
  sessionId,
1057
1270
  prompt,
1058
- callerHop > 0 ? { ...taskOrigin, hop: callerHop } : taskOrigin,
1271
+ { ...taskOrigin, ...(callerHop > 0 ? { hop: callerHop } : {}), ...(botChain.length ? { botChain } : {}) },
1059
1272
  requestId,
1060
1273
  ready
1061
1274
  );
@@ -4,12 +4,57 @@
4
4
  * status for the facade.
5
5
  */
6
6
 
7
- export function createRoutedAgentTool({ rt, agentTool, executeAgentControl = null }) {
7
+ import { clean } from '../runtime/shared/clean.mjs';
8
+
9
+ const lower = (value) => clean(value).toLowerCase();
10
+
11
+ // A pinned model named by its catalog display name ("GPT-6.1-Sol") is not a
12
+ // wire id. Canonicalize it only on an unambiguous EXACT match: the id itself,
13
+ // or a display name / label, case-insensitively, within the requested provider
14
+ // (or across providers when none is given and exactly one row matches).
15
+ // Anything else — no match, several matches, an empty or partial catalog —
16
+ // returns the args unchanged: the catalog never refuses a model and never
17
+ // substitutes a near one.
18
+ export function canonicalizePinnedModel(args, rows) {
19
+ const model = clean(args?.model);
20
+ if (!model || !Array.isArray(rows)) return args;
21
+ const provider = lower(args.provider);
22
+ const pool = rows.filter((row) => row && (!provider || lower(row.provider) === provider));
23
+ const wanted = model.toLowerCase();
24
+ const exactId = pool.filter((row) => row.id === model);
25
+ // A partial catalog may lack the real id a display name collides with, so
26
+ // only an exact id can pass there; aliases wait for a complete catalog.
27
+ const partial = pool.some((row) => row.catalogPartial === true);
28
+ const hits = exactId.length
29
+ ? exactId
30
+ : partial
31
+ ? []
32
+ : pool.filter((row) => lower(row.id) === wanted || lower(row.display) === wanted || lower(row.label) === wanted);
33
+ const distinct = [...new Map(hits.map((row) => [`${row.provider}\n${row.id}`, row])).values()];
34
+ if (distinct.length !== 1) return args;
35
+ const [hit] = distinct;
36
+ if (hit.id === model && hit.provider === args.provider) return args;
37
+ return { ...args, provider: hit.provider, model: hit.id };
38
+ }
39
+
40
+ // `listProviderModels` is this runtime's provider catalog. The agent control
41
+ // behind `executeAgentControl` (the daemon's canonical agent) has none, so a
42
+ // display-name model is canonicalized here before it is forwarded. Only the
43
+ // cached/quick catalog is read, so a spawn never waits on provider listings.
44
+ export function createRoutedAgentTool({ rt, agentTool, executeAgentControl = null, listProviderModels = null }) {
8
45
  const routedAgentTool = {
9
46
  ...agentTool,
10
- execute(args, context = {}) {
11
- if (typeof executeAgentControl === 'function') return executeAgentControl(args, context);
12
- return agentTool.execute(args, context);
47
+ async execute(args, context = {}) {
48
+ if (typeof executeAgentControl !== 'function') return agentTool.execute(args, context);
49
+ let routedArgs = args;
50
+ if (typeof listProviderModels === 'function' && clean(args?.model) && (clean(args.type) || 'spawn') === 'spawn') {
51
+ try {
52
+ routedArgs = canonicalizePinnedModel(args, await listProviderModels({ quick: true }));
53
+ } catch {
54
+ routedArgs = args;
55
+ }
56
+ }
57
+ return executeAgentControl(routedArgs, context);
13
58
  },
14
59
  closeAll(reason, scope = {}) {
15
60
  if (typeof executeAgentControl !== 'function') {