mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -50,10 +50,77 @@ function formatDurationMs(value) {
50
50
  return `${ms}ms`;
51
51
  }
52
52
 
53
+ function compactDurationForStatus(value, { preferMinutes = false } = {}) {
54
+ const ms = Math.max(0, Number(value) || 0);
55
+ if (ms >= 1_000) {
56
+ if (preferMinutes && ms >= 60_000 && ms % 60_000 === 0) return `${Math.round(ms / 60_000)}m`;
57
+ if (ms < 600_000 && ms % 1_000 === 0) return `${Math.round(ms / 1_000)}s`;
58
+ if (ms >= 60_000 && ms % 60_000 === 0) return `${Math.round(ms / 60_000)}m`;
59
+ return `${Math.round(ms / 1_000)}s`;
60
+ }
61
+ return `${ms}ms`;
62
+ }
63
+
64
+ function parseDurationTokenToMs(token) {
65
+ const t = String(token || '').trim().toLowerCase();
66
+ const minutes = /^(\d+)m$/.exec(t);
67
+ if (minutes) return Number(minutes[1]) * 60_000;
68
+ const seconds = /^(\d+)s$/.exec(t);
69
+ if (seconds) return Number(seconds[1]) * 1_000;
70
+ const millis = /^(\d+)ms$/.exec(t);
71
+ if (millis) return Number(millis[1]);
72
+ return NaN;
73
+ }
74
+
75
+ function watchdogMsFromRawError(raw) {
76
+ const value = String(raw || '');
77
+ const timeout = /first response stale\s*\((\d+)ms\)/i.exec(value);
78
+ if (timeout) return { kind: 'timeout', ms: Number(timeout[1]) };
79
+ const stale = /(?:agent )?(?:task|tool running) stale\s*\((\d+)ms/i.exec(value);
80
+ if (stale) return { kind: 'stale', ms: Number(stale[1]) };
81
+ return null;
82
+ }
83
+
84
+ function compactReasonFromNormalizedTimeout(presented) {
85
+ const match = /no first response from (?:the )?[\w\s]*within (\d+m|\d+s|\d+ms)/i.exec(String(presented || ''));
86
+ if (!match) return '';
87
+ const ms = parseDurationTokenToMs(match[1]);
88
+ return Number.isFinite(ms)
89
+ ? `No first response ${compactDurationForStatus(ms)}`
90
+ : `No first response ${match[1]}`;
91
+ }
92
+
93
+ function compactReasonFromNormalizedStale(presented) {
94
+ const match = /went stale after (\d+m|\d+s|\d+ms)/i.exec(String(presented || ''));
95
+ if (!match) return '';
96
+ const ms = parseDurationTokenToMs(match[1]);
97
+ return Number.isFinite(ms)
98
+ ? `No progress ${compactDurationForStatus(ms, { preferMinutes: true })}`
99
+ : `No progress ${match[1]}`;
100
+ }
101
+
102
+ export function isBackgroundErrorOnlyBody(body, error = '') {
103
+ const trimmed = String(body ?? '').trim();
104
+ if (!trimmed) return false;
105
+ const err = String(error ?? '').trim();
106
+ const lines = trimmed.split('\n').map((line) => line.trim()).filter(Boolean);
107
+ if (lines.length !== 1) return false;
108
+ const line = lines[0];
109
+ if (/^error:\s*/i.test(line)) return true;
110
+ if (!err) return /^error:\s*/i.test(line);
111
+ const stripped = stripErrorPrefix(line);
112
+ const presented = presentErrorText(err, { max: 500 });
113
+ return line === err
114
+ || line === `Error: ${err}`
115
+ || stripped === err
116
+ || line === `Error: ${presented}`
117
+ || stripped === presented;
118
+ }
119
+
53
120
  function subjectForSurface(surface) {
54
121
  const value = String(surface || '').toLowerCase();
55
122
  if (value.includes('search') || value.includes('web')) return 'web search agent';
56
- if (value.includes('bridge') || value.includes('agent') || value.includes('task')) return 'agent';
123
+ if (value.includes('agent') || value.includes('task')) return 'agent';
57
124
  return 'tool';
58
125
  }
59
126
 
@@ -75,19 +142,26 @@ export function presentErrorText(error, options = {}) {
75
142
  text = oneLine(stripErrorPrefix(text));
76
143
  if (!text) return 'Unknown error';
77
144
 
78
- if (/\bBRIDGE_CONTEXT_OVERFLOW\b|bridge context overflow|latest turn cannot fit|context budget|context window/i.test(text)) {
145
+ if (/\bAGENT_CONTEXT_OVERFLOW\b|agent context overflow|latest turn cannot fit|context budget|context window/i.test(text)) {
79
146
  return 'Context too large.';
80
147
  }
81
148
  if (/\bcompact(?:ion)?\b.*\b(?:failed|error|overflow)\b|\b(?:failed|error)\b.*\bcompact(?:ion)?\b/i.test(text)) {
82
149
  return 'Compact failed.';
83
150
  }
84
151
 
85
- const firstResponse = /(?:bridge\s+)?first response stale\s*\((\d+)ms\)/i.exec(text);
152
+ const quotaRetry = /retryAfter=([^\s:]+)/i.exec(text);
153
+ if (/\b429\b|rate[_ -]?limit|quota|too many requests|resource exhausted|insufficient_quota|quota_exceeded/i.test(text)) {
154
+ const provider = /Anthropic OAuth/i.test(text) ? 'Anthropic' : 'Provider';
155
+ return `${provider} quota/rate limit hit${quotaRetry?.[1] ? `; retry after ${quotaRetry[1]}` : ''}.`;
156
+ }
157
+
158
+ const firstResponse = /(?:agent\s+)?first response stale\s*\((\d+)ms\)/i.exec(text);
86
159
  if (firstResponse) {
87
160
  return `No first response from the ${subject} within ${formatDurationMs(firstResponse[1])}.`;
88
161
  }
89
162
 
90
- const stale = /bridge task stale\s*\((\d+)ms[^)]*\)/i.exec(text)
163
+ const stale = /agent (?:task|tool running) stale\s*\((\d+)ms[^)]*\)/i.exec(text)
164
+ || /tool running stale\s*\((\d+)ms[^)]*\)/i.exec(text)
91
165
  || /task stale\s*\((\d+)ms[^)]*\)/i.exec(text);
