@nodetool-ai/websocket 0.8.1 → 0.8.2

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 (284) hide show
  1. package/dist/.nodetool-build-stamp +1 -1
  2. package/dist/collection-api.d.ts.map +1 -1
  3. package/dist/collection-api.js +41 -4
  4. package/dist/collection-api.js.map +1 -1
  5. package/dist/error-traces.d.ts +56 -0
  6. package/dist/error-traces.d.ts.map +1 -0
  7. package/dist/error-traces.js +179 -0
  8. package/dist/error-traces.js.map +1 -0
  9. package/dist/fake-runtime.d.ts.map +1 -1
  10. package/dist/fake-runtime.js +40 -3
  11. package/dist/fake-runtime.js.map +1 -1
  12. package/dist/generation-recovery.d.ts.map +1 -1
  13. package/dist/generation-recovery.js +26 -4
  14. package/dist/generation-recovery.js.map +1 -1
  15. package/dist/http-api.d.ts.map +1 -1
  16. package/dist/http-api.js +15 -0
  17. package/dist/http-api.js.map +1 -1
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/interrupted-jobs.d.ts +14 -0
  23. package/dist/interrupted-jobs.d.ts.map +1 -0
  24. package/dist/interrupted-jobs.js +28 -0
  25. package/dist/interrupted-jobs.js.map +1 -0
  26. package/dist/lib/app-debug-service.js +1 -1
  27. package/dist/lib/app-debug-service.js.map +1 -1
  28. package/dist/lib/app-instances-service.d.ts +1284 -0
  29. package/dist/lib/app-instances-service.d.ts.map +1 -0
  30. package/dist/lib/app-instances-service.js +378 -0
  31. package/dist/lib/app-instances-service.js.map +1 -0
  32. package/dist/lib/app-run-cancellation.d.ts +4 -0
  33. package/dist/lib/app-run-cancellation.d.ts.map +1 -0
  34. package/dist/lib/app-run-cancellation.js +28 -0
  35. package/dist/lib/app-run-cancellation.js.map +1 -0
  36. package/dist/lib/applications-service.d.ts +182 -1
  37. package/dist/lib/applications-service.d.ts.map +1 -1
  38. package/dist/lib/applications-service.js +20 -4
  39. package/dist/lib/applications-service.js.map +1 -1
  40. package/dist/lib/asset-paths.d.ts +7 -11
  41. package/dist/lib/asset-paths.d.ts.map +1 -1
  42. package/dist/lib/asset-paths.js +8 -56
  43. package/dist/lib/asset-paths.js.map +1 -1
  44. package/dist/lib/asset-response.d.ts.map +1 -1
  45. package/dist/lib/asset-response.js +2 -1
  46. package/dist/lib/asset-response.js.map +1 -1
  47. package/dist/lib/audio-peaks.d.ts.map +1 -1
  48. package/dist/lib/audio-peaks.js +5 -1
  49. package/dist/lib/audio-peaks.js.map +1 -1
  50. package/dist/lib/bridge.d.ts.map +1 -1
  51. package/dist/lib/bridge.js.map +1 -1
  52. package/dist/lib/document-model-interfaces.d.ts.map +1 -1
  53. package/dist/lib/document-model-interfaces.js +9 -8
  54. package/dist/lib/document-model-interfaces.js.map +1 -1
  55. package/dist/lib/example-apps.d.ts +9 -0
  56. package/dist/lib/example-apps.d.ts.map +1 -1
  57. package/dist/lib/example-apps.js +61 -14
  58. package/dist/lib/example-apps.js.map +1 -1
  59. package/dist/lib/http-tracing.d.ts +12 -0
  60. package/dist/lib/http-tracing.d.ts.map +1 -0
  61. package/dist/lib/http-tracing.js +81 -0
  62. package/dist/lib/http-tracing.js.map +1 -0
  63. package/dist/lib/media.d.ts +1 -6
  64. package/dist/lib/media.d.ts.map +1 -1
  65. package/dist/lib/media.js +8 -44
  66. package/dist/lib/media.js.map +1 -1
  67. package/dist/lib/public-routes.d.ts +7 -0
  68. package/dist/lib/public-routes.d.ts.map +1 -1
  69. package/dist/lib/public-routes.js +14 -0
  70. package/dist/lib/public-routes.js.map +1 -1
  71. package/dist/lib/public-workflow-share.d.ts +20 -0
  72. package/dist/lib/public-workflow-share.d.ts.map +1 -0
  73. package/dist/lib/public-workflow-share.js +45 -0
  74. package/dist/lib/public-workflow-share.js.map +1 -0
  75. package/dist/lib/run-trace-live-delivery.d.ts +25 -0
  76. package/dist/lib/run-trace-live-delivery.d.ts.map +1 -0
  77. package/dist/lib/run-trace-live-delivery.js +91 -0
  78. package/dist/lib/run-trace-live-delivery.js.map +1 -0
  79. package/dist/lib/test-ui-run-routes.d.ts +8 -0
  80. package/dist/lib/test-ui-run-routes.d.ts.map +1 -0
  81. package/dist/lib/test-ui-run-routes.js +42 -0
  82. package/dist/lib/test-ui-run-routes.js.map +1 -0
  83. package/dist/lib/thumbnail.d.ts.map +1 -1
  84. package/dist/lib/thumbnail.js +12 -9
  85. package/dist/lib/thumbnail.js.map +1 -1
  86. package/dist/lib/timeline-bundle.d.ts +148 -16
  87. package/dist/lib/timeline-bundle.d.ts.map +1 -1
  88. package/dist/lib/timeline-bundle.js +12 -0
  89. package/dist/lib/timeline-bundle.js.map +1 -1
  90. package/dist/lib/video-proxy.d.ts.map +1 -1
  91. package/dist/lib/video-proxy.js +3 -2
  92. package/dist/lib/video-proxy.js.map +1 -1
  93. package/dist/lib/workflow-service.d.ts.map +1 -1
  94. package/dist/lib/workflow-service.js +4 -1
  95. package/dist/lib/workflow-service.js.map +1 -1
  96. package/dist/mcp-agent-tools.d.ts.map +1 -1
  97. package/dist/mcp-agent-tools.js +93 -162
  98. package/dist/mcp-agent-tools.js.map +1 -1
  99. package/dist/mcp-client-config.d.ts +64 -0
  100. package/dist/mcp-client-config.d.ts.map +1 -0
  101. package/dist/mcp-client-config.js +353 -0
  102. package/dist/mcp-client-config.js.map +1 -0
  103. package/dist/models-api.d.ts +10 -0
  104. package/dist/models-api.d.ts.map +1 -1
  105. package/dist/models-api.js +23 -0
  106. package/dist/models-api.js.map +1 -1
  107. package/dist/node-registry-setup.d.ts +1 -1
  108. package/dist/node-registry-setup.d.ts.map +1 -1
  109. package/dist/node-registry-setup.js +21 -114
  110. package/dist/node-registry-setup.js.map +1 -1
  111. package/dist/plugins/websocket.d.ts.map +1 -1
  112. package/dist/plugins/websocket.js +20 -57
  113. package/dist/plugins/websocket.js.map +1 -1
  114. package/dist/python-bridge-gate.d.ts +28 -0
  115. package/dist/python-bridge-gate.d.ts.map +1 -0
  116. package/dist/python-bridge-gate.js +36 -0
  117. package/dist/python-bridge-gate.js.map +1 -0
  118. package/dist/routes/app-runs.d.ts +4 -0
  119. package/dist/routes/app-runs.d.ts.map +1 -0
  120. package/dist/routes/app-runs.js +173 -0
  121. package/dist/routes/app-runs.js.map +1 -0
  122. package/dist/routes/js-scripts.d.ts +9 -3
  123. package/dist/routes/js-scripts.d.ts.map +1 -1
  124. package/dist/routes/js-scripts.js +450 -68
  125. package/dist/routes/js-scripts.js.map +1 -1
  126. package/dist/routes/nodes.d.ts.map +1 -1
  127. package/dist/routes/nodes.js +32 -1
  128. package/dist/routes/nodes.js.map +1 -1
  129. package/dist/routes/public-shared-workflows.d.ts +16 -0
  130. package/dist/routes/public-shared-workflows.d.ts.map +1 -0
  131. package/dist/routes/public-shared-workflows.js +55 -0
  132. package/dist/routes/public-shared-workflows.js.map +1 -0
  133. package/dist/routes/run-spans.d.ts +4 -0
  134. package/dist/routes/run-spans.d.ts.map +1 -0
  135. package/dist/routes/run-spans.js +53 -0
  136. package/dist/routes/run-spans.js.map +1 -0
  137. package/dist/routes/timeline-fonts.d.ts +22 -0
  138. package/dist/routes/timeline-fonts.d.ts.map +1 -1
  139. package/dist/routes/timeline-fonts.js +138 -7
  140. package/dist/routes/timeline-fonts.js.map +1 -1
  141. package/dist/screenshot-server.js +4 -5
  142. package/dist/screenshot-server.js.map +1 -1
  143. package/dist/sdk/sdk-model-download-service.d.ts.map +1 -1
  144. package/dist/sdk/sdk-model-download-service.js +5 -2
  145. package/dist/sdk/sdk-model-download-service.js.map +1 -1
  146. package/dist/server.js +86 -19
  147. package/dist/server.js.map +1 -1
  148. package/dist/session/app-run-fold.d.ts +7 -0
  149. package/dist/session/app-run-fold.d.ts.map +1 -0
  150. package/dist/session/app-run-fold.js +45 -0
  151. package/dist/session/app-run-fold.js.map +1 -0
  152. package/dist/session/chat-prompt.d.ts.map +1 -1
  153. package/dist/session/chat-prompt.js +37 -15
  154. package/dist/session/chat-prompt.js.map +1 -1
  155. package/dist/session/chat-turn.d.ts +1 -0
  156. package/dist/session/chat-turn.d.ts.map +1 -1
  157. package/dist/session/chat-turn.js +203 -107
  158. package/dist/session/chat-turn.js.map +1 -1
  159. package/dist/session/commands.d.ts.map +1 -1
  160. package/dist/session/commands.js +6 -0
  161. package/dist/session/commands.js.map +1 -1
  162. package/dist/session/inference.d.ts +5 -0
  163. package/dist/session/inference.d.ts.map +1 -1
  164. package/dist/session/inference.js +80 -9
  165. package/dist/session/inference.js.map +1 -1
  166. package/dist/session/job-execution.d.ts +7 -0
  167. package/dist/session/job-execution.d.ts.map +1 -1
  168. package/dist/session/job-execution.js +297 -7
  169. package/dist/session/job-execution.js.map +1 -1
  170. package/dist/session/model-interfaces.d.ts +2 -1
  171. package/dist/session/model-interfaces.d.ts.map +1 -1
  172. package/dist/session/model-interfaces.js +1 -0
  173. package/dist/session/model-interfaces.js.map +1 -1
  174. package/dist/storage-retention.d.ts +5 -0
  175. package/dist/storage-retention.d.ts.map +1 -1
  176. package/dist/storage-retention.js +22 -1
  177. package/dist/storage-retention.js.map +1 -1
  178. package/dist/test-ui-server.d.ts.map +1 -1
  179. package/dist/test-ui-server.js +21 -3
  180. package/dist/test-ui-server.js.map +1 -1
  181. package/dist/timeline-generation-recovery.d.ts +4 -0
  182. package/dist/timeline-generation-recovery.d.ts.map +1 -0
  183. package/dist/timeline-generation-recovery.js +72 -0
  184. package/dist/timeline-generation-recovery.js.map +1 -0
  185. package/dist/triggers/dispatcher.js +2 -2
  186. package/dist/triggers/dispatcher.js.map +1 -1
  187. package/dist/triggers/stores.js +8 -8
  188. package/dist/triggers/stores.js.map +1 -1
  189. package/dist/triggers/webhook-route.js +2 -2
  190. package/dist/triggers/webhook-route.js.map +1 -1
  191. package/dist/trpc/error-logging.d.ts +10 -0
  192. package/dist/trpc/error-logging.d.ts.map +1 -1
  193. package/dist/trpc/error-logging.js +21 -0
  194. package/dist/trpc/error-logging.js.map +1 -1
  195. package/dist/trpc/router.d.ts +100 -31028
  196. package/dist/trpc/router.d.ts.map +1 -1
  197. package/dist/trpc/router.js +12 -3
  198. package/dist/trpc/router.js.map +1 -1
  199. package/dist/trpc/routers/app-instances.d.ts +2269 -0
  200. package/dist/trpc/routers/app-instances.d.ts.map +1 -0
  201. package/dist/trpc/routers/app-instances.js +52 -0
  202. package/dist/trpc/routers/app-instances.js.map +1 -0
  203. package/dist/trpc/routers/app-runs.d.ts +1092 -0
  204. package/dist/trpc/routers/app-runs.d.ts.map +1 -0
  205. package/dist/trpc/routers/app-runs.js +28 -0
  206. package/dist/trpc/routers/app-runs.js.map +1 -0
  207. package/dist/trpc/routers/applications.d.ts +1095 -0
  208. package/dist/trpc/routers/applications.d.ts.map +1 -1
  209. package/dist/trpc/routers/applications.js +5 -2
  210. package/dist/trpc/routers/applications.js.map +1 -1
  211. package/dist/trpc/routers/assets.d.ts +10 -0
  212. package/dist/trpc/routers/assets.d.ts.map +1 -1
  213. package/dist/trpc/routers/assets.js +4 -0
  214. package/dist/trpc/routers/assets.js.map +1 -1
  215. package/dist/trpc/routers/bug-reports.d.ts +33 -0
  216. package/dist/trpc/routers/bug-reports.d.ts.map +1 -0
  217. package/dist/trpc/routers/bug-reports.js +69 -0
  218. package/dist/trpc/routers/bug-reports.js.map +1 -0
  219. package/dist/trpc/routers/documents.d.ts.map +1 -1
  220. package/dist/trpc/routers/documents.js +8 -2
  221. package/dist/trpc/routers/documents.js.map +1 -1
  222. package/dist/trpc/routers/error-traces.d.ts +132 -0
  223. package/dist/trpc/routers/error-traces.d.ts.map +1 -0
  224. package/dist/trpc/routers/error-traces.js +118 -0
  225. package/dist/trpc/routers/error-traces.js.map +1 -0
  226. package/dist/trpc/routers/extension.d.ts +3 -3
  227. package/dist/trpc/routers/extension.js +6 -6
  228. package/dist/trpc/routers/extension.js.map +1 -1
  229. package/dist/trpc/routers/gameDraftHistory.d.ts +8 -0
  230. package/dist/trpc/routers/gameDraftHistory.d.ts.map +1 -0
  231. package/dist/trpc/routers/gameDraftHistory.js +40 -0
  232. package/dist/trpc/routers/gameDraftHistory.js.map +1 -0
  233. package/dist/trpc/routers/games.d.ts +23691 -4799
  234. package/dist/trpc/routers/games.d.ts.map +1 -1
  235. package/dist/trpc/routers/games.js +301 -83
  236. package/dist/trpc/routers/games.js.map +1 -1
  237. package/dist/trpc/routers/jobs.d.ts +35 -0
  238. package/dist/trpc/routers/jobs.d.ts.map +1 -1
  239. package/dist/trpc/routers/jobs.js +84 -1
  240. package/dist/trpc/routers/jobs.js.map +1 -1
  241. package/dist/trpc/routers/mcp-config.d.ts +3 -1
  242. package/dist/trpc/routers/mcp-config.d.ts.map +1 -1
  243. package/dist/trpc/routers/mcp-config.js +46 -208
  244. package/dist/trpc/routers/mcp-config.js.map +1 -1
  245. package/dist/trpc/routers/models.d.ts +130 -0
  246. package/dist/trpc/routers/models.d.ts.map +1 -1
  247. package/dist/trpc/routers/models.js +99 -31
  248. package/dist/trpc/routers/models.js.map +1 -1
  249. package/dist/trpc/routers/runs.d.ts +472 -0
  250. package/dist/trpc/routers/runs.d.ts.map +1 -0
  251. package/dist/trpc/routers/runs.js +40 -0
  252. package/dist/trpc/routers/runs.js.map +1 -0
  253. package/dist/trpc/routers/settings.d.ts +3 -0
  254. package/dist/trpc/routers/settings.d.ts.map +1 -1
  255. package/dist/trpc/routers/settings.js +2 -0
  256. package/dist/trpc/routers/settings.js.map +1 -1
  257. package/dist/trpc/routers/sketch.d.ts +14 -0
  258. package/dist/trpc/routers/sketch.d.ts.map +1 -1
  259. package/dist/trpc/routers/storyboards.d.ts +732 -0
  260. package/dist/trpc/routers/storyboards.d.ts.map +1 -1
  261. package/dist/trpc/routers/timeline.d.ts +632 -63
  262. package/dist/trpc/routers/timeline.d.ts.map +1 -1
  263. package/dist/trpc/routers/timeline.js +178 -2
  264. package/dist/trpc/routers/timeline.js.map +1 -1
  265. package/dist/trpc/routers/worker.d.ts +10 -9
  266. package/dist/trpc/routers/worker.d.ts.map +1 -1
  267. package/dist/trpc/routers/worker.js +3 -2
  268. package/dist/trpc/routers/worker.js.map +1 -1
  269. package/dist/trpc/routers/workflows.d.ts +56 -3
  270. package/dist/trpc/routers/workflows.d.ts.map +1 -1
  271. package/dist/trpc/routers/workflows.js +23 -2
  272. package/dist/trpc/routers/workflows.js.map +1 -1
  273. package/dist/trpc/sandbox-coverage.d.ts.map +1 -1
  274. package/dist/trpc/sandbox-coverage.js +90 -0
  275. package/dist/trpc/sandbox-coverage.js.map +1 -1
  276. package/dist/websocket-client-session.d.ts +2 -0
  277. package/dist/websocket-client-session.d.ts.map +1 -1
  278. package/dist/websocket-client-session.js +14 -0
  279. package/dist/websocket-client-session.js.map +1 -1
  280. package/package.json +56 -41
  281. package/dist/extension-cdp-bridge.d.ts +0 -85
  282. package/dist/extension-cdp-bridge.d.ts.map +0 -1
  283. package/dist/extension-cdp-bridge.js +0 -163
  284. package/dist/extension-cdp-bridge.js.map +0 -1
