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
@@ -9,6 +9,11 @@
9
9
  export const IMPORT_WARNING_IMAGE_OMITTED = 'imageOmitted';
10
10
  export const IMPORT_WARNING_MALFORMED = 'malformed';
11
11
  export const IMPORT_WARNING_CWD_MISSING = 'cwdMissing';
12
+ /** The conversation continues a history stored in another file that was not followed. */
13
+ export const IMPORT_WARNING_HISTORY_PARTIAL = 'historyPartial';
14
+
15
+ /** Failure code of a record the source app writes for its own use (not a conversation). */
16
+ export const IMPORT_ERROR_INTERNAL = 'internalRecord';
12
17
 
13
18
  /** Stands in for an image the importer does not carry over. */
14
19
  export const IMAGE_OMITTED_TEXT = '[image omitted]';
@@ -17,12 +22,49 @@ export const IMPORTED_USER_STUB_TEXT = '[Imported conversation continues from an
17
22
 
18
23
  const TITLE_MAX_CHARS = 100;
19
24
 
20
- /** Plain-text note for a tool call: its name only, never its arguments. */
25
+ /** Note for a tool call: its name only, never its arguments. A run of notes
26
+ * becomes one compact line in the text and one structured entry in `toolRuns`. */
21
27
  export function toolNote(name) {
22
- const clean = String(name || '')
23
- .replace(/\s+/g, ' ')
24
- .trim();
25
- return { text: `[ran tool: ${clean || 'unknown'}]`, note: true };
28
+ const clean =
29
+ String(name || '')
30
+ .replace(/\s+/g, ' ')
31
+ .trim() || 'unknown';
32
+ return { text: `[ran tool: ${clean}]`, note: true, tool: clean };
33
+ }
34
+
35
+ /** Longest model-facing run line, and the longest tool name shown in it. */
36
+ export const TOOL_RUN_LINE_MAX_CHARS = 400;
37
+ const TOOL_RUN_NAME_MAX_CHARS = 40;
38
+
39
+ /**
40
+ * Model-facing line for one run of tool notes: `[ran tools: shell ×6, Read ×2]`.
41
+ * Bounded: names are clipped, and tools that do not fit are counted as
42
+ * `, +N more`. Deterministic in `tools`, so a stored line can be re-derived.
43
+ */
44
+ export function toolRunLine(tools) {
45
+ const entries = tools.map(({ name, count }) => {
46
+ const chars = Array.from(String(name));
47
+ const shown =
48
+ chars.length > TOOL_RUN_NAME_MAX_CHARS ? `${chars.slice(0, TOOL_RUN_NAME_MAX_CHARS - 1).join('')}…` : name;
49
+ return `${shown} ×${count}`;
50
+ });
51
+ const omittedSuffix = (omitted) => (omitted > 0 ? `, +${omitted} more` : '');
52
+ let body = '';
53
+ let shownCount = 0;
54
+ for (const entry of entries) {
55
+ const next = shownCount ? `${body}, ${entry}` : entry;
56
+ const length = `[ran tools: ${next}${omittedSuffix(entries.length - shownCount - 1)}]`.length;
57
+ if (shownCount && length > TOOL_RUN_LINE_MAX_CHARS) break;
58
+ body = next;
59
+ shownCount += 1;
60
+ }
61
+ return `[ran tools: ${body}${omittedSuffix(entries.length - shownCount)}]`;
62
+ }
63
+
64
+ function toolRunCounts(notes) {
65
+ const counts = new Map();
66
+ for (const note of notes) counts.set(note.tool, (counts.get(note.tool) || 0) + 1);
67
+ return [...counts].map(([name, count]) => ({ name, count }));
26
68
  }
27
69
 
