@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,6 @@
1
+ import { ROOT_CONTEXT, propagation, trace, isSpanContextValid } from "@opentelemetry/api";
2
+ import { getRegisteredRunTrace } from "@nodetool-ai/models";
3
+ import { ensureRunTraceTelemetry, openAppRunTrace, registerWorkflowRunTrace } from "@nodetool-ai/execution";
1
4
  /**
2
5
  * Everything between "run_job arrived" and "terminal status persisted": the
3
6
  * admission gates, the concurrency queue, run start-up, the message stream,
@@ -9,7 +12,9 @@
9
12
  * {@link JobExecutionDeps}.
10
13
  */
11
14
  import { randomUUID } from "node:crypto";
15
+ import { operationTarget, parseInputStateKey } from "@nodetool-ai/app-runtime";
12
16
  import { ApiErrorCode } from "../error-codes.js";
17
+ import { captureError } from "../error-traces.js";
13
18
  import { admitSpend, releaseSpend, reserveSpend } from "../credit-gate.js";
14
19
  import { JobConcurrencyQueue } from "../job-queue.js";
15
20
  import { isBoolean, isFiniteNumber, isFunctionValue, isNonEmptyString, isNumber, isObjectLike, isRecord, isString } from "../lib/wire-values.js";
@@ -18,17 +23,19 @@ import { ExecutionSession, isExecutionPreflightError, isUnitBilledCapability, pr
18
23
  import { persistableOutputs, withoutInlineAssetBytes } from "@nodetool-ai/execution/service";
19
24
  import { createRunSupervisor } from "../run-supervisor.js";
20
25
  import { jobRunRegistry } from "../job-run-registry.js";
21
- import { Application, Asset, Job, listApplicationVersions, invocationIdInUse, releasedApplicationRelease, releasedApplicationVersion, reserveInvocation, settleInvocation, Workflow } from "@nodetool-ai/models";
26
+ import { Application, AppRunError, claimAppRun, ensureDefaultAppInstance, findAppRunByInvocation, getAppInstance, getAppRun, Asset, Job, listApplicationVersions, invocationIdInUse, releasedApplicationRelease, releasedApplicationVersion, reconcileAppRunCost, reserveInvocation, reserveAppRun, setAppRunInputs, settleAppRun, updateAppRunEstimate, settleInvocation, Workflow } from "@nodetool-ai/models";
22
27
  import { getInstanceId } from "../lib/instance-id.js";
23
28
  import { requestRemoteJobCancel } from "../job-control.js";
24
29
  import { estimateWorkflowCost, nodeExpectedQuantity } from "@nodetool-ai/node-sdk/cost-estimate";
25
30
  import { extractPricingParams } from "@nodetool-ai/node-sdk/pricing-params";
26
31
  import { getModelUnitPrice } from "@nodetool-ai/model-pricing";
27
32
  import { applicationReleaseResponse } from "@nodetool-ai/protocol/api-schemas/applications.js";
33
+ import { appRunSnapshot } from "@nodetool-ai/protocol/api-schemas/app-runs.js";
28
34
  import { confineRunRequest, isRunRefusal } from "../lib/app-session-scope.js";
29
35
  import { releaseBlockedReason } from "../lib/app-deployment-service.js";
30
36
  import { autoSaveAssets, primaryTextOutputName } from "./asset-autosave.js";
31
37
  import { createRuntimeContext } from "./model-interfaces.js";
38
+ import { createAppRunFold } from "./app-run-fold.js";
32
39
  import { formatSanitizedError, sanitizeLargeText } from "./sanitize.js";
33
40
  import { createLogger } from "@nodetool-ai/config";
34
41
  const log = createLogger("nodetool.websocket.runner");
@@ -374,7 +381,19 @@ export class JobExecutionManager {
374
381
  */
375
382
  async emitBeforeRunFailure(jobId, workflowId, err, persistJob) {
376
383
  const errorMessage = err instanceof Error ? err.message : String(err);
384
+ const appRun = await findAppRunByInvocation(this.session.requireUserId(), jobId);
385
+ if (appRun) {
386
+ await settleAppRun(this.session.requireUserId(), appRun.id, {
387
+ status: "failed",
388
+ error: errorMessage
389
+ });
390
+ }
377
391
  this.session.logError("beforeRunJob failed", err);
392
+ captureError(err, {
393
+ source: "job",
394
+ userId: this.session.userId,
395
+ context: { job_id: jobId, workflow_id: workflowId ?? "" }
396
+ });
378
397
  await this.session.send({
379
398
  type: "job_update",
380
399
  status: "failed",
@@ -589,6 +608,9 @@ export class JobExecutionManager {
589
608
  * telemetry, which is the same ledger.
590
609
  */
591
610
  async admitApplicationRun(req) {
611
+ if (req.app_run_id || this.session.appSession) {
612
+ return this.admitDurableAppRun(req);
613
+ }
592
614
  const applicationId = req.application_id;
593
615
  if (!applicationId)
594
616
  return true;
@@ -712,6 +734,144 @@ export class JobExecutionManager {
712
734
  }
713
735
  return true;
714
736
  }
737
+ async admitDurableAppRun(req) {
738
+ const userId = this.session.requireUserId();
739
+ const jobId = req.job_id ?? randomUUID();
740
+ req.job_id = jobId;
741
+ let claimedRunId = null;
742
+ try {
743
+ if (this.session.appSession && req.application_id) {
744
+ const release = await releasedApplicationRelease(req.application_id, userId);
745
+ if (!release) {
746
+ throw new Error("Application release not found");
747
+ }
748
+ const instance = await ensureDefaultAppInstance({
749
+ userId,
750
+ applicationId: req.application_id,
751
+ sourceId: `public:${req.application_id}:${release.version}`,
752
+ version: release.version,
753
+ name: "Visitor runs",
754
+ snapshot: appRunSnapshot.parse({
755
+ document: release.document,
756
+ workflow_graphs: Object.fromEntries(release.workflows.flatMap((item) => item.graph ? [[item.workflowId, item.graph]] : [])),
757
+ script_documents: {}
758
+ })
759
+ });
760
+ const decision = await reserveAppRun({
761
+ userId,
762
+ instanceId: instance.id,
763
+ invocationId: jobId,
764
+ operationId: req.operation_id ?? "",
765
+ origin: "public",
766
+ estimatedUsd: this.estimateRunCost(req),
767
+ requireFiniteBudget: true
768
+ });
769
+ if (!decision.allowed) {
770
+ return this.refuseRun(req, jobId, ApiErrorCode.BUDGET_EXCEEDED, decision.reason);
771
+ }
772
+ req.app_run_id = decision.run.id;
773
+ req.instance_id = instance.id;
774
+ }
775
+ const run = req.app_run_id
776
+ ? await getAppRun(userId, req.app_run_id)
777
+ : null;
778
+ const instance = req.instance_id
779
+ ? await getAppInstance(userId, req.instance_id)
780
+ : null;
781
+ if (!run ||
782
+ !instance ||
783
+ run.instance_id !== instance.id ||
784
+ run.invocation_id !== jobId) {
785
+ return this.refuseRun(req, jobId, ApiErrorCode.NOT_FOUND, "App run not found");
786
+ }
787
+ if (run.status !== "running") {
788
+ return this.refuseRun(req, jobId, ApiErrorCode.ALREADY_EXISTS, "App run has already finished");
789
+ }
790
+ const operation = instance.snapshot.document.operations.find((item) => item.id === run.operation_id);
791
+ if (!operation ||
792
+ operationTarget(operation).kind !== "workflow" ||
793
+ (req.operation_id && req.operation_id !== run.operation_id)) {
794
+ return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, "Operation does not match this run");
795
+ }
796
+ const target = operationTarget(operation);
797
+ if (target.kind !== "workflow" ||
798
+ !target.workflowId ||
799
+ (req.workflow_id && req.workflow_id !== target.workflowId)) {
800
+ return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, "Workflow does not match this run");
801
+ }
802
+ if (run.origin !== "public") {
803
+ const frozen = run.snapshot?.workflow_graphs[target.workflowId];
804
+ if (!frozen) {
805
+ return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, "Run has no frozen workflow");
806
+ }
807
+ const overlays = new Map();
808
+ const inputs = instance.variables["__app_inputs"];
809
+ for (const [key, value] of Object.entries(isRecord(inputs) ? inputs : {})) {
810
+ const binding = parseInputStateKey(key);
811
+ if (!binding?.property || binding.operationId !== run.operation_id) {
812
+ continue;
813
+ }
814
+ const properties = overlays.get(binding.nodeId) ?? {};
815
+ properties[binding.property] = value;
816
+ overlays.set(binding.nodeId, properties);
817
+ }
818
+ req.graph = {
819
+ ...frozen,
820
+ nodes: frozen.nodes.map((node) => {
821
+ const overlay = overlays.get(node.id);
822
+ if (!overlay) {
823
+ return node;
824
+ }
825
+ const properties = isRecord(node.properties)
826
+ ? node.properties
827
+ : isRecord(node.data)
828
+ ? node.data
829
+ : {};
830
+ return { ...node, properties: { ...properties, ...overlay } };
831
+ })
832
+ };
833
+ await updateAppRunEstimate(userId, run.id, this.estimateRunCost(req));
834
+ }
835
+ if (!(await claimAppRun(userId, run.id, getInstanceId()))) {
836
+ return this.refuseRun(req, jobId, ApiErrorCode.ALREADY_EXISTS, "App run execution already started");
837
+ }
838
+ claimedRunId = run.id;
839
+ await setAppRunInputs(userId, run.id, req.params ?? {});
840
+ req.app_run_id = run.id;
841
+ req.instance_id = instance.id;
842
+ req.user_id = userId;
843
+ req.workflow_id = target.workflowId;
844
+ req.application_id = run.application_id;
845
+ req.application_version = run.version;
846
+ req.operation_id = run.operation_id;
847
+ return true;
848
+ }
849
+ catch (error) {
850
+ if (claimedRunId) {
851
+ try {
852
+ await settleAppRun(userId, claimedRunId, {
853
+ status: "failed",
854
+ actualUsd: 0,
855
+ knownLlmUsd: 0,
856
+ error: error instanceof Error
857
+ ? error.message
858
+ : "App run admission failed"
859
+ });
860
+ }
861
+ catch (settlementError) {
862
+ if (!(settlementError instanceof AppRunError) ||
863
+ settlementError.code !== "not_found") {
864
+ this.session.logError("app run admission settlement failed", settlementError);
865
+ }
866
+ }
867
+ }
868
+ this.session.logError("app run admission failed", error);
869
+ const code = error instanceof AppRunError && error.code === "budget_exceeded"
870
+ ? ApiErrorCode.BUDGET_EXCEEDED
871
+ : ApiErrorCode.INVALID_INPUT;
872
+ return this.refuseRun(req, jobId, code, error instanceof Error ? error.message : "App run admission failed");
873
+ }
874
+ }
715
875
  /**
716
876
  * Gate a run on the user's credit balance — but only the part of the run
717
877
  * that spends through NodeTool's managed provider. A graph with no
@@ -808,8 +968,15 @@ export class JobExecutionManager {
808
968
  req._accepted_at_ms ??= performance.now();
809
969
  if (!(await this.admitApplicationRun(req)))
810
970
  return;
811
- if (!(await this.admitCreditRun(req)))
971
+ if (!(await this.admitCreditRun(req))) {
972
+ if (req.app_run_id) {
973
+ await settleAppRun(this.session.requireUserId(), req.app_run_id, {
974
+ status: "failed",
975
+ error: "Run refused by spending limit"
976
+ });
977
+ }
812
978
  return;
979
+ }
813
980
  const max = await this.deps.getMaxConcurrentJobs();
814
981
  const perWorkflowMax = await this.perWorkflowLimitFor(req);
815
982
  // Queue the run when over the global cap, or when this workflow already has
@@ -949,6 +1116,13 @@ export class JobExecutionManager {
949
1116
  try {
950
1117
  await this.startJobInner(req, releaseSlot);
951
1118
  }
1119
+ catch (error) {
1120
+ if (req.app_run_id) {
1121
+ await this.emitBeforeRunFailure(req.job_id ?? "", req.workflow_id ?? null, error, true);
1122
+ return;
1123
+ }
1124
+ throw error;
1125
+ }
952
1126
  finally {
953
1127
  // Safety net: if startJobInner returned/threw without registering, the
954
1128
  // slot is freed so it doesn't leak and permanently shrink the cap.
@@ -1011,11 +1185,23 @@ export class JobExecutionManager {
1011
1185
  const workspace = this.session.workspaceResolver
1012
1186
  ? await this.session.workspaceResolver(workflowId ?? null, userId)
1013
1187
  : null;
1188
+ const appRun = req.app_run_id
1189
+ ? await getAppRun(userId, req.app_run_id)
1190
+ : null;
1014
1191
  const context = createRuntimeContext({
1015
1192
  jobId,
1016
1193
  workflowId,
1017
1194
  projectId,
1018
1195
  userId,
1196
+ ...(appRun && {
1197
+ appRunContext: {
1198
+ userId,
1199
+ appRunId: appRun.id,
1200
+ instanceId: appRun.instance_id,
1201
+ traceId: appRun.trace_id,
1202
+ origin: appRun.origin
1203
+ }
1204
+ }),
1019
1205
  workspace,
1020
1206
  assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url",
1021
1207
  persistOutputAssets: executionOptions.assetPersistence === "auto"
@@ -1044,6 +1230,9 @@ export class JobExecutionManager {
1044
1230
  const existing = await Job.get(jobId);
1045
1231
  if (existing) {
1046
1232
  if (existing.status === "cancelled") {
1233
+ if (appRun) {
1234
+ await settleAppRun(userId, appRun.id, { status: "cancelled" });
1235
+ }
1047
1236
  log.info("Skipping start of cancelled job", { jobId });
1048
1237
  // Nothing was registered in activeJobs yet — free the reserved
1049
1238
  // slot and promote any queued run, matching every other slot
@@ -1134,11 +1323,27 @@ export class JobExecutionManager {
1134
1323
  // uses: a bare throw reaches handleCommand as a generic `invalid_command`
1135
1324
  // the UI never associates with the job, so the run appears to spin
1136
1325
  // forever instead of failing with the reason.
1326
+ let finishAppTrace;
1327
+ try {
1328
+ await ensureRunTraceTelemetry();
1329
+ const incomingParent = req.traceparent ? trace.getSpanContext(propagation.extract(ROOT_CONTEXT, { traceparent: req.traceparent })) : undefined;
1330
+ const acceptedParent = incomingParent && isSpanContextValid(incomingParent) && !this.session.appSession && appRun && incomingParent.traceId === appRun.trace_id && await getRegisteredRunTrace(userId, incomingParent.traceId, appRun.id) ? incomingParent : undefined;
1331
+ finishAppTrace = appRun ? await openAppRunTrace(context, appRun, acceptedParent) : undefined;
1332
+ if (executionOptions.persistence === "job") {
1333
+ await registerWorkflowRunTrace(context, { jobId, workflowId: workflowId ?? null, inlineGraph: Boolean(req.graph), origin: this.session.appSession ? "public" : "ui" });
1334
+ }
1335
+ }
1336
+ catch (error) {
1337
+ await finishAppTrace?.("failed", error instanceof Error ? error.message : String(error));
1338
+ await this.emitBeforeRunFailure(jobId, workflowId, error, executionOptions.persistence === "job");
1339
+ return;
1340
+ }
1137
1341
  let session;
1138
1342
  try {
1139
1343
  session = await ExecutionSession.create(sessionOptions);
1140
1344
  }
1141
1345
  catch (err) {
1346
+ await finishAppTrace?.("failed", err instanceof Error ? err.message : String(err));
1142
1347
  if (!isExecutionPreflightError(err))
1143
1348
  throw err;
1144
1349
  await this.emitBeforeRunFailure(jobId, workflowId, err, executionOptions.persistence === "job");
@@ -1163,7 +1368,9 @@ export class JobExecutionManager {
1163
1368
  persistenceMs,
1164
1369
  kernelStartedAt: performance.now()
1165
1370
  },
1166
- applicationId: req.application_id ?? null
1371
+ applicationId: req.application_id ?? null,
1372
+ ...(appRun && { appRunFold: createAppRunFold(appRun) }),
1373
+ ...(finishAppTrace && { finishAppTrace })
1167
1374
  };
1168
1375
  // Decouple the run from this socket: from here on every frame carrying
1169
1376
  // this job_id is stamped with `job_seq` and buffered, so a client that
@@ -1304,6 +1511,16 @@ export class JobExecutionManager {
1304
1511
  }
1305
1512
  else if (active.status === "failed") {
1306
1513
  job.markFailed(active.error ?? "Unknown error");
1514
+ // The run's error is a string by now. A stack built here would
1515
+ // point at this method, not at the failure, so none is sent.
1516
+ captureError(active.error ?? "Unknown error", {
1517
+ source: "job",
1518
+ userId: job.user_id,
1519
+ context: {
1520
+ job_id: active.jobId,
1521
+ workflow_id: active.workflowId ?? ""
1522
+ }
1523
+ });
1307
1524
  }
1308
1525
  else if (active.status === "cancelled") {
1309
1526
  job.markCancelled();
@@ -1327,6 +1544,38 @@ export class JobExecutionManager {
1327
1544
  * the estimate standing rather than handing the spend back. Never throws.
1328
1545
  */
1329
1546
  async settleApplicationInvocation(active) {
1547
+ await active.finishAppTrace?.(active.status === "failed" ? "failed" : active.status === "cancelled" ? "cancelled" : "completed", active.error, this.runMeasuredCost(active));
1548
+ const identity = active.context.appRunContext;
1549
+ if (identity) {
1550
+ try {
1551
+ const settlement = {
1552
+ status: active.status === "failed"
1553
+ ? "failed"
1554
+ : active.status === "cancelled"
1555
+ ? "cancelled"
1556
+ : "completed",
1557
+ error: active.context.runTraceContext?.policy.contentSuppressed && active.error ? "Execution failed" : active.error ?? null,
1558
+ actualUsd: this.runMeasuredCost(active),
1559
+ outputs: active.appRunFold?.outputs() ?? {},
1560
+ documents: active.context.getAppRunDocuments(),
1561
+ contentSuppressed: active.context.runTraceContext?.policy.contentSuppressed,
1562
+ secretValues: [...active.context.getResolvedSecretValues()]
1563
+ };
1564
+ const llmCost = active.context.getAppRunLlmCost();
1565
+ if (llmCost !== null) {
1566
+ settlement.knownLlmUsd = llmCost;
1567
+ }
1568
+ await settleAppRun(identity.userId, identity.appRunId, settlement);
1569
+ await reconcileAppRunCost(identity.userId, identity.appRunId);
1570
+ return;
1571
+ }
1572
+ catch (error) {
1573
+ if (!(error instanceof AppRunError) || error.code !== "not_found") {
1574
+ this.session.logError("app run settlement failed", error);
1575
+ }
1576
+ }
1577
+ return;
1578
+ }
1330
1579
  if (!active.applicationId)
1331
1580
  return;
1332
1581
  try {
@@ -1587,18 +1836,25 @@ export class JobExecutionManager {
1587
1836
  continue;
1588
1837
  }
1589
1838
  const status = outbound.type === "job_update" ? String(outbound.status ?? "") : "";
1590
- const suppressProvisionalCompletion = active.requireTerminalResult &&
1591
- status === "completed" &&
1592
- outbound.result === undefined;
1839
+ const suppressProvisionalCompletion = (Boolean(active.context.appRunContext) &&
1840
+ TERMINAL_JOB_STATUSES.includes(status)) ||
1841
+ (active.requireTerminalResult &&
1842
+ status === "completed" &&
1843
+ outbound.result === undefined);
1593
1844
  if (!suppressProvisionalCompletion) {
1594
1845
  if (outbound.type === "job_update") {
1595
1846
  if (status === "running") {
1596
1847
  runningSeen = true;
1597
1848
  }
1598
1849
  else if (TERMINAL_JOB_STATUSES.includes(status)) {
1850
+ if (active.context.appRunContext) {
1851
+ await executionSettled;
1852
+ await this.settleApplicationInvocation(active);
1853
+ }
1599
1854
  await ensureRunningFrame();
1600
1855
  }
1601
1856
  }
1857
+ active.appRunFold?.fold(outbound);
1602
1858
  await this.session.send(outbound);
1603
1859
  }
1604
1860
  if (outbound.type === "job_update" && !suppressProvisionalCompletion) {
@@ -1620,12 +1876,30 @@ export class JobExecutionManager {
1620
1876
  if (Object.keys(finalOutputs).length > 0) {
1621
1877
  finalOutputs = await this.normalizeFinalOutputs(active, finalOutputs);
1622
1878
  }
1879
+ if (active.appRunFold) {
1880
+ for (const [key, values] of Object.entries(finalOutputs)) {
1881
+ const nodeId = this.resolveOutputNodeForKey(active, key)?.id ?? key;
1882
+ for (const [index, value] of values.entries()) {
1883
+ active.appRunFold.fold({
1884
+ type: "output_update",
1885
+ job_id: active.jobId,
1886
+ node_id: nodeId,
1887
+ value,
1888
+ disposition: index === 0 ? "replace" : "append",
1889
+ done: true
1890
+ });
1891
+ }
1892
+ }
1893
+ }
1623
1894
  if (active.executionOptions.eventDetail !== "terminal" &&
1624
1895
  !outputUpdateSeen &&
1625
1896
  Object.keys(finalOutputs).length > 0) {
1626
1897
  await this.sendOutputUpdates(active, finalOutputs);
1627
1898
  }
1628
1899
  const relayCompletedAt = performance.now();
1900
+ if (active.context.appRunContext) {
1901
+ await this.settleApplicationInvocation(active);
1902
+ }
1629
1903
  if (!terminalSeen ||
1630
1904
  (!terminalWithResultSeen && Object.keys(finalOutputs).length > 0)) {
1631
1905
  await ensureRunningFrame();
@@ -1706,7 +1980,7 @@ export class JobExecutionManager {
1706
1980
  // indistinguishable from a missing one — it must be neither reported
1707
1981
  // nor settled below.
1708
1982
  const row = (await Job.get(jobId));
1709
- const job = row && row.user_id === (this.session.requireUserId()) ? row : null;
1983
+ const job = row && row.user_id === this.session.requireUserId() ? row : null;
1710
1984
  const settled = job != null &&
1711
1985
  (job.status === "completed" ||
1712
1986
  job.status === "failed" ||
@@ -1781,6 +2055,11 @@ export class JobExecutionManager {
1781
2055
  // and tell the client it's cancelled before it ever starts.
1782
2056
  const queued = this.jobQueue.remove(jobId);
1783
2057
  if (queued) {
2058
+ if (queued.app_run_id) {
2059
+ await settleAppRun(this.session.requireUserId(), queued.app_run_id, {
2060
+ status: "cancelled"
2061
+ });
2062
+ }
1784
2063
  releaseSpend(this.session.requireUserId(), jobId);
1785
2064
  const cancelledWorkflowId = queued.workflow_id ?? workflowId ?? null;
1786
2065
  // Mark the persisted queued row cancelled so it leaves the queue in
@@ -2062,6 +2341,11 @@ export class JobExecutionManager {
2062
2341
  if (!queuedId)
2063
2342
  continue;
2064
2343
  try {
2344
+ if (queued.app_run_id) {
2345
+ await settleAppRun(this.session.requireUserId(), queued.app_run_id, {
2346
+ status: "cancelled"
2347
+ });
2348
+ }
2065
2349
  const job = await Job.get(queuedId);
2066
2350
  if (job) {
2067
2351
  job.markCancelled();
@@ -2074,6 +2358,12 @@ export class JobExecutionManager {
2074
2358
  }
2075
2359
  for (const dequeuedId of this.dequeuedJobs) {
2076
2360
  try {
2361
+ const run = await findAppRunByInvocation(this.session.requireUserId(), dequeuedId);
2362
+ if (run) {
2363
+ await settleAppRun(this.session.requireUserId(), run.id, {
2364
+ status: "cancelled"
2365
+ });
2366
+ }
2077
2367
  const job = await Job.get(dequeuedId);
2078
2368
  if (job) {
2079
2369
  job.markCancelled();