@@ -1,3 +1,9 @@
1
+ import { registerChatRunTrace, registerWorkflowRunTrace, withRegisteredRunTrace, settleRegisteredRunTrace } from "@nodetool-ai/execution";
2
+ import { getRunTraceScope, suppressRunTraceContent } from "@nodetool-ai/runtime";
3
+ import { TRACE_RESTRICTED_CAPABILITY_MODULES } from "@nodetool-ai/protocol";
4
+ import { trace, SpanStatusCode } from "@opentelemetry/api";
5
+ import { settleRunTrace, registerRunTraceParent, registerRunTraceParents, threadHasTraceContentTools } from "@nodetool-ai/models";
6
+ import { capabilityModuleOf, listCapabilitySpecs } from "@nodetool-ai/agents";
1
7
  import { createLogger } from "@nodetool-ai/config";
2
8
  import { randomUUID } from "node:crypto";
3
9
  import { SUPERSEDED_TOOL_RESULT, repairOrphanedToolCalls } from "../chat-tool-call-repair.js";
@@ -13,7 +19,7 @@ import { ACTIVE_MODEL_CONTEXT_KEY, DIRECT_TOOL_NAMES, RUN_BUDGET_CONTEXT_KEY, cr
13
19
  import { isModelSelection, PROVIDER_IDS, NO_MODEL_SELECTED_MESSAGE, noMediaModelSelectedMessage } from "@nodetool-ai/protocol";
14
20
  import { Tool, compactAssetUris, compactResourceIds } from "@nodetool-ai/agents";
15
21
  import { createChatCodeActSession, createSandboxClock, sandboxPackagesForChat, CODEACT_RESIDENT_TOOL_NAMES, EXECUTE_CODE_TOOL_NAME } from "@nodetool-ai/agents";
16
- import { getBuiltinTools, getAllMcpTools, getGoogleWorkspaceTools, getApifyTools, getSerpApiTools, toolForCapabilityName, gateTools, capabilityFromTool, createCapabilityRun, contextSecretAvailability, BackgroundSubtaskRegistry, UNGATED, PERMISSION_GATE_CONTEXT_KEY, GAME_ASSET_NODE_RUNNER_CONTEXT_KEY, extractInjectableImages, registerTemporaryImageHandle } from "@nodetool-ai/agents";
22
+ import { availableBuiltinToolNames, GOOGLE_WORKSPACE_TOOL_NAMES, APIFY_TOOL_NAMES, SERPAPI_TOOL_NAMES, capabilityForName, capabilityProviderTool, getAllMcpCapabilitySpecs, toolFromCapability, capabilityFromTool, createCapabilityRun, contextSecretAvailability, BackgroundSubtaskRegistry, PERMISSION_GATE_CONTEXT_KEY, GAME_ASSET_NODE_RUNNER_CONTEXT_KEY, extractInjectableImages, registerTemporaryImageHandle } from "@nodetool-ai/agents";
17
23
  import { mcpToolHostDeps } from "../mcp-tool-deps.js";
18
24
  import { externalMcpToolsFor } from "../external-mcp.js";
19
25
  import { formatSkillCatalogForPrompt, mergeSystemSkills, formatMemoriesForPrompt } from "@nodetool-ai/agents";
@@ -25,7 +31,41 @@ import { getSetting } from "../settings-registry.js";
25
31
  import { appendContextToLastUser, createWorkflowResponseContent, dbMessageToProviderMessage, extractTextContent, historySinceCompaction, invokedSkillsSection, toolResultDisplayText } from "./chat-history.js";
26
32
  import { chooseCompactionCut, compactionThreshold, COMPACTION_SUMMARY_MAX_TOKENS, holdsTranscriptServerSide, readCompactionSettings, summarizeForCompaction } from "./chat-compaction.js";
27
33
  import { createRelayActivityWaiter, DEFAULT_RUN_JOB_EXECUTION_OPTIONS } from "./job-execution.js";
34
+ function restrictedTraceToolNames() {
35
+ const modules = new Set(TRACE_RESTRICTED_CAPABILITY_MODULES);
36
+ return listCapabilitySpecs().filter((spec) => modules.has(capabilityModuleOf(spec.name) ?? "")).map((spec) => spec.name);
37
+ }
38
+ function seedStoredToolProvenance(messages) {
39
+ for (const message of messages) {
40
+ const names = message.name ? [message.name] : [];
41
+ if (isCompactionMessage(message)) {
42
+ names.push(...(message.tools ?? []));
43
+ }
44
+ for (const call of message.tool_calls ?? []) {
45
+ if (!isRecord(call)) {
46
+ continue;
47
+ }
48
+ if (isString(call.name)) {
49
+ names.push(call.name);
50
+ }
51
+ if (isRecord(call.function) && isString(call.function.name)) {
52
+ names.push(call.function.name);
53
+ }
54
+ }
55
+ for (const name of names) {
56
+ const moduleName = capabilityModuleOf(name);
57
+ if (moduleName === "email" || moduleName === "google" || moduleName === "browser") {
58
+ suppressRunTraceContent();
59
+ }
60
+ }
61
+ }
62
+ }
28
63
  const log = createLogger("nodetool.websocket.runner");
64
+ /** Providers whose credential is an account sign-in, not an API key. */
65
+ const SIGN_IN_PROVIDER_LABELS = {
66
+ [PROVIDER_IDS.CODEX]: "Codex",
67
+ [PROVIDER_IDS.CLAUDE_AGENT_SDK]: "Claude"
68
+ };
29
69
  const GENERIC_CHAT_ERROR = "Something went wrong while processing your request. Please try again.";
30
70
  /**
31
71
  * Keep implementation details out of the client error envelope. Provider
@@ -105,24 +145,20 @@ function providerFailureMessage(error, providerId, model) {
105
145
  const gemini = geminiFailureSummary(error, providerId);
106
146
  const groq = groqRequestFailureMessage(error);
107
147
  const status = httpStatusFromError(error) ?? gemini?.status ?? null;
148
+ const statusCode = status ?? undefined;
108
149
  const rawMessage = groq ?? gemini?.message ?? safeClientErrorText(error);
109
- let bodyMessage = null;
110
- if (isObjectLike(error)) {
111
- const body = error.body ?? error.response;
112
- if (isObjectLike(body)) {
113
- const bodyError = isObjectLike(body.error) ? body.error : body;
114
- if (isObjectLike(bodyError) && isString(bodyError.message)) {
115
- const safe = safeClientErrorText(bodyError.message);
116
- bodyMessage = safe === GENERIC_CHAT_ERROR ? null : safe;
117
- }
118
- }
119
- }
150
+ const body = isObjectLike(error) ? (error.body ?? error.response) : undefined;
151
+ const bodyError = isObjectLike(body) && isObjectLike(body.error) ? body.error : body;
152
+ const bodyText = isObjectLike(bodyError) && isString(bodyError.message)
153
+ ? safeClientErrorText(bodyError.message)
154
+ : null;
155
+ const bodyMessage = bodyText === GENERIC_CHAT_ERROR ? null : bodyText;
120
156
  if (providerId.toLowerCase() === "openai" &&
121
157
  /\borganization must be verified\b/i.test(bodyMessage ?? rawMessage)) {
122
158
  return {
123
159
  errorType: status === null ? "error" : "http_status_error",
124
160
  message: "This model requires a verified OpenAI organization. Verify your provider organization or choose a model your account can access.",
125
- statusCode: status ?? undefined
161
+ statusCode
126
162
  };
127
163
  }
128
164
  const detail = providerFailureDetail(error);
@@ -131,28 +167,31 @@ function providerFailureMessage(error, providerId, model) {
131
167
  return {
132
168
  errorType: "error",
133
169
  message: `The ${target} request is too large for the model context window. Shorten the conversation or remove some attachments and try again.`,
134
- statusCode: status ?? undefined
170
+ statusCode
135
171
  };
136
172
  }
137
173
  if (detail?.code === "provider_auth") {
174
+ const signIn = SIGN_IN_PROVIDER_LABELS[detail.provider];
175
+ const remedy = signIn ? `Sign in to ${signIn} again` : "Check the API key";
138
176
  return {
139
177
  errorType: "error",
140
- message: `Authentication failed: ${detail.provider} rejected the configured credentials. Check the API key in Settings → Models & Providers.`,
141
- statusCode: status ?? undefined
178
+ message: `Authentication failed: ${detail.provider} rejected the configured credentials. ${remedy} in Settings → Models & Providers.`,
179
+ statusCode
142
180
  };
143
181
  }
144
182
  if (status === null) {
145
- return { errorType: "error", message: rawMessage, statusCode: undefined };
183
+ return { errorType: "error", message: rawMessage, statusCode };
146
184
  }
147
- let message = rawMessage;
185
+ const modelName = model || "requested";
186
+ let message;
148
187
  if (gemini?.kind === "quota") {
149
188
  message = `Account quota exhausted for ${providerId}/${model}. Check your ${providerId} quota or billing and try again later.`;
150
189
  }
151
190
  else if (gemini?.kind === "model") {
152
- message = `Model ${model || "requested"} is unavailable on ${providerId}. Choose another model or check that your account has access to it.`;
191
+ message = `Model ${modelName} is unavailable on ${providerId}. Choose another model or check that your account has access to it.`;
153
192
  }
154
193
  else if (gemini?.kind === "tools") {
155
- message = `Model ${model || "requested"} does not support the requested tools on ${providerId}. Choose a model with tool support or disable tools.`;
194
+ message = `Model ${modelName} does not support the requested tools on ${providerId}. Choose a model with tool support or disable tools.`;
156
195
  }
157
196
  else if (status === 400) {
158
197
  message = `Bad request: ${groq ?? bodyMessage ?? rawMessage}`;
@@ -168,20 +207,20 @@ function providerFailureMessage(error, providerId, model) {
168
207
  "Access forbidden: You don't have permission for this resource. Check the provider key's model and account access.";
169
208
  }
170
209
  else if (status === 404) {
171
- message = `Model ${model || "requested"} was not found or is unavailable on ${providerId}. Choose another model or check account access.`;
210
+ message = `Model ${modelName} was not found or is unavailable on ${providerId}. Choose another model or check account access.`;
172
211
  }
173
212
  else if (status === 413) {
174
- const detail = groq ?? bodyMessage ?? rawMessage;
213
+ const bodyDetail = bodyMessage ?? rawMessage;
175
214
  message =
176
215
  groq ??
177
- (detail === GENERIC_CHAT_ERROR
216
+ (bodyDetail === GENERIC_CHAT_ERROR
178
217
  ? "Request is too large for the provider. Shorten the conversation or remove some attachments and try again."
179
- : `Request is too large for the provider (${detail}). Shorten the conversation or remove some attachments and try again.`);
218
+ : `Request is too large for the provider (${bodyDetail}). Shorten the conversation or remove some attachments and try again.`);
180
219
  }
181
220
  else if (status === 429) {
182
- message = groq
183
- ? groq
184
- : "Rate limited: Too many requests or insufficient provider quota. Check your provider plan and try again later.";
221
+ message =
222
+ groq ??
223
+ "Rate limited: Too many requests or insufficient provider quota. Check your provider plan and try again later.";
185
224
  }
186
225
  else if (status >= 500) {
187
226
  message =
@@ -192,7 +231,7 @@ function providerFailureMessage(error, providerId, model) {
192
231
  else {
193
232
  message = `HTTP error (${status}): ${rawMessage}`;
194
233
  }
195
- return { errorType: "http_status_error", message, statusCode: status };
234
+ return { errorType: "http_status_error", message, statusCode };
196
235
  }
197
236
  /**
198
237
  * How many of a user's newest memories the turn reads to build its block. The
@@ -487,11 +526,12 @@ export class ChatTurnHandler {
487
526
  delete data.thread_id;
488
527
  const userId = this.session.requireUserId();
489
528
  delete data.user_id;
490
- return Message.create({
491
- thread_id: threadId,
492
- user_id: userId,
493
- ...data
494
- });
529
+ const message = await Message.create({ thread_id: threadId, user_id: userId, ...data });
530
+ const scope = getRunTraceScope();
531
+ if (scope) {
532
+ await registerRunTraceParent(userId, scope.runId, { kind: "message", id: message.id });
533
+ }
534
+ return message;
495
535
  }
496
536
  /**
497
537
  * Persist raw image bytes carried on an assistant message (native providers
@@ -923,6 +963,7 @@ export class ChatTurnHandler {
923
963
  : null,
924
964
  assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url"
925
965
  });
966
+ context.runTraceContext = getRunTraceScope() ?? null;
926
967
  // This context is built here rather than copied from the turn, so the
927
968
  // turn's gate has to be put on it by hand. The node's own loops read it
928
969
  // with `gateFromContext`; a node run outside a chat turn carries none and
@@ -1043,9 +1084,32 @@ export class ChatTurnHandler {
1043
1084
  log.debug("Chat message", { threadId, model, provider: providerId });
1044
1085
  // Save user message to DB — matches Python's _save_message_to_db_async(data)
1045
1086
  const turnMessage = await this.saveMessageToDb(data);
1087
+ const rootContext = createRuntimeContext({ jobId: turnMessage.id, threadId, workflowId, userId, workspace: null });
1088
+ await registerChatRunTrace(rootContext, { messageId: turnMessage.id, threadId });
1089
+ return withRegisteredRunTrace(rootContext, "chat.turn", async () => {
1090
+ try {
1091
+ if (await threadHasTraceContentTools(userId, threadId, restrictedTraceToolNames())) {
1092
+ suppressRunTraceContent();
1093
+ }
1094
+ await this.handlePersistedChatTurn(data, turnMessage, rootContext, requestSeq, signal);
1095
+ await settleRegisteredRunTrace(rootContext, signal?.aborted ? "cancelled" : "completed");
1096
+ }
1097
+ catch (error) {
1098
+ await settleRegisteredRunTrace(rootContext, signal?.aborted ? "cancelled" : "failed", error instanceof Error ? error.message : String(error));
1099
+ throw error;
1100
+ }
1101
+ });
1102
+ }
1103
+ async handlePersistedChatTurn(data, turnMessage, rootContext, requestSeq, signal) {
1104
+ const threadId = String(data.thread_id);
1105
+ const workflowId = isString(data.workflow_id) ? data.workflow_id : null;
1106
+ const userId = this.session.requireUserId();
1107
+ const providerId = String(data.provider);
1108
+ const model = String(data.model);
1046
1109
  if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
1047
1110
  return;
1048
1111
  if (!this.session.resolveProvider) {
1112
+ await settleRegisteredRunTrace(rootContext, "failed", "No provider resolver configured");
1049
1113
  await this.session.send({
1050
1114
  type: "error",
1051
1115
  message: "No provider resolver configured",
@@ -1134,9 +1198,12 @@ export class ChatTurnHandler {
1134
1198
  toolCalls: null,
1135
1199
  threadId: null
1136
1200
  });
1201
+ const copiedMessageIds = new Set();
1137
1202
  const convertDbMessages = (rows) => {
1138
1203
  const out = [];
1139
1204
  for (const m of rows) {
1205
+ copiedMessageIds.add(m.id);
1206
+ seedStoredToolProvenance([m]);
1140
1207
  const pm = dbMessageToProviderMessage(m, this.session.userId);
1141
1208
  if (pm)
1142
1209
  out.push(pm);
@@ -1160,6 +1227,7 @@ export class ChatTurnHandler {
1160
1227
  reverse: true,
1161
1228
  limit: SESSION_PROBE_WINDOW
1162
1229
  });
1230
+ seedStoredToolProvenance(recent);
1163
1231
  const probeHasWholeThread = recent.length < SESSION_PROBE_WINDOW;
1164
1232
  // `recent` is newest-first. Walk to the first boundary marker: an
1165
1233
  // assistant carrying a session token, or a compaction record. Both claim
@@ -1209,6 +1277,9 @@ export class ChatTurnHandler {
1209
1277
  systemHash: probeSession.systemHash,
1210
1278
  checkpoint: 1
1211
1279
  };
1280
+ if (probeSession.traceContentSuppressed) {
1281
+ priorSession.traceContentSuppressed = true;
1282
+ }
1212
1283
  // Absolute position to persist: prior prefix + the prior assistant + the
1213
1284
  // new turns — identical to what the full-load path would store.
1214
1285
  sessionCheckpointOverride =
@@ -1217,6 +1288,10 @@ export class ChatTurnHandler {
1217
1288
  const [rows] = await Message.paginate(threadId, { limit: 1000 });
1218
1289
  const full = convertDbMessages(historySinceCompaction(rows));
1219
1290
  full.unshift(systemChatMessage());
1291
+ const scope = getRunTraceScope();
1292
+ if (scope) {
1293
+ await registerRunTraceParents(userId, scope.runId, [...copiedMessageIds].map((id) => ({ kind: "message", id })));
1294
+ }
1220
1295
  return full;
1221
1296
  };
1222
1297
  }
@@ -1237,6 +1312,9 @@ export class ChatTurnHandler {
1237
1312
  priorSession = lastMatchingProviderSession(kept, providerId, model);
1238
1313
  }
1239
1314
  }
1315
+ if (priorSession?.traceContentSuppressed) {
1316
+ suppressRunTraceContent();
1317
+ }
1240
1318
  // Expose the read-only `run_search` fan-out primitive by default. A client
1241
1319
  // can opt out by sending `enable_read_only_search: false`.
1242
1320
  const enableReadOnlySearch = data.enable_read_only_search !== false;
@@ -1286,13 +1364,6 @@ export class ChatTurnHandler {
1286
1364
  // it starts (invariant I-2). A loop that made its own would hand every
1287
1365
  // nested agent a fresh allowance, which is no ceiling at all.
1288
1366
  const turnBudget = await createChatTurnBudget();
1289
- const gatedRun = (context) => createCapabilityRun({
1290
- context,
1291
- gate: chatGate,
1292
- projectId: chatProjectId,
1293
- availableSecrets: contextSecretAvailability(context),
1294
- budget: turnBudget
1295
- });
1296
1367
  // The user's external MCP servers (Blender, Hugging Face, …). Their tools
1297
1368
  // are plain belt tools named `mcp_<server>_<tool>`, so every loop reaches
1298
1369
  // them the same way; a server that does not answer is skipped, not fatal.
@@ -1305,40 +1376,48 @@ export class ChatTurnHandler {
1305
1376
  error: err instanceof Error ? err.message : String(err)
1306
1377
  });
1307
1378
  }
1308
- const rawToolbelt = [
1309
- ...getBuiltinTools(),
1310
- ...(googleWorkspace ? getGoogleWorkspaceTools() : []),
1311
- // Apify and SerpAPI have no `nodetool.*` namespace, so the belt is how
1312
- // a chat discovers them (`nodetool.searchTools("apify")`) at all.
1313
- ...getApifyTools(gatedRun),
1314
- ...getSerpApiTools(gatedRun),
1315
- ...externalMcpTools,
1316
- ...getAllMcpTools({
1317
- registry: this.session.nodeRegistry,
1318
- providers: chatProviders,
1319
- ...mcpToolHostDeps()
1320
- }),
1321
- toolForCapabilityName("list_collections"),
1322
- toolForCapabilityName("query_collection"),
1323
- runNodeTool
1379
+ const legacyCapabilities = [runNodeTool, ...externalMcpTools].map(capabilityFromTool);
1380
+ const nativeSpecs = [
1381
+ ...availableBuiltinToolNames().map((name) => capabilityForName(name).spec),
1382
+ ...(googleWorkspace ? GOOGLE_WORKSPACE_TOOL_NAMES : []).map((name) => capabilityForName(name).spec),
1383
+ ...[...APIFY_TOOL_NAMES, ...SERPAPI_TOOL_NAMES].map((name) => capabilityForName(name).spec),
1384
+ ...getAllMcpCapabilitySpecs({ registry: this.session.nodeRegistry, providers: chatProviders }),
1385
+ capabilityForName("list_collections").spec,
1386
+ capabilityForName("query_collection").spec
1324
1387
  ];
1325
- // De-duplicate by name (builtins / mcp / extras may overlap); first wins.
1326
- const dedupedToolbelt = [];
1327
- const seenToolNames = new Set();
1328
- for (const tool of rawToolbelt) {
1329
- if (seenToolNames.has(tool.name))
1330
- continue;
1331
- seenToolNames.add(tool.name);
1332
- dedupedToolbelt.push(tool);
1333
- }
1334
- // Wrap the toolbelt in the permission gate. The wrapper is transparent
1335
- // except for `process()`, so the chat loop AND any `run_subtask` child
1336
- // loop inherit gating by simply calling `tool.process()`.
1337
- const baseTools = gateTools(dedupedToolbelt, chatGate);
1388
+ const specsByName = new Map();
1389
+ for (const spec of [...nativeSpecs, ...legacyCapabilities.map((entry) => entry.spec)]) {
1390
+ if (!specsByName.has(spec.name)) {
1391
+ specsByName.set(spec.name, spec);
1392
+ }
1393
+ }
1394
+ const baseSpecs = [...specsByName.values()];
1395
+ const legacyByName = new Map(legacyCapabilities.map((entry) => [entry.spec.name, entry]));
1396
+ // Delegated executors still consume Tool[]. Adapt only at that boundary.
1397
+ // Each child context owns a separate run so its memory and depth are local.
1398
+ const childRuns = new WeakMap();
1399
+ const childRun = (context) => {
1400
+ let run = childRuns.get(context);
1401
+ if (!run) {
1402
+ run = createCapabilityRun({
1403
+ context, gate: chatGate, nodeRegistry: this.session.nodeRegistry,
1404
+ providers: chatProviders, budget: turnBudget,
1405
+ availableSecrets: contextSecretAvailability(context),
1406
+ ...mcpToolHostDeps(), capabilities: legacyCapabilities
1407
+ });
1408
+ childRuns.set(context, run);
1409
+ }
1410
+ return run;
1411
+ };
1412
+ const baseTools = baseSpecs.map((spec) => {
1413
+ const entry = legacyByName.get(spec.name)
1414
+ ?? capabilityForName(spec.name);
1415
+ return toolFromCapability(entry.spec, entry.impl, childRun);
1416
+ });
1338
1417
  // Inject the recursive-decomposition primitive (ungated — it spawns a
1339
1418
  // child loop whose own tools are the gated `baseTools`). Child events
1340
1419
  // stream back tagged with `parent_tool_call_id` so the UI can nest cards.
1341
- const serverTools = baseTools.slice();
1420
+ const serverTools = baseSpecs.slice();
1342
1421
  // The same runtime the delegation tools take, kept for the capability run
1343
1422
  // below: provider, model, the parent belt, and the event forwarder.
1344
1423
  let subAgentRuntime;
@@ -1384,18 +1463,9 @@ export class ChatTurnHandler {
1384
1463
  // `parent_tool_call_id` / `subtask_depth` tagging are unchanged.
1385
1464
  // `start_subtask` / `wait_subtasks` share the per-turn registry above:
1386
1465
  // spawn returns immediately, and the parent collects on its own terms.
1387
- const delegationRun = (context) => createCapabilityRun({
1388
- context,
1389
- // Ungated on purpose, as before: spawning a child loop has no side
1390
- // effect of its own, and the child's tools are the gated `baseTools`.
1391
- gate: UNGATED,
1392
- availableSecrets: contextSecretAvailability(context),
1393
- subAgent: subAgentRuntime,
1394
- budget: turnBudget
1395
- });
1396
- serverTools.unshift(toolForCapabilityName("run_subtask", delegationRun));
1397
- serverTools.unshift(toolForCapabilityName("start_subtask", delegationRun));
1398
- serverTools.unshift(toolForCapabilityName("wait_subtasks", delegationRun));
1466
+ serverTools.unshift(capabilityForName("run_subtask").spec);
1467
+ serverTools.unshift(capabilityForName("start_subtask").spec);
1468
+ serverTools.unshift(capabilityForName("wait_subtasks").spec);
1399
1469
  // Plan mode's own capability: `create_plan` runs the TaskPlanner over the
1400
1470
  // objective and streams the plan into the thread, executing nothing. It
1401
1471
  // is offered only here, because plan mode is the one place where a plan
@@ -1404,7 +1474,7 @@ export class ChatTurnHandler {
1404
1474
  // mode the turn started in — `liveMode` covers a mid-turn switch for the
1405
1475
  // gate, not for which tools were offered.
1406
1476
  if (permissionMode === "plan") {
1407
- serverTools.unshift(toolForCapabilityName("create_plan", delegationRun));
1477
+ serverTools.unshift(capabilityForName("create_plan").spec);
1408
1478
  }
1409
1479
  // The other half: `execute_plan` runs a plan the user has already seen,
1410
1480
  // on the same ParallelTaskExecutor `Agent` uses. It is on the belt in
@@ -1412,12 +1482,12 @@ export class ChatTurnHandler {
1412
1482
  // `blocked_in_plan_mode`, which tells the model to have the user switch
1413
1483
  // out, where dropping it from the belt entirely would only say the tool
1414
1484
  // does not exist. It is *offered* everywhere but plan mode, below.
1415
- serverTools.unshift(...gateTools([toolForCapabilityName("execute_plan", delegationRun)], chatGate));
1485
+ serverTools.unshift(capabilityForName("execute_plan").spec);
1416
1486
  // Read-only fan-out search (opt-in). Reuses the same runtime — the
1417
1487
  // capability filters the parent belt to its read-only allowlist
1418
1488
  // internally, so passing the full snapshot is correct.
1419
1489
  if (enableReadOnlySearch) {
1420
- serverTools.unshift(toolForCapabilityName("run_search", delegationRun));
1490
+ serverTools.unshift(capabilityForName("run_search").spec);
1421
1491
  }
1422
1492
  }
1423
1493
  const serverToolMap = new Map(serverTools.map((t) => [t.name, t]));
@@ -1426,7 +1496,7 @@ export class ChatTurnHandler {
1426
1496
  permissionMode,
1427
1497
  resolved: serverTools.map((t) => t.name)
1428
1498
  });
1429
- const serverSchemas = serverTools.map((t) => t.toProviderTool());
1499
+ const serverSchemas = serverTools.map((t) => capabilityProviderTool(t));
1430
1500
  // Every client tool the connected UI registered is exposed. They used to be
1431
1501
  // gated on an active workflow, which made the editor tools unreachable from
1432
1502
  // plain chat; they are deferred behind `nodetool.searchTools()` anyway, and each one now
@@ -1484,7 +1554,11 @@ export class ChatTurnHandler {
1484
1554
  workspace: chatWorkspace,
1485
1555
  authToken: this.deps.authToken()
1486
1556
  });
1557
+ ctx.runTraceContext = rootContext.runTraceContext;
1487
1558
  ctx.set("chat_message_id", turnMessage.id);
1559
+ if (ctx.runTraceContext) {
1560
+ await registerRunTraceParents(userId, ctx.runTraceContext.runId, [...copiedMessageIds].map((id) => ({ kind: "message", id })));
1561
+ }
1488
1562
  const detachPredictions = attachChatPredictionForwarder((listener) => ctx.addMessageListener(listener), (msg) => this.session.sendDetached(msg), { threadId: threadId || null, workflowId });
1489
1563
  // A chat turn that generates an image or a video spends real money without
1490
1564
  // ever constructing an ExecutionSession, so the ledger is attached here
@@ -1530,7 +1604,7 @@ export class ChatTurnHandler {
1530
1604
  budget: turnBudget,
1531
1605
  secretPrompt: (request) => this.requestSecretEntry(threadId, request),
1532
1606
  ...mcpToolHostDeps(),
1533
- capabilities: [capabilityFromTool(runNodeTool)]
1607
+ capabilities: legacyCapabilities
1534
1608
  });
1535
1609
  const gameAssetRun = this.chatCapabilityRun;
1536
1610
  const gameAssetNodeRunner = (args) => gameAssetRun.invoke("run_node", args);
@@ -1632,9 +1706,20 @@ export class ChatTurnHandler {
1632
1706
  // What to persist onto the assistant message: the provider's token, but with
1633
1707
  // the absolute checkpoint when the fast path sent only a delta (the
1634
1708
  // provider's emitted checkpoint is relative to the trimmed view).
1635
- const sessionForPersist = () => sessionCheckpointOverride != null && capturedSession
1636
- ? { ...capturedSession, checkpoint: sessionCheckpointOverride }
1637
- : capturedSession;
1709
+ const sessionForPersist = () => {
1710
+ if (!capturedSession) {
1711
+ return null;
1712
+ }
1713
+ const contentSuppressed = getRunTraceScope()?.policy.contentSuppressed || priorSession?.traceContentSuppressed || capturedSession.traceContentSuppressed;
1714
+ const persisted = { ...capturedSession };
1715
+ if (sessionCheckpointOverride != null) {
1716
+ persisted.checkpoint = sessionCheckpointOverride;
1717
+ }
1718
+ if (contentSuppressed) {
1719
+ persisted.traceContentSuppressed = true;
1720
+ }
1721
+ return persisted;
1722
+ };
1638
1723
  // Cap on tool-calling rounds before the loop stops. Generous enough to
1639
1724
  // build a multi-component app UI or run a long edit session in one turn —
1640
1725
  // 10 was too low and cut off the app builder mid-build.
@@ -1715,9 +1800,14 @@ export class ChatTurnHandler {
1715
1800
  }
1716
1801
  else if (serverTool) {
1717
1802
  try {
1718
- toolResult = await Tool.executeTool(serverTool, ctx, toolCall.args, {
1719
- toolCallId: toolCall.id
1720
- });
1803
+ const args = { ...Tool.stripMessage(toolCall.args) };
1804
+ if (serverTool.needsToolCallId) {
1805
+ args["_tool_call_id"] = toolCall.id;
1806
+ }
1807
+ if (!this.chatCapabilityRun) {
1808
+ throw new Error("Chat capability run is unavailable");
1809
+ }
1810
+ toolResult = await this.chatCapabilityRun.invoke(serverTool.name, args);
1721
1811
  }
1722
1812
  catch (err) {
1723
1813
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -2020,6 +2110,7 @@ export class ChatTurnHandler {
2020
2110
  role: "user",
2021
2111
  execution_event_type: COMPACTION_EVENT_TYPE,
2022
2112
  content: compactionMessageContent(summary),
2113
+ tools: getRunTraceScope()?.policy.contentSuppressed ? restrictedTraceToolNames() : null,
2023
2114
  created_at: Number.isFinite(boundary)
2024
2115
  ? new Date(boundary - 1).toISOString()
2025
2116
  : cut.keep[0].created_at
@@ -2260,6 +2351,7 @@ export class ChatTurnHandler {
2260
2351
  return;
2261
2352
  }
2262
2353
  const errMsg = err instanceof Error ? err.message : String(err);
2354
+ await settleRegisteredRunTrace(rootContext, "failed", errMsg);
2263
2355
  log.error("Chat processing error", { threadId, error: errMsg });
2264
2356
  let errorType = "error";
2265
2357
  let statusCode;
@@ -3076,6 +3168,11 @@ export class ChatTurnHandler {
3076
3168
  if (cancelled())
3077
3169
  return;
3078
3170
  const errMsg = err instanceof Error ? err.message : String(err);
3171
+ const scope = getRunTraceScope();
3172
+ if (scope) {
3173
+ trace.getActiveSpan()?.setStatus({ code: SpanStatusCode.ERROR });
3174
+ await settleRunTrace(userId, scope.runId, { status: "failed", error: scope.policy.contentSuppressed ? "Execution failed" : errMsg, secretValues: scope.secretValues, contentSuppressed: scope.policy.contentSuppressed });
3175
+ }
3079
3176
  log.error("Media generation error", { threadId, mode, error: errMsg });
3080
3177
  const failure = providerFailureMessage(err, providerId, modelId);
3081
3178
  const failureMessage = `Generation failed: ${failure.message}`;
@@ -3160,6 +3257,11 @@ export class ChatTurnHandler {
3160
3257
  "messages";
3161
3258
  // Build chat history for params — matches Python
3162
3259
  const [dbMessages] = await Message.paginate(threadId, { limit: 1000 });
3260
+ const inheritedTrace = getRunTraceScope();
3261
+ seedStoredToolProvenance(dbMessages);
3262
+ if (inheritedTrace) {
3263
+ await registerRunTraceParents(userId, inheritedTrace.runId, dbMessages.map((m) => ({ kind: "message", id: m.id })));
3264
+ }
3163
3265
  const chatHistorySerialized = dbMessages.map((m) => ({
3164
3266
  role: m.role,
3165
3267
  content: m.content,
@@ -3207,6 +3309,7 @@ export class ChatTurnHandler {
3207
3309
  workspace,
3208
3310
  assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url"
3209
3311
  });
3312
+ context.runTraceContext = getRunTraceScope() ?? null;
3210
3313
  // Expose executor/node-type resolution for sub-workflow nodes
3211
3314
  context.setResolveExecutor((node) => this.session.resolveExecutor(node));
3212
3315
  if (this.session.resolveNodeType) {
@@ -3215,6 +3318,8 @@ export class ChatTurnHandler {
3215
3318
  : this.session.resolveNodeType;
3216
3319
  context.setResolveNodeType((nodeType) => resolverObj.resolveNodeType(nodeType));
3217
3320
  }
3321
+ await Job.create({ id: jobId, workflow_id: workflowId, user_id: userId, status: "running", params, graph });
3322
+ await registerWorkflowRunTrace(context, { jobId, workflowId });
3218
3323
  // Create and run workflow (A5: via the ExecutionSession facade — see
3219
3324
  // the identical note in `startJobInner`).
3220
3325
  const session = await ExecutionSession.create({
@@ -3249,20 +3354,6 @@ export class ChatTurnHandler {
3249
3354
  }
3250
3355
  };
3251
3356
  this.deps.jobs.registerJob(jobId, active);
3252
- // Persist job to DB (best-effort)
3253
- try {
3254
- await Job.create({
3255
- id: jobId,
3256
- workflow_id: workflowId,
3257
- user_id: userId,
3258
- status: "running",
3259
- params,
3260
- graph
3261
- });
3262
- }
3263
- catch (error) {
3264
- this.session.logError("workflow job persistence failed", error);
3265
- }
3266
3357
  // The run already started inside `ExecutionSession.create()` above.
3267
3358
  const executePromise = session.result;
3268
3359
  // Stream events, collect output_update results
@@ -3441,6 +3532,11 @@ export class ChatTurnHandler {
3441
3532
  }
3442
3533
  catch (err) {
3443
3534
  const errMsg = err instanceof Error ? err.message : String(err);
3535
+ const scope = getRunTraceScope();
3536
+ if (scope) {
3537
+ trace.getActiveSpan()?.setStatus({ code: SpanStatusCode.ERROR });
3538
+ await settleRunTrace(userId, scope.runId, { status: "failed", error: scope.policy.contentSuppressed ? "Execution failed" : errMsg, secretValues: scope.secretValues, contentSuppressed: scope.policy.contentSuppressed });
3539
+ }
3444
3540
  log.error("Workflow message error", {
3445
3541
  threadId,
3446
3542
  workflowId,