negotium 0.1.7 → 0.1.8

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 (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,737 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import type {
4
+ HookInput,
5
+ Options,
6
+ PostToolUseHookInput,
7
+ PreToolUseHookInput,
8
+ SDKAssistantMessage,
9
+ SDKPartialAssistantMessage,
10
+ SDKResultMessage,
11
+ SDKSystemMessage,
12
+ SDKTaskStartedMessage,
13
+ SDKToolProgressMessage,
14
+ SDKToolUseSummaryMessage,
15
+ SDKUserMessage,
16
+ SDKUserMessageReplay,
17
+ } from "@anthropic-ai/claude-agent-sdk";
18
+ import { query } from "@anthropic-ai/claude-agent-sdk";
19
+ import type {
20
+ ContentBlockParam,
21
+ ImageBlockParam,
22
+ } from "@anthropic-ai/sdk/resources/messages/messages.mjs";
23
+ import { claudeRegistry } from "#agents/claude-registry";
24
+ import { deepMapStrings } from "#agents/deep-map";
25
+ import {
26
+ hostedClaudeCodeExecutablePath,
27
+ hostedMcpServers,
28
+ redactHostedSecrets,
29
+ referencesHostedSecretStorage,
30
+ shouldRedirectHostedVaultTool,
31
+ } from "#agents/execution-host";
32
+ import { VAULT_BROKER_REDIRECT_ERROR } from "#agents/vault-tool-policy";
33
+ import { extractFileEvents } from "#media/file-events";
34
+ import { errMsg } from "#platform/error";
35
+ import { logger } from "#platform/logger";
36
+ import type { AgentInputAttachment, AgentQueryOptions, EffortLevel, UnifiedEvent } from "#types";
37
+
38
+ const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
39
+ "AskUserQuestion",
40
+ "Workflow",
41
+ "TodoWrite",
42
+ "TaskCreate",
43
+ "TaskUpdate",
44
+ "TaskList",
45
+ "TaskGet",
46
+ "Task",
47
+ "Agent",
48
+ "TaskOutput",
49
+ "TaskStop",
50
+ ] as const;
51
+
52
+ export function buildClaudeDisallowedTools(
53
+ extra: readonly string[] | undefined = undefined,
54
+ ): string[] {
55
+ return [...new Set([...CLAUDE_DEFAULT_DISALLOWED_TOOLS, ...(extra ?? [])])];
56
+ }
57
+
58
+ /**
59
+ * Narrow our shared `EffortLevel` to the subset claude-agent-sdk accepts.
60
+ * Claude SDK rejects "minimal" (codex-only); we drop it silently rather than
61
+ * coercing, matching our policy for unrecognized model strings.
62
+ */
63
+ type ClaudeEffort = Exclude<EffortLevel, "minimal">;
64
+ function toClaudeEffort(e: EffortLevel | undefined): ClaudeEffort | undefined {
65
+ if (!e || !claudeRegistry.validateEffort(e)) return undefined;
66
+ return e as ClaudeEffort;
67
+ }
68
+
69
+ type ClaudeImageMimeType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
70
+
71
+ const CLAUDE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
72
+ const CLAUDE_IMAGE_MIME_TYPES = new Set<string>([
73
+ "image/jpeg",
74
+ "image/png",
75
+ "image/gif",
76
+ "image/webp",
77
+ ]);
78
+
79
+ function isClaudeImageAttachment(
80
+ attachment: AgentInputAttachment,
81
+ ): attachment is AgentInputAttachment & { mimeType: ClaudeImageMimeType } {
82
+ return attachment.type === "image" && CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType);
83
+ }
84
+
85
+ async function* singleUserMessage(message: SDKUserMessage): AsyncGenerator<SDKUserMessage> {
86
+ yield message;
87
+ }
88
+
89
+ function buildClaudePrompt(opts: AgentQueryOptions): string | AsyncIterable<SDKUserMessage> {
90
+ const attachments = opts.attachments ?? [];
91
+ if (attachments.length === 0) return opts.prompt;
92
+
93
+ const imageBlocks: ImageBlockParam[] = [];
94
+ for (const attachment of attachments) {
95
+ if (!isClaudeImageAttachment(attachment)) continue;
96
+ if (attachment.sizeBytes > CLAUDE_IMAGE_MAX_BYTES) {
97
+ logger.info(
98
+ {
99
+ path: attachment.path,
100
+ mimeType: attachment.mimeType,
101
+ sizeBytes: attachment.sizeBytes,
102
+ maxBytes: CLAUDE_IMAGE_MAX_BYTES,
103
+ },
104
+ "claudeProvider: skipping oversized image attachment content block",
105
+ );
106
+ continue;
107
+ }
108
+
109
+ try {
110
+ imageBlocks.push({
111
+ type: "image",
112
+ source: {
113
+ type: "base64",
114
+ media_type: attachment.mimeType,
115
+ data: readFileSync(attachment.path).toString("base64"),
116
+ },
117
+ });
118
+ } catch (err) {
119
+ logger.warn(
120
+ {
121
+ path: attachment.path,
122
+ mimeType: attachment.mimeType,
123
+ err,
124
+ },
125
+ "claudeProvider: failed to read image attachment for content block",
126
+ );
127
+ }
128
+ }
129
+
130
+ if (imageBlocks.length === 0) return opts.prompt;
131
+
132
+ const content: ContentBlockParam[] = [{ type: "text", text: opts.prompt }, ...imageBlocks];
133
+ return singleUserMessage({
134
+ type: "user",
135
+ message: {
136
+ role: "user",
137
+ content,
138
+ },
139
+ parent_tool_use_id: null,
140
+ });
141
+ }
142
+
143
+ // --- Stream event sub-types (from @anthropic-ai/sdk, not directly importable) ---
144
+ interface ContentBlockDelta {
145
+ type: "content_block_delta";
146
+ delta: { type: string; partial_json?: string; text?: string };
147
+ }
148
+ type StreamEvent = ContentBlockDelta | { type: string };
149
+
150
+ // --- Assistant message content block types (from BetaMessage.content) ---
151
+ interface TextBlock {
152
+ type: "text";
153
+ text: string;
154
+ }
155
+ interface ToolUseBlock {
156
+ type: "tool_use";
157
+ id: string;
158
+ name: string;
159
+ input: Record<string, unknown>;
160
+ }
161
+ interface ToolResultBlock {
162
+ type: "tool_result";
163
+ tool_use_id: string;
164
+ content: string | unknown;
165
+ }
166
+ type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | { type: string };
167
+
168
+ // --- Process tree kill on abort ---
169
+ //
170
+ // The Claude Agent SDK spawns a `claude` CLI subprocess, which in turn spawns
171
+ // MCP servers (playwright-mcp, python OCR servers, …) which themselves spawn
172
+ // browsers / interpreters. The SDK's default abort path only signals the
173
+ // direct child, leaving grandchildren orphaned — over time they accumulate as
174
+ // zombies, holding ports (Playwright DevTools 9222) and leaking memory
175
+ // (Chrome ~hundreds of MB per orphan). Single-user Mac deployments notice
176
+ // this within a day of repeated /abort.
177
+ //
178
+ // We override `spawnClaudeCodeProcess` to:
179
+ // 1. spawn with `detached: true` → child becomes the leader of a new
180
+ // process group with the same pgid as its pid;
181
+ // 2. on abort, send the signal to `-pid` (the whole group) instead of the
182
+ // single pid, so all transitive descendants receive it;
183
+ // 3. escalate to SIGKILL after a 2.5s grace if SIGTERM didn't take.
184
+
185
+ type SpawnClaudeCodeProcess = NonNullable<Options["spawnClaudeCodeProcess"]>;
186
+ type SpawnClaudeCodeOptions = Parameters<SpawnClaudeCodeProcess>[0];
187
+ type SpawnClaudeCodeResult = ReturnType<SpawnClaudeCodeProcess>;
188
+
189
+ const CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
190
+
191
+ function signalProcessTree(pid: number, signal: NodeJS.Signals): boolean {
192
+ // Negative pid = "signal every process in this process group". Falls back
193
+ // to single-pid signaling if the group call fails (e.g. on platforms where
194
+ // `detached` didn't materialize a new group).
195
+ try {
196
+ process.kill(-pid, signal);
197
+ return true;
198
+ } catch (groupErr) {
199
+ try {
200
+ process.kill(pid, signal);
201
+ return true;
202
+ } catch (pidErr) {
203
+ logger.debug({ groupErr, pidErr, pid, signal }, "Failed to signal Claude process tree");
204
+ return false;
205
+ }
206
+ }
207
+ }
208
+
209
+ export function spawnClaudeCodeProcessWithTreeKill(
210
+ options: SpawnClaudeCodeOptions,
211
+ ): SpawnClaudeCodeResult {
212
+ const child = spawn(options.command, options.args, {
213
+ cwd: options.cwd,
214
+ detached: true,
215
+ env: options.env as NodeJS.ProcessEnv,
216
+ stdio: ["pipe", "pipe", "inherit"],
217
+ });
218
+ if (!child.stdin || !child.stdout) {
219
+ child.kill("SIGKILL");
220
+ throw new Error("Failed to spawn Claude Code process with piped stdin/stdout");
221
+ }
222
+
223
+ let exited = false;
224
+ let killTimer: NodeJS.Timeout | undefined;
225
+ const clearKillTimer = () => {
226
+ if (killTimer) clearTimeout(killTimer);
227
+ killTimer = undefined;
228
+ };
229
+
230
+ const kill = (signal: NodeJS.Signals): boolean => {
231
+ if (!child.pid) return child.kill(signal);
232
+ const ok = signalProcessTree(child.pid, signal);
233
+ if (signal !== "SIGKILL") {
234
+ // Soft signal: schedule a SIGKILL fallback so a stuck process tree
235
+ // can't survive abort indefinitely.
236
+ clearKillTimer();
237
+ killTimer = setTimeout(() => {
238
+ if (!exited && child.pid) {
239
+ logger.warn({ pid: child.pid }, "Claude process tree still alive after SIGTERM; SIGKILL");
240
+ signalProcessTree(child.pid, "SIGKILL");
241
+ }
242
+ }, CLAUDE_ABORT_SIGKILL_DELAY_MS);
243
+ killTimer.unref?.();
244
+ }
245
+ return ok;
246
+ };
247
+
248
+ const onAbort = () => {
249
+ logger.warn({ pid: child.pid, command: options.command }, "Aborting Claude Code process tree");
250
+ kill("SIGTERM");
251
+ };
252
+
253
+ if (options.signal.aborted) onAbort();
254
+ else options.signal.addEventListener("abort", onAbort, { once: true });
255
+
256
+ child.once("exit", (code, signal) => {
257
+ exited = true;
258
+ clearKillTimer();
259
+ options.signal.removeEventListener("abort", onAbort);
260
+ logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
261
+ });
262
+
263
+ // Consume the child's 'error' event. Without a listener, a spawn-level error
264
+ // propagates into the SDK's readline-over-stdout teardown and crashes the
265
+ // whole process with "this.input.pause is not a function" (node:readline
266
+ // close→pause on a stream that never initialized). Handling it here keeps the
267
+ // failure local and surfaces a clean diagnostic.
268
+ child.once("error", (err) => {
269
+ exited = true;
270
+ clearKillTimer();
271
+ options.signal.removeEventListener("abort", onAbort);
272
+ logger.error(
273
+ {
274
+ pid: child.pid,
275
+ command: options.command,
276
+ err: err instanceof Error ? err.message : String(err),
277
+ },
278
+ "Claude Code process error event",
279
+ );
280
+ });
281
+
282
+ return {
283
+ stdin: child.stdin,
284
+ stdout: child.stdout,
285
+ get killed() {
286
+ return child.killed || exited;
287
+ },
288
+ get exitCode() {
289
+ return child.exitCode;
290
+ },
291
+ kill,
292
+ on(event, listener) {
293
+ child.on(event, listener);
294
+ },
295
+ once(event, listener) {
296
+ child.once(event, listener);
297
+ },
298
+ off(event, listener) {
299
+ child.off(event, listener);
300
+ },
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Recognize the multiple shapes Node + WHATWG fetch use when a request is
306
+ * cancelled via `AbortController`. Mirrors `isAbortError` in maestro/provider
307
+ * — both files keep their own copy so neither has to import the other (the
308
+ * agent providers are deliberately siblings, no shared error helpers).
309
+ */
310
+ function isAbortError(err: unknown): boolean {
311
+ if (!err || typeof err !== "object") return false;
312
+ const e = err as { name?: unknown; code?: unknown };
313
+ if (e.name === "AbortError") return true;
314
+ if (e.code === 20 || e.code === "ABORT_ERR") return true;
315
+ return false;
316
+ }
317
+
318
+ /**
319
+ * Core Claude provider.
320
+ * Wraps the @anthropic-ai/claude-agent-sdk into a UnifiedEvent stream so any
321
+ * caller can consume it identically to other providers such as codexProvider.
322
+ * The dispatch entry point is `runAgent` in agents/index.ts.
323
+ *
324
+ * Failure handling: any throw escaping the SDK iteration (missing Claude CLI
325
+ * binary, OAuth not configured, transient network) is caught and yielded as
326
+ * a `{type:"error"}` UnifiedEvent so the dispatcher never sees a synthetic
327
+ * crash and event-processor's `error` case classifies it with the right
328
+ * provider name. Matches the graceful pattern maestroProvider already uses.
329
+ * Abort signals are swallowed silently (the user moved on) so they don't
330
+ * surface as ghost error messages.
331
+ */
332
+ export async function* claudeProvider(opts: AgentQueryOptions): AsyncGenerator<UnifiedEvent> {
333
+ const claudeExecutable = hostedClaudeCodeExecutablePath();
334
+ // Up-front existence check — gives the user a clean, actionable error
335
+ // instead of waiting for the SDK to spawn the binary and fail mid-stream
336
+ // with an opaque ENOENT. The SDK's own resolution path runs after spawn,
337
+ // so if CLAUDE_EXECUTABLE points nowhere we want to surface it now.
338
+ if (claudeExecutable && !existsSync(claudeExecutable)) {
339
+ yield {
340
+ type: "error",
341
+ content: `Claude CLI not found at ${claudeExecutable}. Install Claude Code or configure the executable path.`,
342
+ };
343
+ return;
344
+ }
345
+
346
+ const cleanEnv = { ...process.env };
347
+ delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_URL;
348
+ delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN;
349
+ delete cleanEnv.CLAUDECODE;
350
+ cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= process.env.PADDLEOCR_TIMEOUT_MS ?? "300000";
351
+ // Workflow is a feature flag, not a disallowedTools-addressable tool name.
352
+ // The env var is the version-robust kill switch (works on any CLI the SDK
353
+ // spawns); the `settings` flag layer below covers it a second time.
354
+ cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
355
+
356
+ const queryOptions: Options = {
357
+ ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
358
+ spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
359
+ cwd: opts.cwd,
360
+ permissionMode: "bypassPermissions",
361
+ allowDangerouslySkipPermissions: true,
362
+ includePartialMessages: true,
363
+ env: cleanEnv,
364
+ mcpServers: hostedMcpServers(opts) as Options["mcpServers"],
365
+ abortController: opts.abortController,
366
+ // `AskUserQuestion` is Claude Code's built-in clarification tool, but it
367
+ // expects the SDK CLI to render a TUI prompt and read stdin for the
368
+ // answer — neither exists in this headless Telegram bridge. Without
369
+ // disallowing it, the model would call AskUserQuestion, the SDK would
370
+ // wait for a TUI response that never comes, and the turn would hang
371
+ // until abort. Forbidding the tool nudges the model to ask the same
372
+ // clarification as plain text (see topic-system prompt), which routes
373
+ // through the normal next-user-message path and the user just answers
374
+ // in the topic.
375
+ //
376
+ // Claude Code's private task store, Workflow, and native subagent tools are
377
+ // not Negotium state. They stay disabled even when a caller supplies custom
378
+ // agent definitions: delegation must go through runtime.spawn_subagent and
379
+ // task state must go through the required shared task MCP.
380
+ disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
381
+ ...(opts.model ? { model: opts.model } : {}),
382
+ ...(opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {}),
383
+ ...(opts.agents
384
+ ? {
385
+ agents: Object.fromEntries(
386
+ Object.entries(opts.agents).map(([name, def]) => {
387
+ const { effort, ...rest } = def;
388
+ const narrowed = typeof effort === "string" ? toClaudeEffort(effort) : effort;
389
+ return [name, narrowed !== undefined ? { ...rest, effort: narrowed } : rest];
390
+ }),
391
+ ),
392
+ }
393
+ : {}),
394
+ ...(toClaudeEffort(opts.effort) ? { effort: toClaudeEffort(opts.effort) } : {}),
395
+ hooks: {
396
+ PreToolUse: [
397
+ {
398
+ hooks: [
399
+ async (input: HookInput) => {
400
+ const { tool_name, tool_input } = input as PreToolUseHookInput;
401
+ if (referencesHostedSecretStorage(tool_input)) {
402
+ return {
403
+ hookSpecificOutput: {
404
+ hookEventName: "PreToolUse" as const,
405
+ permissionDecision: "deny" as const,
406
+ permissionDecisionReason: "Runtime secret storage access is not permitted",
407
+ },
408
+ };
409
+ }
410
+
411
+ // Block built-in run_in_background: the bash runs inside the CLI
412
+ // subprocess and dies when the turn ends (SIGTERM on IPC close).
413
+ // Direct the model to the MCP tool that survives across turns and
414
+ // auto-injects a completion notification via session-inbox.
415
+ if (
416
+ tool_name === "Bash" &&
417
+ (tool_input as Record<string, unknown>)?.run_in_background === true
418
+ ) {
419
+ return {
420
+ hookSpecificOutput: {
421
+ hookEventName: "PreToolUse" as const,
422
+ permissionDecision: "deny" as const,
423
+ permissionDecisionReason:
424
+ "run_in_background is not supported in headless mode — the bash process dies when the agent turn ends. Use mcp__background-bash__background_bash_run instead: it survives across turns and automatically injects the result into the session when done.",
425
+ },
426
+ };
427
+ }
428
+
429
+ // Detect real Vault placeholders but never inject plaintext into
430
+ // provider-visible tool input. The Vault MCP broker expands them.
431
+ const userId = opts.userId ?? "";
432
+ if (!shouldRedirectHostedVaultTool(userId, tool_name, tool_input)) {
433
+ return { continue: true };
434
+ }
435
+ return {
436
+ hookSpecificOutput: {
437
+ hookEventName: "PreToolUse" as const,
438
+ permissionDecision: "deny" as const,
439
+ permissionDecisionReason: VAULT_BROKER_REDIRECT_ERROR,
440
+ },
441
+ };
442
+ },
443
+ ],
444
+ },
445
+ ],
446
+ PostToolUse: [
447
+ {
448
+ hooks: [
449
+ async (input: HookInput) => {
450
+ const { tool_response } = input as PostToolUseHookInput;
451
+ const userId = opts.userId ?? "";
452
+ const redacted = deepMapStrings(tool_response, (value) =>
453
+ redactHostedSecrets(userId, value),
454
+ );
455
+ if (JSON.stringify(redacted) === JSON.stringify(tool_response)) {
456
+ return { continue: true };
457
+ }
458
+ return {
459
+ hookSpecificOutput: {
460
+ hookEventName: "PostToolUse" as const,
461
+ updatedToolOutput: redacted,
462
+ },
463
+ };
464
+ },
465
+ ],
466
+ },
467
+ ],
468
+ },
469
+ settingSources: ["project"] as Options["settingSources"],
470
+ // Disable the multi-agent Workflow feature: a headless backend query has
471
+ // no business fanning out orchestration subagents, and the "ultracode"
472
+ // keyword trigger would let user-authored message text opt into it.
473
+ settings: {
474
+ disableWorkflows: true,
475
+ workflowKeywordTriggerEnabled: false,
476
+ },
477
+ systemPrompt: opts.systemPrompt,
478
+ };
479
+
480
+ if (opts.sessionId) {
481
+ queryOptions.resume = opts.sessionId;
482
+ }
483
+
484
+ // Wrap the SDK iteration so init failures (missing claude CLI, OAuth not
485
+ // configured, transient network errors before the first event) surface as
486
+ // a normal error UnifiedEvent. Without this, an unconfigured Claude
487
+ // install would crash the dispatcher with a synthetic exception and the
488
+ // user-facing classifyError would still fire — but only via the handler
489
+ // catch path, which means cleanup ordering differed from maestro/codex.
490
+ // Keeping the failure shape consistent across providers simplifies the
491
+ // upstream error code.
492
+ // Thinking heartbeat: long extended-thinking stretches (minutes on fable /
493
+ // high effort) emit no text or tool events, so the room UI goes silent and
494
+ // the user can't tell whether the turn is still alive. The SDK does stream
495
+ // thinking signals (`thinking_delta` content deltas, `system/thinking_tokens`
496
+ // pings during redacted thinking) — digest them into a throttled
497
+ // `tool_progress` heartbeat that the client already renders as a live status
498
+ // chip (its handler also re-asserts the typing indicator).
499
+ const THINKING_BEAT_MS = 5000;
500
+ let thinkingStart: number | null = null;
501
+ let lastThinkingBeat = 0;
502
+ let lastContextTokens: number | undefined;
503
+ let lastContextModel: string | undefined;
504
+ const thinkingBeat = (): { type: "tool_progress"; toolName: string; elapsed: number } | null => {
505
+ const now = Date.now();
506
+ if (thinkingStart === null) {
507
+ thinkingStart = now;
508
+ lastThinkingBeat = now;
509
+ return { type: "tool_progress", toolName: "thinking", elapsed: 0 };
510
+ }
511
+ if (now - lastThinkingBeat < THINKING_BEAT_MS) return null;
512
+ lastThinkingBeat = now;
513
+ return { type: "tool_progress", toolName: "thinking", elapsed: (now - thinkingStart) / 1000 };
514
+ };
515
+
516
+ try {
517
+ const prompt = buildClaudePrompt(opts);
518
+ for await (const message of query({
519
+ prompt,
520
+ options: queryOptions,
521
+ })) {
522
+ // --- Thinking heartbeat (see above) ---
523
+ // A thinking "stretch" ends at the first non-thinking signal so elapsed
524
+ // restarts cleanly on the next thinking phase within the same turn.
525
+ if (
526
+ message.type === "system" &&
527
+ (message as { subtype?: string }).subtype === "thinking_tokens"
528
+ ) {
529
+ const beat = thinkingBeat();
530
+ if (beat) yield beat;
531
+ continue;
532
+ }
533
+
534
+ // --- Stream events (token-by-token) ---
535
+ // tool_use is NOT emitted from the stream path: the completed assistant
536
+ // message below carries the same block with its input already parsed,
537
+ // and tools only start executing after the message completes — so the
538
+ // stream copy added no timeliness, only a duplicate tool_use per turn
539
+ // in the unified log (doubling `<!-- Tool: ... -->` comments when
540
+ // bridging to another agent).
541
+ if (message.type === "stream_event") {
542
+ const streamMsg = message as SDKPartialAssistantMessage;
543
+ const evt = (streamMsg as { event?: StreamEvent }).event;
544
+ if (!evt) continue;
545
+
546
+ if (evt.type === "content_block_delta") {
547
+ const delta = evt as ContentBlockDelta;
548
+ if (delta.delta.type === "thinking_delta") {
549
+ const beat = thinkingBeat();
550
+ if (beat) yield beat;
551
+ } else if (delta.delta.type === "text_delta" && delta.delta.text) {
552
+ // Claude only exposes tool_use blocks on the completed assistant
553
+ // message below. Forwarding text deltas here would place all text
554
+ // before those tools, making the original block order impossible
555
+ // for consumers to reconstruct. The completed message emits text
556
+ // and tool blocks in their canonical order.
557
+ thinkingStart = null;
558
+ } else {
559
+ // Tool-args streaming (partial_json) etc. — thinking stretch over.
560
+ thinkingStart = null;
561
+ }
562
+ }
563
+
564
+ continue;
565
+ }
566
+
567
+ // Any completed message (assistant text, tool events, results) ends the
568
+ // current thinking stretch.
569
+ thinkingStart = null;
570
+
571
+ // --- Tool progress ---
572
+ if (message.type === "tool_progress") {
573
+ const m = message as SDKToolProgressMessage;
574
+ yield {
575
+ type: "tool_progress",
576
+ toolName: m.tool_name,
577
+ elapsed: m.elapsed_time_seconds,
578
+ };
579
+ continue;
580
+ }
581
+
582
+ // --- Tool use summary ---
583
+ if (message.type === "tool_use_summary") {
584
+ const m = message as SDKToolUseSummaryMessage;
585
+ yield { type: "tool_use_summary", summary: m.summary };
586
+ continue;
587
+ }
588
+
589
+ // --- System init ---
590
+ if (message.type === "system") {
591
+ const m = message as SDKSystemMessage;
592
+ if (m.subtype === "init") {
593
+ yield { type: "session", sessionId: m.session_id };
594
+ } else if (m.subtype === "task_started") {
595
+ const t = m as unknown as SDKTaskStartedMessage;
596
+ if (t.subagent_type && !t.skip_transcript) {
597
+ const desc = t.description ? ` ${t.description.slice(0, 60)}` : "";
598
+ yield { type: "status", content: `▶ [${t.subagent_type}]${desc}` };
599
+ }
600
+ } else {
601
+ // Log all other system messages (task_updated, task_progress,
602
+ // task_notification, etc.) so we can observe lifecycle events.
603
+ logger.debug(
604
+ { subtype: m.subtype, msg: m },
605
+ "claudeProvider: system message (unhandled subtype)",
606
+ );
607
+ }
608
+ continue;
609
+ }
610
+
611
+ // --- Result (terminal) ---
612
+ // SDK occasionally yields stray messages after a `result`; consuming them
613
+ // surfaces as ghost messages in the topic ("어? 뭐가 더 나오네?"). Close
614
+ // the generator immediately on terminal so the SDK stream is dropped and
615
+ // any trailing payloads can't leak through.
616
+ if (message.type === "result") {
617
+ const m = message as SDKResultMessage;
618
+ if (m.subtype === "success") {
619
+ const contextWindow =
620
+ (lastContextModel ? m.modelUsage?.[lastContextModel]?.contextWindow : undefined) ??
621
+ Object.values(m.modelUsage ?? {})[0]?.contextWindow;
622
+ yield {
623
+ type: "result",
624
+ content: m.result,
625
+ stopReason: m.stop_reason ?? "end_turn",
626
+ usage: m.usage
627
+ ? {
628
+ inputTokens: m.usage.input_tokens,
629
+ outputTokens: m.usage.output_tokens,
630
+ cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
631
+ cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
632
+ costUsd: m.total_cost_usd,
633
+ ...(lastContextTokens !== undefined && contextWindow
634
+ ? { contextTokens: lastContextTokens, contextWindow }
635
+ : {}),
636
+ }
637
+ : undefined,
638
+ };
639
+ yield* extractFileEvents(m.result, "result");
640
+ return;
641
+ }
642
+ // Error result
643
+ const budgetExceeded = m.subtype === "error_max_budget_usd";
644
+ const errorMsg = budgetExceeded
645
+ ? "The job reached its cost limit"
646
+ : m.errors?.join("; ") || "Unknown error";
647
+ yield {
648
+ type: "error",
649
+ content: errorMsg,
650
+ ...(budgetExceeded ? { code: "budget_exceeded" as const } : {}),
651
+ ...(m.usage
652
+ ? {
653
+ usage: {
654
+ inputTokens: m.usage.input_tokens,
655
+ outputTokens: m.usage.output_tokens,
656
+ cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
657
+ cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
658
+ costUsd: m.total_cost_usd,
659
+ },
660
+ }
661
+ : {}),
662
+ };
663
+ return;
664
+ }
665
+
666
+ // --- Assistant message ---
667
+ if (message.type === "assistant") {
668
+ const m = message as SDKAssistantMessage;
669
+ const content = (m.message?.content ?? []) as ContentBlock[];
670
+ const usage = m.message?.usage;
671
+ if (usage) {
672
+ lastContextTokens =
673
+ (usage.input_tokens ?? 0) +
674
+ (usage.cache_creation_input_tokens ?? 0) +
675
+ (usage.cache_read_input_tokens ?? 0) +
676
+ (usage.output_tokens ?? 0);
677
+ lastContextModel = m.message.model;
678
+ }
679
+
680
+ for (const block of content) {
681
+ if (block.type === "text") {
682
+ const textBlock = block as TextBlock;
683
+ yield { type: "text", content: textBlock.text };
684
+ // Surface mid-turn [FILE:/abs/path] tags right away, matching codex/
685
+ // maestro providers — without this, a model that narrates a file
686
+ // before its next tool call (rather than emitting the tag in the
687
+ // final `result` text) wouldn't trigger send-file at all. The file
688
+ // event handler dedups on normalized path so the same tag appearing
689
+ // again in `result` won't double-send.
690
+ yield* extractFileEvents(textBlock.text, "text");
691
+ } else if (block.type === "tool_use") {
692
+ const tb = block as ToolUseBlock;
693
+ yield {
694
+ type: "tool_use",
695
+ name: tb.name,
696
+ input: tb.input || {},
697
+ toolUseId: tb.id,
698
+ };
699
+ }
700
+ }
701
+ continue;
702
+ }
703
+
704
+ // --- User message (tool results ride on user-role messages) ---
705
+ // The Anthropic message protocol puts tool_result blocks in USER
706
+ // messages, never assistant ones — without this branch the unified log
707
+ // records tool calls but no results, losing information codex topics
708
+ // keep (visible when bridging a claude topic to another agent).
709
+ if (message.type === "user") {
710
+ const um = message as SDKUserMessage | SDKUserMessageReplay;
711
+ // Replays re-deliver transcript history on resume; mapping them would
712
+ // duplicate every past tool_result into the unified log.
713
+ if ("isReplay" in um && um.isReplay) continue;
714
+ const content = um.message?.content;
715
+ if (!Array.isArray(content)) continue;
716
+ for (const block of content as ContentBlock[]) {
717
+ if (block.type !== "tool_result") continue;
718
+ const trBlock = block as ToolResultBlock;
719
+ const trContent =
720
+ typeof trBlock.content === "string" ? trBlock.content.slice(0, 200) : "";
721
+ yield {
722
+ type: "tool_result",
723
+ toolUseId: trBlock.tool_use_id || "",
724
+ content: trContent,
725
+ };
726
+ }
727
+ }
728
+ }
729
+ } catch (e) {
730
+ // Abort = user moved on; don't surface as an error message. The handler
731
+ // catches the abort signal separately via abortReason, so swallowing
732
+ // here matches both maestro and codex behavior.
733
+ if (isAbortError(e) || opts.abortController?.signal.aborted) return;
734
+ logger.error({ err: e }, "claudeProvider: SDK iteration failed");
735
+ yield { type: "error", content: errMsg(e) };
736
+ }
737
+ }