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
@@ -453,8 +453,9 @@ function callArgs(call) {
453
453
  return args && typeof args === 'object' ? args : {};
454
454
  }
455
455
 
456
- /** The paths one tool call wrote or produced. */
457
- function toolCallOutputs(call) {
456
+ /** The paths one tool call wrote or produced; `written`: only those it
457
+ * actually created or modified (opening a file in visualize does neither). */
458
+ function toolCallOutputs(call, written) {
458
459
  const name = String(call?.function?.name || call?.name || '');
459
460
  const args = callArgs(call);
460
461
  const text = (value) => (typeof value === 'string' && value.trim() ? [value.trim()] : []);
@@ -464,7 +465,7 @@ function toolCallOutputs(call) {
464
465
  if (name === 'office') return OFFICE_WRITE_ACTIONS.has(args.action) ? [...text(args.output), ...text(args.path)] : [];
465
466
  if (name === 'visualize') {
466
467
  const input = args.input && typeof args.input === 'object' ? args.input : {};
467
- const targets = args.action === 'open' && Array.isArray(input.targets) ? input.targets : [];
468
+ const targets = !written && args.action === 'open' && Array.isArray(input.targets) ? input.targets : [];
468
469
  return [
469
470
  ...text(input.output),
470
471
  ...targets.filter((target) => typeof target === 'string' && !/^[a-z][a-z0-9+.-]*:\/\//i.test(target)),
@@ -520,8 +521,9 @@ export function turnSlice(messages, { runId = '', since = 0 } = {}) {
520
521
 
521
522
  /** The candidate artifact paths of a turn's messages, most relevant first:
522
523
  * what its final reply names, then media/office/visualize outputs, then the
523
- * files it edited. */
524
- export function turnArtifactCandidates(messages) {
524
+ * files it edited. `written`: only the files it created or modified (no
525
+ * reply paths, no visualize open targets). */
526
+ export function turnArtifactCandidates(messages, { written = false } = {}) {
525
527
  const list = Array.isArray(messages) ? messages : [];
526
528
  const reply = list.findLast((message) => message?.role === 'assistant' && textOf(message.content).trim());
527
529
  const produced = [];
@@ -535,10 +537,10 @@ export function turnArtifactCandidates(messages) {
535
537
  : [];
536
538
  for (const call of calls) {
537
539
  const name = String(call?.function?.name || call?.name || '');
538
- (name === 'edit' || name === 'apply_patch' ? edited : produced).push(...toolCallOutputs(call));
540
+ (name === 'edit' || name === 'apply_patch' ? edited : produced).push(...toolCallOutputs(call, written));
539
541
  }
540
542
  }
541
- return [...(reply ? replyPaths(textOf(reply.content)) : []), ...produced, ...edited];
543
+ return [...(!written && reply ? replyPaths(textOf(reply.content)) : []), ...produced, ...edited];
542
544
  }
543
545
 
544
546
  /**
@@ -549,38 +551,16 @@ export function turnArtifactCandidates(messages) {
549
551
  * the roots are left out silently; at most HANDOFF_LIMITS.count are kept.
550
552
  */
551
553
  export function collectTurnArtifacts(messages, { cwd = '', roots = [] } = {}) {
552
- const seen = new Set();
553
554
  const files = [];
554
555
  let omitted = 0;
555
556
  const budget = createBudget();
556
- for (const candidate of turnArtifactCandidates(messages)) {
557
- // A candidate is a lexical path; its canonical form dedupes after the read.
558
- const lexical = samePlatformPath(
559
- isAbsolute(candidate) ? resolve(candidate) : resolve(cwd || process.cwd(), candidate)
560
- );
561
- if (seen.has(lexical)) continue;
562
- seen.add(lexical);
563
- let file;
564
- try {
565
- file = readContained(candidate, { cwd, roots, surface: 'artifact' });
566
- } catch {
567
- continue;
568
- }
569
- const key = samePlatformPath(file.path);
570
- if (key !== lexical && seen.has(key)) continue;
571
- seen.add(key);
557
+ for (const { file } of readableCandidates(turnArtifactCandidates(messages), { cwd, roots })) {
572
558
  if (files.length >= HANDOFF_LIMITS.count) {
573
559
  omitted += 1;
574
560
  continue;
575
561
  }
576
562
  const { type, bytes } = file;
577
- const entry = {
578
- path: file.path,
579
- name: file.name,
580
- kind: type?.kind || 'file',
581
- mimeType: type?.mimeType || 'application/octet-stream',
582
- size: file.size,
583
- };
563
+ const entry = pathEntry(file);
584
564
  const storable = bytes && (type.kind !== 'image' || SUPPORTED_IMAGE_MIME_TYPES.has(type.mimeType));
585
565
  if (storable && budget.take(type.kind, bytes.length)) {
586
566
  try {
@@ -591,3 +571,74 @@ export function collectTurnArtifacts(messages, { cwd = '', roots = [] } = {}) {
591
571
  }
592
572
  return { files, omitted };
593
573
  }
574
+
575
+ /**
576
+ * The files `messages` (one turn) created or modified, inside `roots`, that
577
+ * `report` (the text handed to the Lead) does not name: `{ files, omitted }`
578
+ * as path-only entries, which the Lead forwards by path. Nothing is stored.
579
+ */
580
+ export function collectUnreportedWrites(messages, { cwd = '', roots = [], report }) {
581
+ const found = [...readableCandidates(turnArtifactCandidates(messages, { written: true }), { cwd, roots })];
582
+ const unnamed = found.filter((item) => !reportNames(report, item, found, cwd));
583
+ return {
584
+ files: unnamed.slice(0, HANDOFF_LIMITS.count).map(({ file }) => pathEntry(file)),
585
+ omitted: Math.max(0, unnamed.length - HANDOFF_LIMITS.count),
586
+ };
587
+ }
588
+
589
+ const lexicalKey = (path, cwd) => samePlatformPath(resolve(cwd || process.cwd(), path));
590
+
591
+ const pathEntry = (file) => ({
592
+ path: file.path,
593
+ name: file.name,
594
+ kind: file.type?.kind || 'file',
595
+ mimeType: file.type?.mimeType || 'application/octet-stream',
596
+ size: file.size,
597
+ });
598
+
599
+ /** The distinct files among `candidates` that can be read inside `roots`, with
600
+ * their lexical and canonical keys. A candidate is a lexical path; its
601
+ * canonical form dedupes after the read. */
602
+ function* readableCandidates(candidates, { cwd, roots }) {
603
+ const seen = new Set();
604
+ for (const candidate of candidates) {
605
+ const lexical = lexicalKey(candidate, cwd);
606
+ if (seen.has(lexical)) continue;
607
+ seen.add(lexical);
608
+ let file;
609
+ try {
610
+ file = readContained(candidate, { cwd, roots, surface: 'artifact' });
611
+ } catch {
612
+ continue;
613
+ }
614
+ const key = samePlatformPath(file.path);
615
+ if (key !== lexical && seen.has(key)) continue;
616
+ seen.add(key);
617
+ yield { file, lexical, key };
618
+ }
619
+ }
620
+
621
+ /** Whether `report` names the produced file `item` among `found`: by its full
622
+ * path or a cwd-relative path resolving to it exactly, or by its bare file name
623
+ * when no other produced file shares that name. */
624
+ function reportNames(report, { file, lexical, key }, found, cwd) {
625
+ const flat = (value) => String(value).replaceAll('\\', '/');
626
+ const filePath = flat(file.path);
627
+ if (flat(report).includes(filePath)) return true;
628
+ const plain = report
629
+ .split(/[\s`"'<>()[\],;|]+/)
630
+ .map((token) => token.replace(/:\d+(?::\d+)?$/, '').replace(/[.:!?]+$/, ''))
631
+ .filter((token) => /\.[A-Za-z0-9]{1,8}$/.test(token) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(token));
632
+ const refs = new Set([...replyPaths(report), ...plain]);
633
+ const unique = found.filter((other) => other.file.name === file.name).length === 1;
634
+ for (const ref of refs) {
635
+ const normalized = flat(ref).replace(/^(?:\.\/)+/, '');
636
+ if (!normalized.includes('/')) {
637
+ if (unique && normalized === file.name) return true;
638
+ continue;
639
+ }
640
+ const refKey = lexicalKey(ref, cwd);
641
+ if (refKey === lexical || refKey === key) return true;
642
+ }
643
+ return false;
644
+ }
@@ -1,8 +1,9 @@
1
1
  // ask_user: put 1-4 multiple-choice questions to the user through the session's
2
2
  // tool-approval hook (kind: 'question') and report the answers to the model.
3
+ // handoff_user (the secret / takeover handoff) shares this module's human-gate code.
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import {
5
- validateAskUserHandoff,
6
+ validateUserHandoff,
6
7
  validateAskUserQuestions,
7
8
  ASK_USER_MAX_TEXT,
8
9
  } from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
@@ -78,7 +79,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
78
79
  const gate = {
79
80
  agentId: agentIdForSession(sessionId),
80
81
  kind: handoff.kind,
81
- tool: `ask_user:${handoff.surface}`,
82
+ tool: `handoff_user:${handoff.surface}`,
82
83
  target: handoff.target || handoff.field || '',
83
84
  };
84
85
  // A schedule or webhook run has nobody to take over: the step ends it now,
@@ -133,7 +134,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
133
134
  }
134
135
  const result = await runAbortable(signal, () =>
135
136
  hook({
136
- name: 'ask_user',
137
+ name: 'handoff_user',
137
138
  kind: 'secret',
138
139
  secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field, ...bound },
139
140
  reason: '',
@@ -171,7 +172,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
171
172
  : null;
172
173
  const result = await runAbortable(signal, () =>
173
174
  hook({
174
- name: 'ask_user',
175
+ name: 'handoff_user',
175
176
  kind: 'takeover',
176
177
  takeover: {
177
178
  surface: handoff.surface,
@@ -278,16 +279,14 @@ function formatAskUserResult(questions, answers, reply) {
278
279
  export function createAskUserToolHandlers({ rt }) {
279
280
  const { sessionIdFor, signalFor } = createCallerContextResolvers(rt);
280
281
  return {
282
+ handoff_user: async (args, { callerCtx }) =>
283
+ runAskUserHandoff(validateUserHandoff(args), {
284
+ hook: callerCtx?.toolApprovalHook,
285
+ sessionId: sessionIdFor(callerCtx),
286
+ toolCallId: callerCtx?.toolCallId || null,
287
+ signal: signalFor(callerCtx),
288
+ }),
281
289
  ask_user: async (args, { callerCtx }) => {
282
- const handoff = validateAskUserHandoff(args);
283
- if (handoff) {
284
- return runAskUserHandoff(handoff, {
285
- hook: callerCtx?.toolApprovalHook,
286
- sessionId: sessionIdFor(callerCtx),
287
- toolCallId: callerCtx?.toolCallId || null,
288
- signal: signalFor(callerCtx),
289
- });
290
- }
291
290
  const questions = validateAskUserQuestions(args);
292
291
  const hook = callerCtx?.toolApprovalHook;
293
292
  if (typeof hook !== 'function') return NO_USER;
@@ -83,6 +83,7 @@ export function createBridgeToolHandlers({ rt }) {
83
83
  }
84
84
  return await executeVisualizeTool(args, {
85
85
  sessionId: sessionIdFor(callerCtx),
86
+ callId: callerCtx?.toolCallId,
86
87
  cwd: callerCwd,
87
88
  signal: signalFor(callerCtx),
88
89
  });
@@ -4,7 +4,10 @@
4
4
  // the message is the user's row in the room's log. Never a coding session, a
5
5
  // delegated worker, or a run nobody attends.
6
6
  import { isAgentOwner } from '../../runtime/agent/orchestrator/agent-owner.mjs';
7
- import { noteReactionTarget, validateReactionArgs } from '../../runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs';
7
+ import {
8
+ noteReactionTarget,
9
+ validateReactionArgs,
10
+ } from '../../runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs';
8
11
  import { agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
9
12
  import { roomMemberOfSession, roomSessionId } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
10
13
  import { reactInSession } from '../../runtime/agent/orchestrator/session/message-reactions.mjs';
@@ -17,6 +17,7 @@ import { scheduleCallerScope } from '../services/schedule-access.mjs';
17
17
  import { worktreeCallerScope } from '../services/worktree-access.mjs';
18
18
  import { resolveHandoffAttachments, sessionRoots } from '../handoff-attachments.mjs';
19
19
  import { delegationChainOf } from '../bot-chain.mjs';
20
+ import { BOT_TARGET_ACTIONS, resolveBotTarget } from '../bot-target.mjs';
20
21
 
21
22
  const USER_TITLE_ERROR = 'session: the user already titled this session; the title was not changed';
22
23
 
@@ -50,6 +51,7 @@ export function createWorkspaceToolHandlers({
50
51
  getSessionManager = () => null,
51
52
  getLifecycleApi = () => null,
52
53
  executeSessionControl = null,
54
+ listBots,
53
55
  }) {
54
56
  const assertLead = (action, callerCtx) => {
55
57
  const callerSessionId = callerCtx?.callerSessionId || rt.session?.id || null;
@@ -110,12 +112,16 @@ export function createWorkspaceToolHandlers({
110
112
  'session: create belongs to a bot conversation; delegated workers, agent task sessions and schedule/webhook runs cannot create agent task sessions'
111
113
  );
112
114
  }
115
+ // A bot id names that bot's home chat; every check below and in the
116
+ // control runs on the chat it resolves to.
117
+ const sessionTarget =
118
+ BOT_TARGET_ACTIONS.has(action) && target ? await resolveBotTarget(target, listBots) : target;
113
119
  // A task session reaches its agent only through its reported result.
114
120
  const origin = rt.session?.taskOrigin;
115
121
  if (
116
122
  action === 'send' &&
117
123
  origin &&
118
- (target === origin.sessionId || agentIdOfHomeSession(target) === origin.agentId)
124
+ (sessionTarget === origin.sessionId || agentIdOfHomeSession(sessionTarget) === origin.agentId)
119
125
  ) {
120
126
  throw new Error('session: a task session reports to its agent only through its result');
121
127
  }
@@ -129,7 +135,7 @@ export function createWorkspaceToolHandlers({
129
135
  const attachmentParts = callerAttachmentParts(rt, args, callerCwd, 'session');
130
136
  return JSON.stringify(
131
137
  await executeSessionControl(
132
- { ...rest, action },
138
+ { ...rest, action, ...(sessionTarget !== target ? { session_id: sessionTarget } : {}) },
133
139
  {
134
140
  callerSessionId,
135
141
  callerAgentId,
@@ -150,14 +150,19 @@ export function buildColdHeir(source, now = Date.now()) {
150
150
  // earlier folder) is the source's own: the heir's prompt is built for the
151
151
  // folder it actually starts in. Project memory follows the Project, which a
152
152
  // worktree shares, so the source's current memory slice carries over.
153
+ // In the same folder only the environment facts are rebuilt: they name the
154
+ // heir's own scratch directory, never the source's, while the inherited
155
+ // instruction files and memory stay as the source presented them.
153
156
  const pending = source.workspaceReroot;
154
157
  delete heir.workspaceReroot;
155
- if ((projectFolder || pending) && heir.cwd) {
156
- refreshSessionWorkspaceContext(heir, {
157
- cwd: heir.cwd,
158
- ...buildWorkspacePromptContext(heir, heir.cwd),
159
- coreMemoryContext: pending?.target?.coreMemoryContext,
160
- });
158
+ if (heir.cwd) {
159
+ const context = buildWorkspacePromptContext(heir, heir.cwd);
160
+ refreshSessionWorkspaceContext(
161
+ heir,
162
+ projectFolder || pending
163
+ ? { cwd: heir.cwd, ...context, coreMemoryContext: pending?.target?.coreMemoryContext }
164
+ : { cwd: heir.cwd, environmentContext: context.environmentContext }
165
+ );
161
166
  }
162
167
  invalidateContextUsageSnapshot(heir);
163
168
  invalidateProviderContextBaseline(heir);
@@ -21,7 +21,11 @@ import {
21
21
  wantsIncomingTail,
22
22
  } from '../../runtime/agent/orchestrator/session/store-summary-last-message.mjs';
23
23
  import { isRealTurnMessage } from '../../runtime/agent/orchestrator/session/compact/user-message-kind.mjs';
24
- import { realTurnCount as realTurnCountOf } from '../../runtime/agent/orchestrator/session/store-summary-fields.mjs';
24
+ import {
25
+ compactCount as compactCountOf,
26
+ compactCountField,
27
+ realTurnCount as realTurnCountOf,
28
+ } from '../../runtime/agent/orchestrator/session/store-summary-fields.mjs';
25
29
 
26
30
  const VISIBLE_OWNERS = ['cli', 'user', 'mixdog'];
27
31
 
@@ -107,6 +111,10 @@ export function leadSessionRow(s, working = false) {
107
111
  // sit among the counted ones: a bot row's unread pill counts those past
108
112
  // the read cursor. Absent when unknown; clients then show one.
109
113
  ...(incomingTail ? { incomingTail } : {}),
114
+ // The numbering epoch of messageCount: each compaction restarts it. A
115
+ // summary row carries it at the top; a stored session in its compaction
116
+ // record. Unknown stays absent.
117
+ ...compactCountField(compactCountOf(s) === null ? s.compaction : s),
110
118
  title: cleanSessionPreview(s.title || '', 100),
111
119
  preview,
112
120
  // The latest user or assistant line (an agent contact's row shows it).
@@ -217,11 +217,20 @@ function switchSessionInPlace(deps, session) {
217
217
 
218
218
  /** The Lead's fallback models after this selection, checked like an agent's
219
219
  * chain (leaf routes, distinct providers): an explicit list replaces the
220
- * stored one ([] clears it), a selection without one keeps it. */
220
+ * stored one ([] clears it), a selection without one keeps it as stored — a
221
+ * fallback on the new main model's provider stays saved and is inactive
222
+ * (executionFallbacks). */
221
223
  async function leadFallbackChain(deps, next, primary) {
222
224
  const storedPreset = findPreset(deps.getConfig(), workflowPresetId('lead'));
223
- const requested = hasOwn(next || {}, 'fallbacks') ? next.fallbacks : storedPreset?.fallbacks;
225
+ const explicit = hasOwn(next || {}, 'fallbacks');
226
+ const requested = explicit ? next.fallbacks : storedPreset?.fallbacks;
224
227
  if (!Array.isArray(requested) || !requested.length) return [];
228
+ if (!explicit) {
229
+ return (
230
+ normalizeWorkflowRoute({ provider: primary.provider, model: primary.model, fallbacks: requested })?.fallbacks ||
231
+ []
232
+ );
233
+ }
225
234
  const chain = routeChain({ provider: primary.provider, model: primary.model, fallbacks: requested });
226
235
  validateRouteChainProviders(chain);
227
236
  const storedChain = routeChain(storedPreset || {});
@@ -37,6 +37,19 @@ let sharedPartialEntries = null;
37
37
  let sharedCatalogPromise = null;
38
38
  let sharedCatalogPromiseKey = null;
39
39
  let sharedCatalogPromiseForced = false;
40
+ // Every read that publishes to the shared state (a full load or a scoped
41
+ // refresh) runs through this queue, one at a time, so the last write is always
42
+ // the newest successful answer.
43
+ // A catalog state that moved on (new revision or account epoch) starts a fresh
44
+ // queue: reads of the old state never publish, so nothing waits for them.
45
+ let publishing = { key: null, tail: Promise.resolve() };
46
+ function serial(key, run) {
47
+ if (publishing.key !== key) publishing = { key, tail: Promise.resolve() };
48
+ const queue = publishing;
49
+ const result = queue.tail.then(run);
50
+ queue.tail = result.catch(() => {});
51
+ return result;
52
+ }
40
53
 
41
54
  export function catalogRevision(registry) {
42
55
  const value = Number(registry?.providerCatalogRevision?.());
@@ -60,6 +73,24 @@ export function invalidateSharedProviderCatalog() {
60
73
 
61
74
  const answered = (entry) => !Object.hasOwn(entry, 'error');
62
75
 
76
+ /** Folds a scoped refresh into the shared state of `key`: only the providers it
77
+ * answered are replaced; a failed entry replaces nothing. */
78
+ function mergeScopedEntries(key, scoped) {
79
+ const fresh = new Map(scoped.filter(answered).map((entry) => [entry.name, entry]));
80
+ const merge = (current) => current.map((entry) => fresh.get(entry.name) ?? entry);
81
+ if (sharedCatalogKey === key) {
82
+ sharedCatalogEntries = merge(sharedCatalogEntries);
83
+ } else if (sharedPartialKey === key) {
84
+ const merged = merge(sharedPartialEntries);
85
+ if (merged.every(answered)) {
86
+ // The refresh answered the last missing provider: the catalog is whole.
87
+ [sharedCatalogKey, sharedCatalogEntries, sharedPartialKey, sharedPartialEntries] = [key, merged, null, null];
88
+ } else {
89
+ sharedPartialEntries = merged;
90
+ }
91
+ }
92
+ }
93
+
63
94
  async function providerEntry(name, provider, timeoutMs, epoch) {
64
95
  if (typeof provider?.listModels !== 'function') return { name, models: [], ms: 0 };
65
96
  const startedAt = performance.now();
@@ -78,6 +109,23 @@ async function providerEntry(name, provider, timeoutMs, epoch) {
78
109
  return { name, models: Array.isArray(outcome.value) ? outcome.value : [], ms };
79
110
  }
80
111
 
112
+ /** Fresh entries for just the named providers (an account-pool provider is one
113
+ * entry, however many accounts it holds), merged into the shared catalog. */
114
+ export function scopedProviderCatalog(registry, names, { timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS } = {}) {
115
+ const key = catalogKey(registry);
116
+ const wanted = new Set(names);
117
+ return serial(key, async () => {
118
+ const entries = await Promise.all(
119
+ [...registry.getAllProviders()]
120
+ .filter(([name]) => wanted.has(name))
121
+ .map(([name, provider]) => providerEntry(name, provider, timeoutMs, key))
122
+ );
123
+ // A catalog state that moved on while this read ran is not written to.
124
+ if (catalogKey(registry) === key) mergeScopedEntries(key, entries);
125
+ return entries;
126
+ });
127
+ }
128
+
81
129
  export async function sharedProviderCatalog(registry, { force = false, timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS } = {}) {
82
130
  const key = catalogKey(registry);
83
131
  if (!force && sharedCatalogKey === key && Array.isArray(sharedCatalogEntries)) {
@@ -86,45 +134,43 @@ export async function sharedProviderCatalog(registry, { force = false, timeoutMs
86
134
  if (sharedCatalogPromise && sharedCatalogPromiseKey === key && (sharedCatalogPromiseForced || !force)) {
87
135
  return await sharedCatalogPromise;
88
136
  }
89
- const kept = new Map();
90
- if (!force && sharedPartialKey === key) {
91
- const now = Date.now();
92
- for (const entry of sharedPartialEntries) {
93
- if (answered(entry) || now < entry.retryAt) kept.set(entry.name, entry);
94
- }
95
- }
96
- const providers = [...registry.getAllProviders()];
97
- // Every missing provider is still inside its backoff: the partial answer is
98
- // the answer, without asking anyone.
99
- if (providers.length > 0 && providers.every(([name]) => kept.has(name))) {
100
- return providers.map(([name]) => kept.get(name));
101
- }
102
- // A pending list from an older catalog state (revision or account epoch) is
103
- // not joined: its answer may describe the previous catalog.
104
- const epoch = key;
105
137
  sharedCatalogPromiseKey = key;
106
138
  sharedCatalogPromiseForced = force;
107
- const request = Promise.all(
108
- providers.map(([name, provider]) => kept.get(name) || providerEntry(name, provider, timeoutMs, epoch))
109
- )
110
- .then((entries) => {
111
- if (sharedCatalogPromise === request && catalogKey(registry) === key) {
112
- const complete = entries.every(answered);
113
- const partial = !complete && entries.some(answered);
114
- sharedCatalogKey = complete ? key : null;
115
- sharedCatalogEntries = complete ? entries : null;
116
- sharedPartialKey = partial ? key : null;
117
- sharedPartialEntries = partial ? entries : null;
139
+ const request = serial(key, async () => {
140
+ const kept = new Map();
141
+ if (!force && sharedPartialKey === key) {
142
+ const now = Date.now();
143
+ for (const entry of sharedPartialEntries) {
144
+ if (answered(entry) || now < entry.retryAt) kept.set(entry.name, entry);
118
145
  }
119
- return entries;
120
- })
121
- .finally(() => {
122
- if (sharedCatalogPromise === request) {
123
- sharedCatalogPromise = null;
124
- sharedCatalogPromiseKey = null;
125
- sharedCatalogPromiseForced = false;
126
- }
127
- });
146
+ }
147
+ const providers = [...registry.getAllProviders()];
148
+ // Every missing provider is still inside its backoff: the partial answer is
149
+ // the answer, without asking anyone.
150
+ if (providers.length > 0 && providers.every(([name]) => kept.has(name))) {
151
+ return providers.map(([name]) => kept.get(name));
152
+ }
153
+ // A pending list from an older catalog state (revision or account epoch) is
154
+ // not joined: its answer may describe the previous catalog.
155
+ const entries = await Promise.all(
156
+ providers.map(([name, provider]) => kept.get(name) || providerEntry(name, provider, timeoutMs, key))
157
+ );
158
+ if (sharedCatalogPromise === request && catalogKey(registry) === key) {
159
+ const complete = entries.every(answered);
160
+ const partial = !complete && entries.some(answered);
161
+ sharedCatalogKey = complete ? key : null;
162
+ sharedCatalogEntries = complete ? entries : null;
163
+ sharedPartialKey = partial ? key : null;
164
+ sharedPartialEntries = partial ? entries : null;
165
+ }
166
+ return entries;
167
+ }).finally(() => {
168
+ if (sharedCatalogPromise === request) {
169
+ sharedCatalogPromise = null;
170
+ sharedCatalogPromiseKey = null;
171
+ sharedCatalogPromiseForced = false;
172
+ }
173
+ });
128
174
  sharedCatalogPromise = request;
129
175
  return await request;
130
176
  }
@@ -4,7 +4,8 @@
4
4
  // catalog flattened into deduplicated rows with per-provider profiling.
5
5
  import { modelProxyClientMode } from '../../runtime/shared/model-proxy-mode.mjs';
6
6
  import { isSelectableLlmModel } from '../model-recency.mjs';
7
- import { sharedProviderCatalog } from '../provider-catalog-cache.mjs';
7
+ import { scopedProviderCatalog, sharedProviderCatalog } from '../provider-catalog-cache.mjs';
8
+ import { providerRows } from '../services/agent-tool/model-select.mjs';
8
9
  import { NO_ENABLED_PROVIDERS } from '../../runtime/agent/orchestrator/providers/registry-errors.mjs';
9
10
 
10
11
  export function createCatalogLoader({
@@ -42,7 +43,7 @@ export function createCatalogLoader({
42
43
  * refresh invalidates it, leaving the UI on the stale first-open rows.
43
44
  * Resolves false when no provider is enabled: a fresh install has nothing
44
45
  * to load, and the registry refuses an all-disabled init by design. */
45
- async function prepare({ forceRefresh, loadSecrets }) {
46
+ async function prepare({ forceRefresh, loadSecrets, provider }) {
46
47
  if (loadSecrets) {
47
48
  const secretsStartedAt = performance.now();
48
49
  await awaitKeychainPrewarm();
@@ -69,7 +70,15 @@ export function createCatalogLoader({
69
70
  if (!forceRefresh && typeof reg().refreshProviderCatalogsOnStartup === 'function') {
70
71
  await reg().refreshProviderCatalogsOnStartup();
71
72
  }
72
- if (forceRefresh && typeof reg().refreshCatalogs === 'function') {
73
+ if (forceRefresh && provider) {
74
+ // A provider-scoped refresh asks only the registry providers the name
75
+ // matches and folds their entries into the shared catalog; every other
76
+ // provider keeps what it loaded.
77
+ const known = [...reg().getAllProviders().keys()].map((name) => ({ provider: name }));
78
+ const names = providerRows(known, provider).map((row) => row.provider);
79
+ await reg().refreshProviderCatalogsFor(names);
80
+ await scopedProviderCatalog(reg(), names, { timeoutMs: providerTimeoutMs });
81
+ } else if (forceRefresh && typeof reg().refreshCatalogs === 'function') {
73
82
  await reg().refreshCatalogs({ force: true });
74
83
  }
75
84
  profile('catalog-refresh-ready', { ms: (performance.now() - refreshStartedAt).toFixed(1) });
@@ -81,13 +90,18 @@ export function createCatalogLoader({
81
90
  * providers that failed or timed out, `request.partial` marks a load
82
91
  * where some but not all providers answered, and `request.retryAt` is when
83
92
  * the first missing provider may be asked again. */
84
- return async function loadProviderModelsFresh({ forceRefresh = false, loadSecrets = true, request = null } = {}) {
93
+ return async function loadProviderModelsFresh({
94
+ forceRefresh = false,
95
+ loadSecrets = true,
96
+ request = null,
97
+ provider = null,
98
+ } = {}) {
85
99
  const startedAt = performance.now();
86
100
  profile('load:start', { forceRefresh, loadSecrets });
87
101
  // No enabled provider is an empty catalog, not a failure. The request
88
102
  // stays incomplete so this answer is never cached: connecting a provider
89
103
  // is picked up by the next read.
90
- if (!(await prepare({ forceRefresh, loadSecrets }))) {
104
+ if (!(await prepare({ forceRefresh, loadSecrets, provider }))) {
91
105
  profile('load:done', { ms: (performance.now() - startedAt).toFixed(1), providers: 0, rows: 0 });
92
106
  return [];
93
107
  }
@@ -97,7 +111,10 @@ export function createCatalogLoader({
97
111
  // forward only for the still-current request, never for a superseded load.
98
112
  if (ownsLoad) request.seq = caches.providerModelsLoadSeq;
99
113
  const seq = request?.seq ?? caches.providerModelsLoadSeq;
100
- const catalogEntries = await sharedProviderCatalog(reg(), { force: forceRefresh, timeoutMs: providerTimeoutMs });
114
+ const catalogEntries = await sharedProviderCatalog(reg(), {
115
+ force: forceRefresh && !provider,
116
+ timeoutMs: providerTimeoutMs,
117
+ });
101
118
  if (request) {
102
119
  const failed = catalogEntries.filter((entry) => Object.hasOwn(entry, 'error'));
103
120
  request.missing = failed.map((entry) => entry.name);
@@ -156,7 +156,9 @@ export function createProviderModels({
156
156
  return promise;
157
157
  }
158
158
 
159
- async function collectProviderModels({ force = false, quick = false } = {}) {
159
+ /** `provider` (a name or fragment of one) scopes a forced read: only the
160
+ * providers it names are refreshed, the rest keep their loaded entries. */
161
+ async function collectProviderModels({ force = false, quick = false, provider = null } = {}) {
160
162
  meta.syncCatalogRevision();
161
163
  const cached = caches.providerModelsCache;
162
164
  if (!force && Array.isArray(cached.models)) {
@@ -180,7 +182,7 @@ export function createProviderModels({
180
182
  }
181
183
  if (force) {
182
184
  const request = { seq: ++caches.providerModelsLoadSeq };
183
- const models = await loadProviderModelsFresh({ forceRefresh: true, loadSecrets: true, request });
185
+ const models = await loadProviderModelsFresh({ forceRefresh: true, loadSecrets: true, request, provider });
184
186
  adoptProviderModelCache(models, request);
185
187
  return withCatalogState(rows.providerModelsFromCacheRows(models), partialOf(request));
186
188
  }
@@ -1,6 +1,11 @@
1
1
  // Memory control and recall: the current session's own rows first, then the
2
2
  // memory runtime (with a forced ingest of the live session when it is empty).
3
- import { clean, toolResponseText, isEmptyRecallText, currentSessionRecallRows } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
3
+ import {
4
+ clean,
5
+ toolResponseText,
6
+ isEmptyRecallText,
7
+ currentSessionRecallRows,
8
+ } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
4
9
 
5
10
  function isIdentityRecallQuery(query) {
6
11
  const q = clean(query).toLowerCase().replace(/\s+/g, '');
@@ -4,7 +4,11 @@
4
4
  //
5
5
  // Key names mirror the dependency names of settings-api.mjs / the session-build
6
6
  // deps, so the runtime can hand them straight through.
7
- import { builtinFeatureActive, featureDisallowedToolsFor, localGitToolsActive } from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
7
+ import {
8
+ builtinFeatureActive,
9
+ featureDisallowedToolsFor,
10
+ localGitToolsActive,
11
+ } from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
8
12
  import { recapEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
9
13
  import { browserBridgeAvailableSync } from '../runtime/browser-bridge/client.mjs';
10
14
  import { computerBridgeAvailableSync } from '../runtime/computer-bridge/client.mjs';