92
166
  if (stale) {
93
167
  return `The ${subject} went stale after ${formatDurationMs(stale[1])} without new stream/tool progress.`;
@@ -101,6 +175,10 @@ export function presentErrorText(error, options = {}) {
101
175
  return `${subject[0].toUpperCase()}${subject.slice(1)} stopped before the response completed.`;
102
176
  }
103
177
 
178
+ if (/^Session closed:\s*(?:closeSession|closed|aborted)\b/i.test(text)) {
179
+ return `${subject[0].toUpperCase()}${subject.slice(1)} stopped.`;
180
+ }
181
+
104
182
  if (/parent signal aborted/i.test(text)) {
105
183
  return `${subject[0].toUpperCase()}${subject.slice(1)} was cancelled by its caller.`;
106
184
  }
@@ -115,6 +193,55 @@ export function presentErrorText(error, options = {}) {
115
193
  return capText(text || 'Unknown error', max);
116
194
  }
117
195
 
196
+ /**
197
+ * Compact `Failed · reason` label for background task/agent failure cards.
198
+ */
199
+ export function backgroundTaskFailureStatusLabel(status, error, options = {}) {
200
+ const surface = options.surface || options.tool || '';
201
+ const raw = String(error ?? '').trim();
202
+ const statusNorm = String(status || '').trim().toLowerCase();
203
+ const terminal = /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(statusNorm);
204
+ if (!raw && !terminal) return '';
205
+
206
+ const presented = presentErrorText(error, { surface, max: 160 });
207
+ const watchdog = watchdogMsFromRawError(raw);
208
+
209
+ let head = 'Failed';
210
+ if (/^(cancelled|canceled)$/i.test(statusNorm)) head = 'Cancelled';
211
+ else if (
212
+ /^timeout$/i.test(statusNorm)
213
+ || watchdog?.kind === 'timeout'
214
+ || /first response stale/i.test(raw)
215
+ || /no first response from/i.test(presented)
216
+ ) head = 'Timeout';
217
+ else if (
218
+ watchdog?.kind === 'stale'
219
+ || /(?:agent )?(?:task|tool running) stale|went stale without/i.test(raw)
220
+ || /went stale after/i.test(presented)
221
+ ) head = 'Stale';
222
+
223
+ let reason = '';
224
+ if (/\bcontext too large\b/i.test(presented)) {
225
+ reason = 'Context too large';
226
+ } else if (head === 'Timeout') {
227
+ if (watchdog?.kind === 'timeout') {
228
+ reason = `No first response ${compactDurationForStatus(watchdog.ms)}`;
229
+ } else {
230
+ reason = compactReasonFromNormalizedTimeout(presented) || presented.replace(/\.$/, '');
231
+ }
232
+ } else if (head === 'Stale') {
233
+ if (watchdog?.kind === 'stale') {
234
+ reason = `No progress ${compactDurationForStatus(watchdog.ms, { preferMinutes: true })}`;
235
+ } else {
236
+ reason = compactReasonFromNormalizedStale(presented) || presented.replace(/\.$/, '');
237
+ }
238
+ } else {
239
+ reason = presented.replace(/\.$/, '');
240
+ }
241
+ if (!reason) return head;
242
+ return `${head} · ${reason}`;
243
+ }
244
+
118
245
  export function errorLine(error, options = {}) {
119
246
  const text = presentErrorText(error, options);
120
247
  return /^error\s*:/i.test(text) ? text : `Error: ${text}`;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Per-call cost estimator for bridge-trace usage rows.
2
+ * Per-call cost estimator for agent-trace usage rows.
3
3
  *
4
4
  * Pricing is pulled from the LiteLLM catalog (already warmed by providers/
5
5
  * agent bootstrap). All four token slots — input / output / cacheRead /
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { getModelMetadataSync } from '../../agent/orchestrator/providers/model-catalog.mjs';
15
15
 
16
- // OpenAI / Codex / Gemini report `input_tokens` as the total prompt token
16
+ // OpenAI OAuth / OpenAI API / Gemini report `input_tokens` as the total prompt token
17
17
  // count *including* the cached portion (inclusive). Anthropic reports the
18
18
  // uncached remainder only and bills cached_read / cached_write as separate
19
19
  // additive slots (additive). Cost and prompt-total math has to branch on this.
@@ -66,7 +66,7 @@ function proxyConfigured() {
66
66
  * The shared singleton dispatcher for LLM API requests. Created on first use.
67
67
  *
68
68
  * keepAliveTimeout (~60s) keeps a socket warm across the typical idle gap
69
- * between bridge turns; connections caps concurrent sockets per origin so a
69
+ * between agent turns; connections caps concurrent sockets per origin so a
70
70
  * burst of parallel calls can't open an unbounded number of handshakes.
71
71
  *
72
72
  * Returns `undefined` when a proxy / any custom (non-default) global dispatcher
@@ -92,9 +92,19 @@ export function getLlmDispatcher() {
92
92
  return _globalInstalled ? undefined : _agent
93
93
  }
94
94
 
95
- // Origins already warmed (or warming) this process, so repeated provider
96
- // construction or multiple providers on the same host don't spam handshakes.
97
- const _preconnected = new Set()
95
+ // Origins warmed (or warming) recently, with the timestamp of the last warm.
96
+ // A time-based gate (instead of a permanent Set) lets us RE-warm a socket once
97
+ // the kept-alive window has lapsed: a one-shot warm at provider construction
98
+ // went cold after keepAliveTimeout (~60s idle between turns), so the first
99
+ // request after a pause paid the full TLS handshake again. Re-warming just
100
+ // before a turn keeps the hot-path send on a live socket.
101
+ const _preconnectedAt = new Map()
102
+ // Re-warm cadence: slightly below the keep-alive window so a socket is renewed
103
+ // before it can lapse. Capped so an explicit short keepAlive still re-warms.
104
+ function _preconnectTtlMs() {
105
+ const keepAlive = envInt('MIXDOG_LLM_KEEPALIVE_MS', 60_000)
106
+ return Math.max(5_000, keepAlive - 10_000)
107
+ }
98
108
 
99
109
  /**
100
110
  * Best-effort warm a kept-alive socket to `origin` so the first real request
@@ -113,8 +123,10 @@ export function preconnect(origin) {
113
123
  try { url = new URL(origin) } catch { return }
114
124
  if (url.protocol !== 'https:' && url.protocol !== 'http:') return
115
125
  const key = url.origin
116
- if (_preconnected.has(key)) return
117
- _preconnected.add(key)
126
+ const now = Date.now()
127
+ const last = _preconnectedAt.get(key) || 0
128
+ if (now - last < _preconnectTtlMs()) return
129
+ _preconnectedAt.set(key, now)
118
130
  // A throwaway HEAD lands a pooled socket without fetching a body. Any
119
131
  // failure (offline, DNS, 4xx/5xx) is irrelevant — the handshake is the
120
132
  // point, and the real request will surface genuine errors.
@@ -124,6 +136,10 @@ export function preconnect(origin) {
124
136
  signal: AbortSignal.timeout(10_000),
125
137
  })
126
138
  .then((res) => res.body?.dump?.())
127
- .catch(() => { /* best-effort warmup; ignore */ })
139
+ .catch(() => {
140
+ // Warm failed — clear the timestamp so the next call can retry instead
141
+ // of waiting out the full TTL on a connection that never landed.
142
+ _preconnectedAt.delete(key)
143
+ })
128
144
  } catch { /* never let warmup break construction */ }
