mixdog 0.8.1 → 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 (280) 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 +205 -33
  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 +1222 -90
  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 +2212 -446
  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 +110 -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 +76 -28
  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/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,11 +1,11 @@
1
1
  export const TOOL_SYNC_EXECUTION_CONTRACT =
2
- 'Runs synchronously and returns the bounded result in this tool call.';
2
+ 'Runs synchronously in this tool call.';
3
3
 
4
4
  export const TOOL_ASYNC_EXECUTION_CONTRACT =
5
- 'Async execution returns immediately; completion is delivered as an owner-session notification. Status/read/wait tools are for manual recovery or explicit blocking control only.';
5
+ 'Async returns task_id; completion notification follows. status/read/wait are manual recovery or blocking only.';
6
6
 
7
7
  export const TOOL_MANUAL_CONTROL_CONTRACT =
8
- 'Use manual wait/peek/read/status only when you need explicit blocking, recovery, inspection, or cancellation.';
8
+ 'Manual wait/read/status/cancel only for explicit blocking or recovery.';
9
9
 
10
10
  function clean(value) {
11
11
  return String(value ?? '').trim();
@@ -16,13 +16,111 @@ function positivePid(value) {
16
16
  return Number.isFinite(n) && n > 0 ? n : null;
17
17
  }
18
18
 
19
+ const NON_PERSISTENT_TOOL_STATUSES = new Set(['running', 'pending', 'queued']);
20
+ const TERMINAL_TOOL_STATUSES = new Set([
21
+ 'completed',
22
+ 'failed',
23
+ 'cancelled',
24
+ 'canceled',
25
+ 'error',
26
+ 'timeout',
27
+ 'done',
28
+ 'success',
29
+ ]);
30
+
31
+ function notificationResultBody(text) {
32
+ const match = /\n\s*\n([\s\S]*)$/.exec(String(text || ''));
33
+ return match ? String(match[1] || '').trim() : '';
34
+ }
35
+
36
+ function backgroundTaskHeaderStatus(text) {
37
+ const match = /^status:\s*(\S+)/mi.exec(String(text || ''));
38
+ return clean(match?.[1]).toLowerCase();
39
+ }
40
+
41
+ function notificationHead(text) {
42
+ const value = String(text || '').trim();
43
+ const match = /\n\s*\n/.exec(value);
44
+ if (!match) return value;
45
+ return value.slice(0, match.index).trim();
46
+ }
47
+
48
+ function isInternalTaskNotificationEnvelope(text) {
49
+ const value = String(text || '').trim();
50
+ if (!value) return false;
51
+ if (/^background task\b/i.test(value)) return false;
52
+ if (/^<task-notification\b/i.test(value)) return true;
53
+ const head = notificationHead(value);
54
+ return /^<task-notification\b/i.test(head);
55
+ }
56
+
57
+ export function shouldPersistModelVisibleToolCompletion(text, meta = {}) {
58
+ const message = String(text || '').trim();
59
+ if (!message) return false;
60
+ if (isInternalTaskNotificationEnvelope(message)) return false;
61
+
62
+ const metaStatus = clean(meta?.status).toLowerCase();
63
+ if (NON_PERSISTENT_TOOL_STATUSES.has(metaStatus)) return false;
64
+
65
+ if (/^background task\b/i.test(message)) {
66
+ const headerStatus = backgroundTaskHeaderStatus(message) || metaStatus;
67
+ if (NON_PERSISTENT_TOOL_STATUSES.has(headerStatus)) return false;
68
+ if (!TERMINAL_TOOL_STATUSES.has(headerStatus) && !TERMINAL_TOOL_STATUSES.has(metaStatus)) return false;
69
+ return Boolean(notificationResultBody(message));
70
+ }
71
+
72
+ if (meta?.execution_id || meta?.execution_surface) {
73
+ if (NON_PERSISTENT_TOOL_STATUSES.has(metaStatus)) return false;
74
+ if (!TERMINAL_TOOL_STATUSES.has(metaStatus)) return false;
75
+ return Boolean(notificationResultBody(message));
76
+ }
77
+
78
+ if (/^(?:agent task:|task_id:)/mi.test(message)) {
79
+ if (NON_PERSISTENT_TOOL_STATUSES.has(metaStatus)) return false;
80
+ if (!TERMINAL_TOOL_STATUSES.has(metaStatus)) return false;
81
+ return Boolean(notificationResultBody(message));
82
+ }
83
+
84
+ return false;
85
+ }
86
+
87
+ const BRACKETED_SHELL_STATUS_RE = /^\[status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled|error|timeout|done|success)\]/im;
88
+
89
+ function isBracketedShellNotificationEnvelope(text) {
90
+ const value = String(text ?? '').trim();
91
+ if (!value) return false;
92
+ if (!/^\[task_id:\s*\S+\]/im.test(value)) return false;
93
+ return BRACKETED_SHELL_STATUS_RE.test(value);
94
+ }
95
+
96
+ export function isInternalRuntimeNotificationText(text) {
97
+ const value = String(text ?? '').trim();
98
+ if (!value) return false;
99
+ if (isInternalTaskNotificationEnvelope(value)) return true;
100
+ if (isBracketedShellNotificationEnvelope(value)) return true;
101
+ if (/^background task\b/i.test(value)
102
+ && /^task_id:\s*\S+/mi.test(value)
103
+ && /^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)\b/mi.test(value)) {
104
+ return true;
105
+ }
106
+ if (/^task_id:\s*\S+/mi.test(value)
107
+ && /^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)\b/mi.test(value)
108
+ && /^(?:surface|operation|type|target|role|agent|preset|model|effort|fast|notification):\s*/mi.test(value)) {
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+
19
114
  export function normalizeToolNotifyContext(context = {}) {
20
- const callerSessionId = clean(context.callerSessionId || context.routingSessionId || context.sessionId);
115
+ const explicitCallerSessionId = clean(context.callerSessionId || context.sessionId);
116
+ const explicitRoutingSessionId = clean(context.routingSessionId);
117
+ const callerSessionId = explicitCallerSessionId || explicitRoutingSessionId;
118
+ const routingSessionId = explicitRoutingSessionId || callerSessionId;
21
119
  const clientHostPid = positivePid(context.clientHostPid);
22
120
  return {
23
121
  notifyFn: typeof context.notifyFn === 'function' ? context.notifyFn : null,
24
122
  callerSessionId: callerSessionId || null,
25
- routingSessionId: callerSessionId || null,
123
+ routingSessionId: routingSessionId || null,
26
124
  clientHostPid,
27
125
  };
28
126
  }
@@ -30,8 +128,8 @@ export function normalizeToolNotifyContext(context = {}) {
30
128
  export function toolCompletionInstruction({ surface = 'tool', id, status, detail } = {}) {
31
129
  const label = surface === 'shell'
32
130
  ? 'shell task'
33
- : surface === 'bridge'
34
- ? 'bridge agent'
131
+ : surface === 'agent'
132
+ ? 'agent task'
35
133
  : `${surface} execution`;
36
134
  const statusText = status ? ` (${status}${detail ? `, ${detail}` : ''})` : '';
37
135
  return `The async ${label} ${id || ''} has finished${statusText} - review this result in your next step.`;
@@ -53,10 +151,73 @@ export function toolCompletionMeta({
53
151
  status: status || null,
54
152
  instruction: instruction || toolCompletionInstruction({ surface, id, status }),
55
153
  ...(ctx.callerSessionId ? { caller_session_id: ctx.callerSessionId } : {}),
154
+ ...(ctx.routingSessionId && ctx.routingSessionId !== ctx.callerSessionId ? { routing_session_id: ctx.routingSessionId } : {}),
56
155
  ...(ctx.clientHostPid ? { client_host_pid: String(ctx.clientHostPid) } : {}),
57
156
  };
58
157
  }
59
158
 
159
+ const MODEL_VISIBLE_COMPLETION_INSTRUCTION_RE = /\b(async (?:agent task|shell task|\w+ execution)|Async \S+)/i;
160
+ const MODEL_VISIBLE_COMPLETION_REVIEW_RE = /has finished\b[\s\S]*review this result in your next step/i;
161
+ const MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE = /^Async .+ finished\./i;
162
+
163
+ export function isModelVisibleToolCompletionWrapper(text) {
164
+ const value = String(text ?? '').trim();
165
+ if (!value) return false;
166
+ const resultSplit = /\n\nResult:\n/.exec(value);
167
+ if (!resultSplit) return false;
168
+ const preamble = value.slice(0, resultSplit.index).trim();
169
+ if (!preamble) return false;
170
+ const instructionLike = MODEL_VISIBLE_COMPLETION_INSTRUCTION_RE.test(preamble)
171
+ || MODEL_VISIBLE_COMPLETION_REVIEW_RE.test(preamble)
172
+ || MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
173
+ if (!instructionLike) return false;
174
+ const quotedSection = value.slice(resultSplit.index + resultSplit[0].length);
175
+ const quotedLines = quotedSection.split(/\r?\n/).filter((line) => line.length > 0);
176
+ if (quotedLines.length === 0) return false;
177
+ if (!quotedLines.every((line) => /^> /.test(line))) return false;
178
+ const unquoted = quotedLines.map((line) => line.slice(2)).join('\n');
179
+ return isInternalRuntimeNotificationText(unquoted);
180
+ }
181
+
182
+ export function modelVisibleToolCompletionMessage(text, meta = {}) {
183
+ const message = String(text || '').trim();
184
+ if (!message) return '';
185
+ if (!shouldPersistModelVisibleToolCompletion(message, meta)) return '';
186
+ const instruction = clean(meta?.instruction);
187
+ const type = clean(meta?.type || meta?.execution_surface || 'tool_completion');
188
+ const id = clean(meta?.execution_id);
189
+ const status = clean(meta?.status);
190
+ const header = `Async ${type}${id ? ` ${id}` : ''}${status ? ` ${status}` : ''} finished.`;
191
+ const MODEL_VISIBLE_RESULT_BODY_MAX = 12_000;
192
+ const bounded = message.length > MODEL_VISIBLE_RESULT_BODY_MAX
193
+ ? `${message.slice(0, MODEL_VISIBLE_RESULT_BODY_MAX)}\n\n[result truncated for model context]`
194
+ : message;
195
+ const quoted = bounded.split(/\r?\n/).map((line) => `> ${line}`).join('\n');
196
+ return [
197
+ instruction || header,
198
+ '',
199
+ 'Result:',
200
+ quoted,
201
+ ].join('\n');
202
+ }
203
+
204
+ // Shared enqueue-fallback helper used by both the synchronous fallback path and
205
+ // the asynchronous notifyFn reject/false-resolve rescue path. Only enqueues when
206
+ // a caller session and fallback fn are present. Returns true only on a non-false,
207
+ // non-zero fallback result; logs and returns false on throw.
208
+ function tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id) {
209
+ if (!ctx.callerSessionId || typeof enqueueFallback !== 'function') return false;
210
+ try {
211
+ const enq = enqueueFallback(ctx.callerSessionId, message, meta);
212
+ return enq !== false && enq !== 0;
213
+ } catch (err) {
214
+ try {
215
+ process.stderr.write(`[${logPrefix}] async completion fallback enqueue failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
216
+ } catch {}
217
+ }
218
+ return false;
219
+ }
220
+
60
221
  export function notifyToolCompletion({
61
222
  surface = 'tool',
62
223
  id,
@@ -80,25 +241,43 @@ export function notifyToolCompletion({
80
241
  context: ctx,
81
242
  });
82
243
 
244
+ // Try the upstream owner notifyFn first. A `false` return means the owner
245
+ // *declined* delivery and a throw means it failed outright — in both cases we
246
+ // do NOT return early but fall through to the enqueueFallback path so the
247
+ // completion can still reach the caller session. Only a successful (non-false)
248
+ // notifyFn short-circuits as delivered.
83
249
  if (typeof ctx.notifyFn === 'function') {
84
- Promise.resolve(ctx.notifyFn(message, meta)).catch((err) => {
85
- try {
86
- process.stderr.write(`[${logPrefix}] async completion notify failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
87
- } catch {}
88
- });
89
- return true;
90
- }
91
-
92
- if (ctx.callerSessionId && typeof enqueueFallback === 'function') {
93
250
  try {
94
- enqueueFallback(ctx.callerSessionId, message, meta);
95
- return true;
251
+ const notifyResult = ctx.notifyFn(message, meta);
252
+ if (notifyResult !== false) {
253
+ // Optimistically report delivered (keeps this fn synchronous). But a
254
+ // notifyFn that returns a Promise can still reject — or resolve to an
255
+ // explicit false/0 (declined/failed) — *after* we returned. In that
256
+ // case rescue the completion via enqueueFallback so the owner isn't
257
+ // left without a notification. The normal success path (truthy
258
+ // resolve) never enqueues, preserving exact-once delivery.
259
+ Promise.resolve(notifyResult).then((settled) => {
260
+ if (settled === false || settled === 0) {
261
+ tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id);
262
+ }
263
+ }).catch((err) => {
264
+ try {
265
+ process.stderr.write(`[${logPrefix}] async completion notify failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
266
+ } catch {}
267
+ tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id);
268
+ });
269
+ return true;
270
+ }
96
271
  } catch (err) {
97
272
  try {
98
- process.stderr.write(`[${logPrefix}] async completion fallback enqueue failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
273
+ process.stderr.write(`[${logPrefix}] async completion notify failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
99
274
  } catch {}
100
275
  }
101
276
  }
102
277
 
103
- return false;
278
+ // Fallback enqueue (used when notifyFn is absent, declined, or threw). Respect
279
+ // the fallback's own success signal: an explicit false/0 return means the
280
+ // enqueue failed, so report failure and leave room for a retry rather than
281
+ // marking the task notified.
282
+ return tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id);
104
283
  }
@@ -0,0 +1,183 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ isInternalRuntimeNotificationText,
5
+ isModelVisibleToolCompletionWrapper,
6
+ modelVisibleToolCompletionMessage,
7
+ shouldPersistModelVisibleToolCompletion,
8
+ toolCompletionMeta,
9
+ toolCompletionInstruction,
10
+ } from './tool-execution-contract.mjs';
11
+ import { _isInternalRuntimeNotificationText } from '../agent/orchestrator/session/manager.mjs';
12
+
13
+ const completedBackgroundTask = [
14
+ 'background task',
15
+ 'task_id: task_agent_abc123',
16
+ 'surface: agent',
17
+ 'operation: run',
18
+ 'status: completed',
19
+ 'started: 2026-03-28T00:00:00.000Z',
20
+ 'finished: 2026-03-28T00:00:05.000Z',
21
+ '',
22
+ 'worker finished successfully',
23
+ ].join('\n');
24
+
25
+ const runningBackgroundTask = [
26
+ 'background task',
27
+ 'task_id: task_agent_abc123',
28
+ 'surface: agent',
29
+ 'operation: run',
30
+ 'status: running',
31
+ 'started: 2026-03-28T00:00:00.000Z',
32
+ 'notification: completion will be delivered to the owner session; use status/read only for manual recovery.',
33
+ ].join('\n');
34
+
35
+ test('terminal background task with body should persist as model-visible wrapper', () => {
36
+ const meta = toolCompletionMeta({
37
+ surface: 'agent',
38
+ id: 'task_agent_abc123',
39
+ status: 'completed',
40
+ context: { callerSessionId: 'sess_owner' },
41
+ });
42
+ assert.equal(shouldPersistModelVisibleToolCompletion(completedBackgroundTask, meta), true);
43
+ const visible = modelVisibleToolCompletionMessage(completedBackgroundTask, meta);
44
+ assert.ok(visible);
45
+ assert.equal(isInternalRuntimeNotificationText(visible), false);
46
+ assert.equal(_isInternalRuntimeNotificationText(visible), false);
47
+ assert.equal(isModelVisibleToolCompletionWrapper(visible), true);
48
+ assert.match(visible, /worker finished successfully/);
49
+ });
50
+
51
+ test('model-visible completion wrapper is not a human transcript item', () => {
52
+ const meta = toolCompletionMeta({
53
+ surface: 'agent',
54
+ id: 'task_agent_abc123',
55
+ status: 'completed',
56
+ instruction: toolCompletionInstruction({
57
+ surface: 'agent',
58
+ id: 'task_agent_abc123',
59
+ status: 'completed',
60
+ }),
61
+ context: { callerSessionId: 'sess_owner' },
62
+ });
63
+ const visible = modelVisibleToolCompletionMessage(completedBackgroundTask, meta);
64
+ assert.ok(visible);
65
+ assert.match(visible, /^The async agent task/);
66
+ assert.match(visible, /\n\nResult:\n/);
67
+ assert.equal(isModelVisibleToolCompletionWrapper(visible), true);
68
+ assert.equal(isModelVisibleToolCompletionWrapper('please fix the leak in engine.mjs'), false);
69
+ assert.equal(isModelVisibleToolCompletionWrapper(completedBackgroundTask), false);
70
+ });
71
+
72
+ test('running background task stays hidden from model-visible pending drain', () => {
73
+ const meta = toolCompletionMeta({
74
+ surface: 'agent',
75
+ id: 'task_agent_abc123',
76
+ status: 'running',
77
+ context: { callerSessionId: 'sess_owner' },
78
+ });
79
+ assert.equal(shouldPersistModelVisibleToolCompletion(runningBackgroundTask, meta), false);
80
+ assert.equal(modelVisibleToolCompletionMessage(runningBackgroundTask, meta), '');
81
+ });
82
+
83
+ test('terminal background task persists when result body mentions task-notification', () => {
84
+ const taskWithTagInBody = [
85
+ ...completedBackgroundTask.split('\n').slice(0, -1),
86
+ 'log line: saw <task-notification status="completed"> in worker output',
87
+ ].join('\n');
88
+ const meta = toolCompletionMeta({
89
+ surface: 'agent',
90
+ id: 'task_agent_abc123',
91
+ status: 'completed',
92
+ context: { callerSessionId: 'sess_owner' },
93
+ });
94
+ assert.equal(shouldPersistModelVisibleToolCompletion(taskWithTagInBody, meta), true);
95
+ const visible = modelVisibleToolCompletionMessage(taskWithTagInBody, meta);
96
+ assert.ok(visible);
97
+ assert.match(visible, /<task-notification/);
98
+ assert.equal(isInternalRuntimeNotificationText(visible), false);
99
+ assert.equal(_isInternalRuntimeNotificationText(visible), false);
100
+ });
101
+
102
+ test('internal task-notification envelope does not persist', () => {
103
+ const internalEnvelope = [
104
+ '<task-notification>',
105
+ '<status>completed</status>',
106
+ '<summary>Agent completed</summary>',
107
+ '<task-id>task_agent_internal</task-id>',
108
+ '</task-notification>',
109
+ ].join('\n');
110
+ const meta = toolCompletionMeta({
111
+ surface: 'agent',
112
+ id: 'task_agent_internal',
113
+ status: 'completed',
114
+ context: { callerSessionId: 'sess_owner' },
115
+ });
116
+ assert.equal(shouldPersistModelVisibleToolCompletion(internalEnvelope, meta), false);
117
+ assert.equal(modelVisibleToolCompletionMessage(internalEnvelope, meta), '');
118
+ assert.equal(isInternalRuntimeNotificationText(internalEnvelope), true);
119
+ assert.equal(_isInternalRuntimeNotificationText(internalEnvelope), true);
120
+ });
121
+
122
+ test('raw running background task remains internal for manager pending drain', () => {
123
+ assert.equal(isInternalRuntimeNotificationText(runningBackgroundTask), true);
124
+ assert.equal(_isInternalRuntimeNotificationText(runningBackgroundTask), true);
125
+ });
126
+
127
+
128
+
129
+ const bracketedShellCompleted = [
130
+ "[task_id: shell_job_1]",
131
+ "[status: completed]",
132
+ "[exit: 0]",
133
+ "",
134
+ "done output",
135
+ ].join("\n");
136
+
137
+ const bracketedShellRunning = [
138
+ "[task_id: shell_job_1]",
139
+ "[status: running]",
140
+ "[command: npm test]",
141
+ ].join("\n");
142
+
143
+ test("bracketed shell completed raw envelope is internal", () => {
144
+ assert.equal(isInternalRuntimeNotificationText(bracketedShellCompleted), true);
145
+ assert.equal(_isInternalRuntimeNotificationText(bracketedShellCompleted), true);
146
+ });
147
+
148
+ test("bracketed shell running raw envelope is internal", () => {
149
+ assert.equal(isInternalRuntimeNotificationText(bracketedShellRunning), true);
150
+ assert.equal(_isInternalRuntimeNotificationText(bracketedShellRunning), true);
151
+ });
152
+
153
+ test("wrapped shell completion stays model-visible and not internal", () => {
154
+ const meta = toolCompletionMeta({
155
+ surface: "shell",
156
+ id: "shell_job_1",
157
+ status: "completed",
158
+ context: { callerSessionId: "sess_owner" },
159
+ });
160
+ const visible = modelVisibleToolCompletionMessage(bracketedShellCompleted, meta);
161
+ assert.ok(visible);
162
+ assert.match(visible, /done output/);
163
+ assert.equal(isInternalRuntimeNotificationText(visible), false);
164
+ assert.equal(_isInternalRuntimeNotificationText(visible), false);
165
+ });
166
+
167
+ test("model-visible wrapper bounds large completion bodies", () => {
168
+ const hugeBody = "x".repeat(20_000);
169
+ const hugeTask = [
170
+ ...completedBackgroundTask.split("\n").slice(0, -1),
171
+ hugeBody,
172
+ ].join("\n");
173
+ const meta = toolCompletionMeta({
174
+ surface: "agent",
175
+ id: "task_agent_abc123",
176
+ status: "completed",
177
+ context: { callerSessionId: "sess_owner" },
178
+ });
179
+ const visible = modelVisibleToolCompletionMessage(hugeTask, meta);
180
+ assert.ok(visible);
181
+ assert.match(visible, /\[result truncated for model context\]/);
182
+ assert.ok(visible.length < hugeBody.length);
183
+ });