28
70
  export function textPart(text) {
@@ -42,18 +84,31 @@ export function timestampMs(value) {
42
84
  return Number.isFinite(ms) && ms > 0 ? ms : null;
43
85
  }
44
86
 
45
- // Consecutive tool notes stay one per line; every other boundary is a blank line.
87
+ // Each run of consecutive tool notes collapses into one compact line; every
88
+ // boundary is a blank line. `toolRuns` locates those lines in `text`.
46
89
  function joinParts(parts) {
47
- let text = '';
48
- let previous = null;
90
+ const blocks = [];
49
91
  for (const part of parts) {
50
92
  const piece = String(part.text || '').trim();
51
93
  if (!piece) continue;
52
- if (previous) text += previous.note && part.note ? '\n' : '\n\n';
53
- text += piece;
54
- previous = part;
94
+ const last = blocks[blocks.length - 1];
95
+ if (part.note && last?.notes) last.notes.push(part);
96
+ else blocks.push(part.note ? { notes: [part] } : { text: piece });
97
+ }
98
+ let text = '';
99
+ const toolRuns = [];
100
+ for (const block of blocks) {
101
+ if (text) text += '\n\n';
102
+ if (block.notes) {
103
+ const tools = toolRunCounts(block.notes);
104
+ const line = toolRunLine(tools);
105
+ toolRuns.push({ start: text.length, end: text.length + line.length, tools });
106
+ text += line;
107
+ } else {
108
+ text += block.text;
109
+ }
55
110
  }
56
- return { text, last: previous };
111
+ return { text, toolRuns };
57
112
  }
58
113
 
59
114
  /**
@@ -72,9 +127,15 @@ export function normalizeTurns(rawTurns) {
72
127
  else merged.push({ role: raw.role, parts: [...parts], at: timestampMs(raw.at) });
73
128
  }
74
129
  const turns = merged.map(({ role, parts, at }) => {
75
- const turn = { role, text: joinParts(parts).text, at };
76
- // A carried-over compaction summary never names the conversation.
77
- if (parts.some((part) => part.summary)) turn.titleText = joinParts(parts.filter((part) => !part.summary)).text;
130
+ const joined = joinParts(parts);
131
+ const turn = { role, text: joined.text, at };
132
+ if (joined.toolRuns.length) turn.toolRuns = joined.toolRuns;
133
+ // A carried-over compaction summary never names the conversation; a slash
134
+ // command only when no typed prompt does.
135
+ if (parts.some((part) => part.summary || part.command)) {
136
+ turn.titleText = joinParts(parts.filter((part) => !part.summary && !part.command)).text;
137
+ }
138
+ if (parts.some((part) => part.command)) turn.commandText = joinParts(parts.filter((part) => part.command)).text;
78
139
  return turn;
79
140
  });
80
141
  if (turns[0]?.role === 'assistant') {
@@ -83,19 +144,26 @@ export function normalizeTurns(rawTurns) {
83
144
  return turns;
84
145
  }
85
146
 
86
- const titleLines = (turn) =>
87
- (turn.titleText ?? turn.text)
147
+ const textLines = (text) =>
148
+ String(text ?? '')
88
149
  .split('\n')
89
150
  .map((value) => value.replace(/\s+/g, ' ').trim())
90
151
  .filter(Boolean);
91
152
 
153
+ const titleLines = (turn) => textLines(turn.titleText ?? turn.text);
154
+
155
+ /** First non-empty line of a title text, whitespace-normalized, ≤100 chars. */
156
+ export function titleFromText(text) {
157
+ const line = textLines(text)[0];
158
+ if (!line) return null;
159
+ return Array.from(line).slice(0, TITLE_MAX_CHARS).join('').trim() || null;
160
+ }
161
+
92
162
  /** First line of the first genuine user turn, whitespace-normalized, ≤100 chars
93
- * (an image marker only when the turn has no text line). */
163
+ * (an image marker only when the turn has no text line), else the first slash command. */
94
164
  export function conversationTitle(turns) {
95
165
  const first = turns.find((turn) => turn.role === 'user' && !turn.stub && titleLines(turn).length);
96
- if (!first) return null;
166
+ if (!first) return titleFromText(turns.find((turn) => turn.role === 'user' && turn.commandText)?.commandText);
97
167
  const lines = titleLines(first);
98
- const line = lines.find((value) => value !== IMAGE_OMITTED_TEXT) || lines[0];
99
- if (!line) return null;
100
- return Array.from(line).slice(0, TITLE_MAX_CHARS).join('').trim() || null;
168
+ return titleFromText(lines.find((value) => value !== IMAGE_OMITTED_TEXT) || lines[0]);
101
169
  }
@@ -16,6 +16,9 @@ import { CONTEXT_SHARE_RATIO, COMPACT_TARGET_MIN_TOKENS, COMPACT_SAFETY_PERCENT
16
16
  import { HANDOFF_TIMEOUT_MAX_MS } from '../compact/constants.mjs';
17
17
  import { envFlag, envPositiveInt } from '../../../../shared/env.mjs';
18
18
  import { positiveInt } from '../../../../shared/numbers.mjs';
19
+ import { cacheWarmUseFor, lastCacheWarmUseAt } from '../../../../shared/idle-auto-clear-gate.mjs';
20
+ import { ACCOUNT_PROVIDERS } from '../../../../shared/provider-accounts.mjs';
21
+ import { plannedAccountId } from '../../providers/account-pool.mjs';
19
22
  import { isAgentOwner } from '../../agent-owner.mjs';
20
23
  import { isAgentModeSession } from '../agent-mode.mjs';
21
24
  import { providerInputExcludesCache, providerNativePdf } from '../../providers/registry.mjs';
@@ -703,13 +706,40 @@ export function shouldCompactForRequestMedia(messages, provider = '') {
703
706
  // Anthropic's 5m message-tail TTL (agent-runtime/cache-strategy.mjs gives it
704
707
  // to agent sessions). A send this long after the previous one finds the tail
705
708
  // cache expired and would rewrite the whole accumulated transcript; compacting
706
- // first makes that cold write the compacted transcript instead.
709
+ // first makes that cold write the compacted transcript instead. The tail was
710
+ // last used by the session's own send or by a successful cache warm
711
+ // (cache-warm.mjs), which reads and refreshes it without moving
712
+ // lastProviderSendAt: a warmed tail has not expired — for a request on the
713
+ // warm's provider, model and account only, since the entry lives there.
707
714
  const AGENT_MESSAGE_CACHE_TTL_MS = 5 * 60_000;
708
715
 
709
- export function shouldCompactForExpiredAgentCache(sessionRef, opts) {
716
+ // The account the request would be served by: the roster's first pick for a
717
+ // provider with accounts (account-pool.mjs), '' for one without a roster.
718
+ function requestAccountId(provider, model, sessionRef, opts, messages) {
719
+ if (!ACCOUNT_PROVIDERS.includes(provider)) return '';
720
+ return plannedAccountId(provider, {
721
+ sessionId: opts?.sessionId || sessionRef?.id || null,
722
+ messages,
723
+ providerState: opts?.providerState,
724
+ model,
725
+ });
726
+ }
727
+
728
+ /** Whether the agent session's 5m message tail expired before this request
729
+ * on `provider` / `model` (default: the session's route). */
730
+ export function shouldCompactForExpiredAgentCache(sessionRef, opts, { provider, model, messages = [] } = {}) {
710
731
  if (!isAgentOwner(sessionRef) || opts?.cacheStrategy?.messages !== '5m') return false;
711
732
  const lastSendAt = Number(sessionRef.lastProviderSendAt);
712
- return lastSendAt > 0 && Date.now() - lastSendAt > AGENT_MESSAGE_CACHE_TTL_MS;
733
+ if (!(lastSendAt > 0)) return false;
734
+ const now = Date.now();
735
+ if (now - lastSendAt <= AGENT_MESSAGE_CACHE_TTL_MS) return false;
736
+ // No warm since the last send: the tail expired.
737
+ if (!(lastCacheWarmUseAt(sessionRef.id) > lastSendAt)) return true;
738
+ const route = { provider: provider || sessionRef.provider || '', model: model || sessionRef.model || '' };
739
+ const accountId = requestAccountId(route.provider, route.model, sessionRef, opts, messages);
740
+ if (accountId === null) return true;
741
+ const warmAt = cacheWarmUseFor(sessionRef.id, { ...route, accountId });
742
+ return now - Math.max(lastSendAt, warmAt) > AGENT_MESSAGE_CACHE_TTL_MS;
713
743
  }
714
744
 
715
745
  export function shouldCompactForSession(
@@ -278,14 +278,19 @@ function currentSkillResolver(sessionRef, { preview = false } = {}) {
278
278
  return (name) => {
279
279
  const delivery = currentSkillDelivery(name, cwd, config, skills);
280
280
  if (!delivery) return null;
281
- if (preview) return { body: delivery.body, native: undefined };
282
281
  const { resource } = delivery;
282
+ // The tools whose use counts as use of the skill: its tool dependencies,
283
+ // an MCP server as `mcp:<server>`.
284
+ const tools = (resource.toolDependencies || []).flatMap(({ type, value }) =>
285
+ type === 'tool' ? [value] : type === 'mcp' ? [`mcp:${value}`] : []
286
+ );
287
+ if (preview) return { body: delivery.body, native: undefined, tools };
283
288
  const envelope = loadSkillToolDependencies(
284
289
  buildSkillToolEnvelope(name, resource.content, resource.dir, resource),
285
290
  sessionRef
286
291
  );
287
292
  const native = parseNativeToolSearchPayload('Skill', envelope.result);
288
- return { body: delivery.body, native, summary: native?.summary ?? envelope.result };
293
+ return { body: delivery.body, native, summary: native?.summary ?? envelope.result, tools };
289
294
  };
290
295
  }
291
296
 
@@ -19,7 +19,7 @@ import { providerToolExclusionReason } from './provider-tool-exclusions.mjs';
19
19
  import { workflowOverlayDenial } from '../manager/workflow-switch-notice.mjs';
20
20
  import { agentMcpToolPermission, connectorPolicyAgentOf, projectMcpToolDisabled } from '../../mcp/agent-connectors.mjs';
21
21
 
22
- const WORKER_DENIED_TOOLS = new Set(['agent', 'ask_user', 'session']);
22
+ const WORKER_DENIED_TOOLS = new Set(['agent', 'ask_user', 'handoff_user', 'session']);
23
23
  const BRIDGE_DISCOVERY_TOOL_NAMES = new Set(['load_tool']);
24
24
 
25
25
  const IMAGE_PATH_RE = /\.(?:png|jpe?g|gif|webp)(?:$|[?#])/i;
@@ -31,6 +31,13 @@ function exclusionsFor(sessionRef) {
31
31
 
32
32
  const isSessionRef = (value) => value !== null && typeof value === 'object';
33
33
 
34
+ // A schema 400 drop holds for the schema rule that refused it: a route on
35
+ // another wire family (a fallback hop, a model switch) sends the tool again.
36
+ function droppedOnRoute(sessionRef, name) {
37
+ const entry = isSessionRef(sessionRef) ? sessions.get(sessionRef)?.dropped.get(String(name || '')) : null;
38
+ return entry && entry.family === routeSchemaFamily(sessionRef) ? entry : null;
39
+ }
40
+
34
41
  /** A new provider request: exclusions describe the latest request only. */
35
42
  export function beginProviderToolExclusions(sessionRef) {
36
43
  if (isSessionRef(sessionRef)) sessions.get(sessionRef)?.current.clear();
@@ -64,15 +71,16 @@ export function providerToolExclusionReason(sessionRef, name) {
64
71
  if (!isSessionRef(sessionRef)) return null;
65
72
  const state = sessions.get(sessionRef);
66
73
  const key = String(name || '');
67
- const exclusion = state?.current.get(key) || state?.dropped.get(key);
74
+ const exclusion = state?.current.get(key) || droppedOnRoute(sessionRef, key);
68
75
  return exclusion ? unavailableText(exclusion) : null;
69
76
  }
70
77
 
71
78
  /** The provider rejected `exclusion.name`'s schema with an HTTP 400: leave the
72
- * tool out of every later request of the session and tell the user once. */
79
+ * tool out of the session's later requests on this wire family and tell the
80
+ * user once. */
73
81
  export function dropSessionTool(sessionRef, exclusion, onStageChange) {
74
82
  if (!isSessionRef(sessionRef)) return;
75
- exclusionsFor(sessionRef).dropped.set(exclusion.name, exclusion);
83
+ exclusionsFor(sessionRef).dropped.set(exclusion.name, { ...exclusion, family: routeSchemaFamily(sessionRef) });
76
84
  recordProviderToolExclusion(sessionRef, exclusion, onStageChange);
77
85
  }
78
86
 
@@ -82,7 +90,7 @@ export function withoutDroppedTools(sessionRef, { sendTools, requestToolScope })
82
90
  const dropped = isSessionRef(sessionRef) ? sessions.get(sessionRef)?.dropped : null;
83
91
  if (!dropped?.size) return { sendTools, requestToolScope };
84
92
  const prefix = providerNativeToolPrefixCount(sendTools);
85
- const kept = sendTools.filter((tool, index) => index < prefix || !dropped.has(tool?.name));
93
+ const kept = sendTools.filter((tool, index) => index < prefix || !droppedOnRoute(sessionRef, tool?.name));
86
94
  if (kept.length === sendTools.length) return { sendTools, requestToolScope };
87
95
  const next = finalizeProviderRequestTools(kept, prefix);
88
96
  return { sendTools: next, requestToolScope: { ...requestToolScope, requestTools: next } };
@@ -128,7 +136,7 @@ function schemaExclusion(session, family, tool) {
128
136
  * tools are never excluded by the schema rule. */
129
137
  export function routeToolExclusionReason(session, tool) {
130
138
  if (!tool) return null;
131
- const dropped = isSessionRef(session) ? sessions.get(session)?.dropped.get(String(tool.name || '')) : null;
139
+ const dropped = droppedOnRoute(session, tool.name);
132
140
  if (dropped) return unavailableText(dropped);
133
141
  const exclusion = schemaExclusion(session, routeSchemaFamily(session), tool);
134
142
  return exclusion ? unavailableText(exclusion) : null;
@@ -17,6 +17,8 @@ import { buildRequestSendOptions } from '../manager/ask-turn-loop-options.mjs';
17
17
  import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
18
18
  import { resolveSessionContextMeta } from '../manager/context-meta.mjs';
19
19
  import { stripProviderReplayMessages } from '../manager/provider-replay.mjs';
20
+ import { isAgentOwner } from '../../agent-owner.mjs';
21
+ import { rebuildToolSurfaceForProviderWithMigration } from './tool-exposure-migration.mjs';
20
22
 
21
23
  // The short reason a notice gives for leaving a model, by provider kind.
22
24
  const HOP_REASONS = {
@@ -149,6 +151,11 @@ export async function advanceAgentRoute(state, error) {
149
151
  updated.routeFallbackFailures = [...(session.routeFallbackFailures || []), failed];
150
152
  await pinSessionWireRoute(updated, provider);
151
153
  state.throwIfAborted();
154
+ // The route change replaced the session's tools with its whole resolved list.
155
+ // A delegated agent's surface was already re-applied by updateSessionRoute
156
+ // (applyAgentToolExposure); a Lead-class session gets its deferred surface
157
+ // restored for the new provider, as a manual model switch does.
158
+ if (!isAgentOwner(updated)) rebuildToolSurfaceForProviderWithMigration(updated, next.provider, state.messages);
152
159
  // The live turn (prompt, assistant tool calls, tool results) carries on;
153
160
  // only the previous provider's native replay state is dropped from it.
154
161
  state.messages.splice(0, state.messages.length, ...stripProviderReplayMessages(state.messages));
@@ -164,6 +171,13 @@ export async function advanceAgentRoute(state, error) {
164
171
  state.cacheBreakTraceKeys.clear();
165
172
  state.transportRetriesUsed = 0;
166
173
  state.transportRetryMax = 0;
174
+ // The one-shot recoveries the left model spent belong to its requests; the
175
+ // next model's first request gets its own (its window, images and schemas).
176
+ state.contextOverflowRetryUsed = false;
177
+ state.imageStripUsed = false;
178
+ state.imageStrip = null;
179
+ state.thinkingReplayRepairUsed = false;
180
+ state.toolSchemaDropUsed = false;
167
181
  for (const key of Object.keys(oldOptions)) delete state.opts[key];
168
182
  Object.assign(
169
183
  state.opts,
@@ -4,7 +4,12 @@
4
4
  * consume a retry verdict, and settle a completed send into the loop state.
5
5
  */
6
6
  import { traceCacheBreak } from '../../cache-break-trace.mjs';
7
- import { anthropicPrefixChanges } from '../../providers/lib/anthropic-prefix-trace.mjs';
7
+ import {
8
+ anthropicPrefixChanges,
9
+ latestAnthropicPrefix,
10
+ persistableAnthropicPrefix,
11
+ seedAnthropicPrefix,
12
+ } from '../../providers/lib/anthropic-prefix-trace.mjs';
8
13
  import { resolveLiveToolCwd } from './tool-exec.mjs';
9
14
  import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
10
15
  import { addUsage, usageDeltaEvent } from './usage.mjs';
@@ -179,6 +184,9 @@ export async function sendProviderRequest(state, round) {
179
184
  } catch {
180
185
  // the send-started observer is advisory
181
186
  }
187
+ // Diagnostic only: a restarted process compares its first request with the
188
+ // last prefix fingerprint the session record kept (anthropic-prefix-trace.mjs).
189
+ seedAnthropicPrefix(sessionId, sessionRef?.providerPrefixBaseline);
182
190
  // The request a /btw side question forks: kept while it is in flight, with
183
191
  // its reply once it proceeds, and dropped again if it fails.
184
192
  const sideQuestion = captureSideQuestionRequest({
@@ -374,6 +382,10 @@ function traceActualCacheMiss(state, sent, response, { previousGuardState, previ
374
382
  );
375
383
  }
376
384
  sessionRef.lastProviderCachedPrefixTokens = cachePrefixTokens;
385
+ // The fingerprint of this request survives a restart on the record, so the
386
+ // next process's first miss can still name what moved.
387
+ const fingerprint = persistableAnthropicPrefix(latestAnthropicPrefix(sessionId));
388
+ if (fingerprint) sessionRef.providerPrefixBaseline = fingerprint;
377
389
  }
378
390
 
379
391
  /** Fold a completed send into the state: prefix guard, image-strip
@@ -132,8 +132,8 @@ export async function applyBeforeToolHook({
132
132
  mcpScopeId = null,
133
133
  }) {
134
134
  const call = { name, args, cwd };
135
- // ask_user is itself the user prompt; it never goes through the ordinary approval gate.
136
- if (name === 'ask_user') return call;
135
+ // ask_user and handoff_user are themselves the user prompt; they never go through the ordinary approval gate.
136
+ if (name === 'ask_user' || name === 'handoff_user') return call;
137
137
  try {
138
138
  const hookDecision = beforeToolHook
139
139
  ? await runAbortable(executeOpts.signal, () =>
@@ -46,12 +46,44 @@ export function markToolSurfaceMigrated(session, intent = TOOL_SURFACE_MIGRATION
46
46
  session.pendingCacheBreakIntent = intent;
47
47
  }
48
48
 
49
- /** Provider switch: rebuild the surface for `provider`, migrating bridge ↔ native. */
50
- export function rebuildToolSurfaceForProviderWithMigration(session, provider) {
49
+ /**
50
+ * Run `apply` with `messages` (the transcript a running turn works on) as the
51
+ * session's messages, so a fixed-guidance rewrite lands there; the canonical
52
+ * transcript takes the rewritten system prefix. Without `messages`, `apply`
53
+ * runs on the session's own. `apply` runs synchronously.
54
+ */
55
+ function applyOnWorkingMessages(session, messages, apply) {
56
+ const stored = session.messages;
57
+ const working = Array.isArray(messages) ? messages : stored;
58
+ if (Array.isArray(working)) session.messages = working;
59
+ try {
60
+ apply();
61
+ } finally {
62
+ if (Array.isArray(working) && session.messages !== working) {
63
+ // The rewrite may drop an emptied system message: keep the array
64
+ // identity the request loop holds.
65
+ working.splice(0, working.length, ...session.messages);
66
+ }
67
+ session.messages = stored;
68
+ }
69
+ // The rewritten prompt messages are fresh objects; the canonical transcript
70
+ // the save path persists takes them too, so the next save writes the new
71
+ // prompt together with the new exposure.
72
+ if (Array.isArray(stored) && Array.isArray(working) && stored !== working) syncSystemPrefix(stored, working);
73
+ }
74
+
75
+ /**
76
+ * Provider switch: rebuild the surface for `provider`, migrating bridge ↔
77
+ * native. `messages`: the running turn's transcript, when a switch happens
78
+ * mid-turn (a route fallback hop).
79
+ */
80
+ export function rebuildToolSurfaceForProviderWithMigration(session, provider, messages) {
51
81
  if (!session) return false;
52
82
  const migrating = toolExposureMigrationNeeded(session, provider);
53
- rebuildDeferredToolSurfaceForProvider(session, provider);
54
- if (migrating) markToolSurfaceMigrated(session);
83
+ applyOnWorkingMessages(session, messages, () => {
84
+ rebuildDeferredToolSurfaceForProvider(session, provider);
85
+ if (migrating) markToolSurfaceMigrated(session);
86
+ });
55
87
  return migrating;
56
88
  }
57
89
 
@@ -75,26 +107,12 @@ function hasUnansweredToolCalls(messages) {
75
107
  */
76
108
  export function migrateToolExposureAtBoundary(session, messages) {
77
109
  if (!session || !toolExposureMigrationNeeded(session) || hasUnansweredToolCalls(messages)) return false;
78
- const stored = session.messages;
79
- const working = Array.isArray(messages) ? messages : stored;
80
- if (Array.isArray(working)) session.messages = working;
81
- try {
110
+ applyOnWorkingMessages(session, messages, () =>
82
111
  applyDeferredToolSurface(session, session.deferredSurfaceMode || 'lead', [], {
83
112
  provider: session.provider,
84
113
  model: session.model,
85
- });
86
- } finally {
87
- if (Array.isArray(working) && session.messages !== working) {
88
- // The rewrite may drop an emptied system message: keep the request
89
- // transcript array identity the loop holds.
90
- working.splice(0, working.length, ...session.messages);
91
- }
92
- session.messages = stored;
93
- }
94
- // The rewritten prompt messages are fresh objects; the canonical transcript
95
- // the save path persists takes them too, so the next save writes the new
96
- // prompt together with the new exposure.
97
- if (Array.isArray(stored) && Array.isArray(working) && stored !== working) syncSystemPrefix(stored, working);
114
+ })
115
+ );
98
116
  invalidateForToolSurfaceMigration(session);
99
117
  return true;
100
118
  }
@@ -231,9 +231,9 @@ export function buildAgentLoopOptions({
231
231
  },
232
232
  onStreamDelta: (kind = 'semantic') => {
233
233
  markSessionStreamDelta(sessionId, kind).catch(() => {});
234
- // Raw transport is an internal health signal, not model progress.
235
- // Preserve the public callback's historical semantic-only contract.
236
- if (kind !== 'transport') relay(askOpts?.onStreamDelta, kind);
234
+ // Raw transport and keepalives are internal health signals, not model
235
+ // progress. Preserve the public callback's historical semantic-only contract.
236
+ if (kind !== 'transport' && kind !== 'keepalive') relay(askOpts?.onStreamDelta, kind);
237
237
  },
238
238
  };
239
239
  }
@@ -225,7 +225,9 @@ export function publishAskTurn({ sessionId, opened, terminalResultPreview, askOp
225
225
  // (which inspects entry.emptyFinal first) classifies the
226
226
  // terminal state correctly even if it ticks during unwind.
227
227
  const isEmptyFinal =
228
- !terminalResultPreview.content && !terminalResultPreview.reasoningContent && !terminalResultPreview.reactionCompleted;
228
+ !terminalResultPreview.content &&
229
+ !terminalResultPreview.reasoningContent &&
230
+ !terminalResultPreview.reactionCompleted;
229
231
  if (isEmptyFinal) {
230
232
  markSessionEmptyFinal(sessionId);
231
233
  }
@@ -13,7 +13,7 @@
13
13
  import { loadConfig } from '../../config.mjs';
14
14
  import { findPreset } from '../../runtime-core/config-helpers.mjs';
15
15
  import { normalizeWorkflowRoute } from '../../runtime-core/workflow.mjs';
16
- import { canonicalRouteProvider } from '../../../../shared/model-route-chain.mjs';
16
+ import { canonicalRouteProvider, executionFallbacks } from '../../../../shared/model-route-chain.mjs';
17
17
  import { storedBotRoute } from '../../../../shared/agent-store.mjs';
18
18
  import { configuredAgentRouteCandidates } from '../../../../shared/agent-route-config.mjs';
19
19
  import { CODING_MODE } from '../agent-mode.mjs';
@@ -48,5 +48,5 @@ export function ownerConfiguredRoute({ sessionMode, agentId, config = null }) {
48
48
  export function ownerRouteFallbacks({ sessionMode, agentId, provider, model, config = null }) {
49
49
  const route = ownerConfiguredRoute({ sessionMode, agentId, config });
50
50
  if (!route?.fallbacks?.length) return [];
51
- return sameRouteModel(route, { provider, model }) ? route.fallbacks : [];
51
+ return sameRouteModel(route, { provider, model }) ? executionFallbacks(route) : [];
52
52
  }
@@ -12,6 +12,7 @@
12
12
  import { findPreset } from '../../runtime-core/config-helpers.mjs';
13
13
  import { normalizeWorkflowRoute } from '../../runtime-core/workflow.mjs';
14
14
  import { configuredAgentRouteCandidates } from '../../../../shared/agent-route-config.mjs';
15
+ import { executionFallbacks } from '../../../../shared/model-route-chain.mjs';
15
16
  import { CODING_MODE, resolveSessionMode } from '../agent-mode.mjs';
16
17
  import { _liveSessions } from '../store/live-state.mjs';
17
18
  import { ownerConfiguredRoute, sameRouteModel } from './owner-route-fallbacks.mjs';
@@ -64,7 +65,7 @@ export function liveRouteFallbacks(session, config) {
64
65
  if (!source)
65
66
  return { chain: Array.isArray(session?.routeFallbacks) ? session.routeFallbacks : [], fromSettings: false };
66
67
  const onSourcePrimary = sameRouteModel(source, sessionRoutePrimary(session));
67
- return { chain: onSourcePrimary ? source.fallbacks || [] : [], fromSettings: true };
68
+ return { chain: onSourcePrimary ? executionFallbacks(source) : [], fromSettings: true };
68
69
  }
69
70
 
70
71
  /** Index in `chain` of the next route to try: the first after the session's
@@ -59,15 +59,19 @@ function omitToolsKey(omitTools) {
59
59
  .join(',');
60
60
  }
61
61
 
62
- export function _buildSharedRules({ omitTools = [], allowTools = null } = {}) {
62
+ export function _buildSharedRules({ omitTools = [], allowTools = null, ownerIsAgent = false } = {}) {
63
63
  const PLUGIN_ROOT = mixdogRoot();
64
64
  const RULES_DIR = join(PLUGIN_ROOT, 'rules');
65
65
  return buildCachedRules(
66
66
  'buildSharedToolContent',
67
67
  'shared tool rules',
68
68
  [join(RULES_DIR, 'shared')],
69
- { PLUGIN_ROOT, DATA_DIR: resolvePluginData(), omitTools, allowTools },
70
- JSON.stringify([omitToolsKey(omitTools), Array.isArray(allowTools) ? omitToolsKey(allowTools) : null])
69
+ { PLUGIN_ROOT, DATA_DIR: resolvePluginData(), omitTools, allowTools, ownerIsAgent },
70
+ JSON.stringify([
71
+ omitToolsKey(omitTools),
72
+ Array.isArray(allowTools) ? omitToolsKey(allowTools) : null,
73
+ ownerIsAgent === true,
74
+ ])
71
75
  );
72
76
  }
73
77
 
@@ -98,8 +102,17 @@ export function _buildRouteRules({ provider = null, model = null, omitTools = []
98
102
 
99
103
  // BP1 = shared tool policy followed by this route's rules. Every site that
100
104
  // renders or re-identifies the BP1 block must use this one composition.
101
- export function _buildBaseRules({ provider = null, model = null, omitTools = [], allowTools = null } = {}) {
102
- return [_buildSharedRules({ omitTools, allowTools }), _buildRouteRules({ provider, model, omitTools, allowTools })]
105
+ export function _buildBaseRules({
106
+ provider = null,
107
+ model = null,
108
+ omitTools = [],
109
+ allowTools = null,
110
+ ownerIsAgent = false,
111
+ } = {}) {
112
+ return [
113
+ _buildSharedRules({ omitTools, allowTools, ownerIsAgent }),
114
+ _buildRouteRules({ provider, model, omitTools, allowTools }),
115
+ ]
103
116
  .filter(Boolean)
104
117
  .join('\n\n---\n\n');
105
118
  }
@@ -201,9 +214,14 @@ export function _buildLeadLanguage() {
201
214
  });
202
215
  }
203
216
 
204
- export function _buildLeadLanguageContext() {
217
+ export function _buildLeadLanguageContext({ includeOutputStyle = true } = {}) {
218
+ const PLUGIN_ROOT = mixdogRoot();
205
219
  const DATA_DIR = resolvePluginData();
206
- return buildCachedRules('buildLeadLanguageContent', 'lead language context', [join(DATA_DIR, 'mixdog-config.json')], {
207
- DATA_DIR,
208
- });
220
+ return buildCachedRules(
221
+ 'buildLeadLanguageContent',
222
+ 'lead language context',
223
+ [join(DATA_DIR, 'mixdog-config.json'), join(PLUGIN_ROOT, 'output-styles'), join(DATA_DIR, 'output-styles')],
224
+ { PLUGIN_ROOT, DATA_DIR, includeOutputStyle },
225
+ includeOutputStyle ? 'style' : 'no-style'
226
+ );
209
227
  }