129
145
  }
@@ -2,8 +2,8 @@
2
2
  * Shared LLM helpers (post v0.6.46).
3
3
  *
4
4
  * The legacy `callLLM` dispatcher and direct CLI/HTTP runners have been
5
- * removed — every LLM call now flows through `bridge-llm.mjs`
6
- * (`makeBridgeLlm({ taskType })`) and, for memory maintenance specifically,
5
+ * removed — every LLM call now flows through `agent-dispatch.mjs`
6
+ * (`makeAgentDispatch({ taskType })`) and, for memory maintenance specifically,
7
7
  * through `maintenance-llm.mjs`'s thin wrapper.
8
8
  *
9
9
  * Only preset resolution remains here: memory-cycle and future backend
@@ -24,18 +24,41 @@ function loadAgentConfig() {
24
24
  }
25
25
 
26
26
  /**
27
- * Resolve maintenance preset ID for a given task from agent-config.
28
- * Falls back to canonical defaults (DEFAULT_MAINTENANCE from config.mjs).
29
- * Returns null if no matching preset is registered (lets callers skip the call).
27
+ * Resolve the maintenance model ROUTE for a given task from agent-config.
28
+ *
29
+ * Maintenance slots now store a direct `{provider, model, effort?, fast?}`
30
+ * route (parity with `agents.<role>`). This returns that route object. For
31
+ * backward compatibility a slot still holding a legacy preset-NAME string is
32
+ * resolved against config.presets and returned as a route. Falls back to the
33
+ * canonical DEFAULT_MAINTENANCE route. Returns null only when nothing resolves.
34
+ *
35
+ * The returned value is passed straight through as `opts.preset` to
36
+ * makeAgentDispatch, whose resolveMaintenanceRoute()/maintenanceRouteToPreset()
37
+ * accept either a route object or a legacy name string.
30
38
  */
31
39
  export function resolveMaintenancePreset(task, agentConfig) {
32
40
  const cfg = agentConfig || loadAgentConfig()
33
41
  const maint = cfg?.maintenance || {}
34
- const presetId = maint[task] || DEFAULT_MAINTENANCE[task]
35
42
  const presets = cfg?.presets || []
36
- if (presets.some(p => p.id === presetId || p.name === presetId)) return presetId
37
- // No registered preset found return the first available preset id so callers
38
- // always receive a real id, or null if the presets list is empty.
39
- const first = presets.find(p => p?.id || p?.name)
40
- return first ? (first.id || first.name) : null
43
+ const slot = maint[task]
44
+ // Preferred: slot already holds a direct route object.
45
+ if (slot && typeof slot === 'object' && !Array.isArray(slot)
46
+ && slot.provider && slot.model) {
47
+ return { ...slot }
48
+ }
49
+ // Legacy: slot holds a preset-NAME string — resolve it to a route.
50
+ const name = String(slot || '').trim()
51
+ if (name) {
52
+ const preset = presets.find(p => p && (p.id === name || p.name === name))
53
+ if (preset && preset.provider && preset.model) {
54
+ const route = { provider: preset.provider, model: preset.model }
55
+ if (preset.effort) route.effort = preset.effort
56
+ if (preset.fast === true) route.fast = true
57
+ return route
58
+ }
59
+ }
60
+ // Canonical default route (DEFAULT_MAINTENANCE is now route-shaped).
61
+ const def = DEFAULT_MAINTENANCE[task]
62
+ if (def && def.provider && def.model) return { ...def }
63
+ return null
41
64
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Unified LLM usage logger.
3
3
  *
4
- * Phase 2: Routes via appendBridgeTrace (HTTP buffer to memory-service)
5
- * instead of writing directly to bridge-trace.jsonl.
4
+ * Phase 2: Routes via appendAgentTrace (HTTP buffer to memory-service)
5
+ * instead of writing directly to the local trace jsonl.
6
6
  *
7
7
  * Signature unchanged — callers are unaffected.
8
8
  */
9
9
 
10
- import { appendBridgeTrace } from '../../agent/orchestrator/bridge-trace.mjs';
10
+ import { appendAgentTrace } from '../../agent/orchestrator/agent-trace.mjs';
11
11
 
12
12
  /**
13
13
  * Append a usage entry to the trace store.
@@ -34,7 +34,7 @@ function warnMissingProviderOnce(key) {
34
34
  export function logLlmCall(entry, opts = {}) {
35
35
  try {
36
36
  if (!entry.provider) warnMissingProviderOnce(entry.model || '?');
37
- appendBridgeTrace({
37
+ appendAgentTrace({
38
38
  ts: entry.ts || Date.now(),
39
39
  kind: 'usage',
40
40
  ...entry,
@@ -0,0 +1,56 @@
1
+ const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
2
+
3
+ const AGENT_PERMISSION_ALIASES = new Map([
4
+ ['none', 'none'],
5
+ ['readonly', 'read'],
6
+ ['read-only', 'read'],
7
+ ['read', 'read'],
8
+ ['write', 'read-write'],
9
+ ['readwrite', 'read-write'],
10
+ ['read_write', 'read-write'],
11
+ ['read-write', 'read-write'],
12
+ ['mcp', 'mcp'],
13
+ ['full', 'full'],
14
+ ]);
15
+
16
+ function clean(value) {
17
+ return String(value ?? '').trim();
18
+ }
19
+
20
+ function unquote(value) {
21
+ return clean(value).replace(/^['"]|['"]$/g, '').trim();
22
+ }
23
+
24
+ export function parseMarkdownFrontmatter(markdown) {
25
+ const match = String(markdown || '').match(FRONTMATTER_RE);
26
+ if (!match) return {};
27
+ const meta = {};
28
+ for (const line of match[1].split(/\r?\n/)) {
29
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
30
+ if (!kv) continue;
31
+ meta[kv[1]] = unquote(kv[2]);
32
+ }
33
+ return meta;
34
+ }
35
+
36
+ export function stripMarkdownFrontmatter(markdown) {
37
+ return String(markdown || '').replace(FRONTMATTER_RE, '').trim();
38
+ }
39
+
40
+ export function readMarkdownDocument(markdown) {
41
+ return {
42
+ frontmatter: parseMarkdownFrontmatter(markdown),
43
+ body: stripMarkdownFrontmatter(markdown),
44
+ };
45
+ }
46
+
47
+ export function normalizeAgentPermission(value) {
48
+ const key = clean(value).toLowerCase();
49
+ return AGENT_PERMISSION_ALIASES.get(key) || null;
50
+ }
51
+
52
+ export function normalizeAgentPermissionOrNone(value) {
53
+ const raw = clean(value);
54
+ if (!raw) return null;
55
+ return normalizeAgentPermission(raw) || 'none';
56
+ }
@@ -0,0 +1,104 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+
4
+ export function isPidAlive(pid) {
5
+ const n = Number(pid);
6
+ if (!Number.isInteger(n) || n <= 0) return false;
7
+ try {
8
+ process.kill(n, 0);
9
+ return true;
10
+ } catch (error) {
11
+ return error?.code === 'EPERM';
12
+ }
13
+ }
14
+
15
+ function readJson(path) {
16
+ try {
17
+ if (!existsSync(path)) return null;
18
+ return JSON.parse(readFileSync(path, 'utf8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function writeJsonAtomic(path, value) {
25
+ mkdirSync(dirname(path), { recursive: true });
26
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
27
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
28
+ renameSync(tmp, path);
29
+ }
30
+
31
+ function waitSync(ms) {
32
+ try {
33
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
34
+ } catch {}
35
+ }
36
+
37
+ function acquireClaimLock(path) {
38
+ const lockPath = `${path}.claim.lock`;
39
+ for (let i = 0; i < 20; i++) {
40
+ try {
41
+ mkdirSync(lockPath, { recursive: false });
42
+ return lockPath;
43
+ } catch (error) {
44
+ if (error?.code !== 'EEXIST') throw error;
45
+ try {
46
+ const ageMs = Date.now() - statSync(lockPath).mtimeMs;
47
+ if (ageMs > 5000) {
48
+ rmSync(lockPath, { recursive: true, force: true });
49
+ continue;
50
+ }
51
+ } catch {}
52
+ waitSync(25);
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export function readSingletonOwner(path) {
59
+ const owner = readJson(path);
60
+ if (!owner || typeof owner !== 'object') return { owner: null, alive: false };
61
+ return { owner, alive: isPidAlive(owner.pid) };
62
+ }
63
+
64
+ export function claimSingletonOwner(path, {
65
+ kind = 'runtime',
66
+ pid = process.pid,
67
+ meta = {},
68
+ } = {}) {
69
+ const owner = {
70
+ kind,
71
+ pid,
72
+ claimedAt: new Date().toISOString(),
73
+ ...meta,
74
+ };
75
+
76
+ const current = readJson(path);
77
+ if (current?.pid && Number(current.pid) !== Number(pid) && isPidAlive(current.pid)) {
78
+ return { owned: false, owner: current };
79
+ }
80
+
81
+ const lockPath = acquireClaimLock(path);
82
+ if (!lockPath) return { owned: false, owner: readJson(path) };
83
+ try {
84
+ const lockedCurrent = readJson(path);
85
+ if (lockedCurrent?.pid && Number(lockedCurrent.pid) !== Number(pid) && isPidAlive(lockedCurrent.pid)) {
86
+ return { owned: false, owner: lockedCurrent };
87
+ }
88
+ writeJsonAtomic(path, owner);
89
+ return { owned: true, owner };
90
+ } finally {
91
+ rmSync(lockPath, { recursive: true, force: true });
92
+ }
93
+ }
94
+
95
+ export function releaseSingletonOwner(path, pid = process.pid) {
96
+ const current = readJson(path);
97
+ if (!current?.pid || Number(current.pid) !== Number(pid)) return false;
98
+ try {
99
+ rmSync(path, { force: true });
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }