deepline 0.3.89 → 0.3.90

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 (211) hide show
  1. package/contracts/install-commands.json +29 -0
  2. package/dist/bundling-sources/sdk/src/client.ts +120 -37
  3. package/dist/bundling-sources/sdk/src/index.ts +4 -0
  4. package/dist/bundling-sources/sdk/src/pagination.ts +25 -0
  5. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  6. package/dist/bundling-sources/sdk/src/release.ts +16 -9
  7. package/dist/bundling-sources/sdk/src/types.ts +23 -0
  8. package/dist/bundling-sources/shared_libs/monitors/analytics-public.ts +10 -0
  9. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +4 -4
  11. package/dist/bundling-sources/shared_libs/monitors/inventory-reconciliation.ts +59 -0
  12. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +7 -9
  13. package/dist/bundling-sources/shared_libs/monitors/package.json +7 -0
  14. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +44 -2
  15. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +2 -2
  16. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +2 -2
  17. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +1 -1
  18. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +245 -0
  19. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-message.ts +38 -0
  20. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-ack-probe.ts +94 -0
  21. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-delivery.ts +60 -0
  22. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue.ts +60 -0
  23. package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +1 -1
  24. package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +2 -2
  25. package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +1 -1
  26. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +1 -1
  27. package/dist/bundling-sources/shared_libs/observability/build-context.ts +13 -0
  28. package/dist/bundling-sources/shared_libs/observability/log-metadata.ts +160 -0
  29. package/dist/bundling-sources/shared_libs/observability/request-context.ts +68 -0
  30. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +654 -0
  31. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +35 -0
  32. package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +5 -0
  33. package/dist/bundling-sources/shared_libs/play-data-plane/neon-constants.ts +9 -0
  34. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +272 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +66 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +0 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/bounceban-batching.ts +137 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +779 -140
  40. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +46 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/daytona-payload-contract.ts +68 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +103 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +725 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +29 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +23 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +442 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +3 -3
  48. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +18 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +9 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +1 -1
  51. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +1 -1
  52. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +66 -0
  53. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +145 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/ports.ts +21 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/postgres-store.ts +170 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +1322 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/internal-step-ids.ts +2 -2
  58. package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +1 -1
  59. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +112 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +8 -0
  61. package/dist/bundling-sources/shared_libs/play-runtime/perf-trace.ts +127 -0
  62. package/dist/bundling-sources/shared_libs/play-runtime/play-artifact-runtime-contract.ts +5 -0
  63. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +24 -1
  64. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +56 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +13 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/job.ts +41 -0
  67. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/ports.ts +17 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +54 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/contracts.ts +114 -0
  70. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/engine-suspension.ts +108 -0
  71. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/index.ts +36 -0
  72. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/job.ts +97 -0
  73. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/ports.ts +104 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/postgres-fence-adapter.ts +263 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +64 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/contracts.ts +29 -0
  77. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +56 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/index.ts +15 -0
  79. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/job.ts +65 -0
  80. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/ports.ts +17 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/contracts.ts +42 -0
  82. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/index.ts +17 -0
  83. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +142 -0
  84. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/ports.ts +42 -0
  85. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/postgres-adapter.ts +102 -0
  86. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +60 -0
  87. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/index.ts +16 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +78 -0
  89. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +54 -0
  90. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +71 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +4 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +5 -8
  94. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +3 -5
  95. package/dist/bundling-sources/shared_libs/play-runtime/run-identity.ts +168 -0
  96. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +72 -9
  97. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +6 -12
  98. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +19 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +0 -1
  100. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +14 -44
  101. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +57 -205
  102. package/dist/bundling-sources/shared_libs/play-runtime/runtime-authority.ts +201 -0
  103. package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +6 -0
  104. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +67 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/runtime-release-environment.ts +2 -0
  106. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-contract.ts +68 -0
  107. package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +14 -2
  108. package/dist/bundling-sources/shared_libs/play-runtime/runtime-telemetry.ts +108 -0
  109. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +4 -0
  110. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +3 -0
  111. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  112. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +88 -0
  113. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +124 -0
  114. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-run-task.ts +68 -0
  115. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool.ts +193 -0
  116. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-lifecycle.ts +269 -0
  117. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-suffix.ts +73 -0
  118. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +6642 -0
  119. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +897 -0
  120. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-metrics.ts +120 -0
  121. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +76 -0
  122. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +476 -0
  123. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +47 -0
  124. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +147 -0
  125. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/index.ts +27 -0
  126. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/lazy.ts +50 -0
  127. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/play-run-recovery.ts +98 -0
  128. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-budget-state.ts +260 -0
  129. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +1416 -0
  130. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +1255 -0
  131. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +942 -0
  132. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +14636 -0
  133. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +888 -0
  134. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +355 -0
  135. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +46 -0
  136. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runner-push.ts +60 -0
  137. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-api-origin.ts +33 -0
  138. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-gateway-health.ts +64 -0
  139. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +835 -0
  140. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-ledger-reconciliation.ts +1218 -0
  141. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-operation.ts +207 -0
  142. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-scheduler-schema.ts +10 -0
  143. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-capacity.ts +123 -0
  144. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-launch-read-observability.ts +78 -0
  145. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +169 -0
  146. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/terminal-contract.ts +20 -0
  147. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-queue-metrics.ts +34 -0
  148. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +249 -0
  149. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +7 -0
  150. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +296 -0
  151. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/native-batch.ts +3 -3
  152. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +20 -1
  153. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +60 -15
  154. package/dist/bundling-sources/shared_libs/play-runtime/tool-execution-outcome.ts +20 -0
  155. package/dist/bundling-sources/shared_libs/play-runtime/transient-service-error.ts +12 -0
  156. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +14 -1
  157. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +165 -0
  158. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +15 -23
  159. package/dist/bundling-sources/shared_libs/play-runtime/workflow-executor-auth.ts +169 -0
  160. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +122 -0
  161. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +2 -0
  162. package/dist/bundling-sources/shared_libs/plays/contracts.ts +2 -0
  163. package/dist/bundling-sources/shared_libs/plays/deployment-status.ts +1 -1
  164. package/dist/bundling-sources/shared_libs/plays/docflow.ts +1 -1
  165. package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +39 -0
  166. package/dist/bundling-sources/shared_libs/plays/share/copy.ts +271 -0
  167. package/dist/bundling-sources/shared_libs/plays/share/metrics.ts +140 -0
  168. package/dist/bundling-sources/shared_libs/plays/share/public-read-model.ts +86 -0
  169. package/dist/bundling-sources/shared_libs/plays/share/seo-quality.ts +181 -0
  170. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +658 -0
  171. package/dist/bundling-sources/shared_libs/plays/share/types.ts +197 -0
  172. package/dist/bundling-sources/shared_libs/plays/slack-hitl-timeout-error.ts +37 -0
  173. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +6 -1
  174. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +1 -1
  175. package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +7 -1
  176. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +3 -2
  177. package/dist/bundling-sources/shared_libs/product-notifications/overflow.ts +18 -0
  178. package/dist/bundling-sources/shared_libs/product-notifications/rolling-limit.ts +24 -0
  179. package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +624 -0
  180. package/dist/bundling-sources/shared_libs/product-notifications/slack-payload.ts +140 -0
  181. package/dist/bundling-sources/shared_libs/product-notifications/slack-transport.ts +75 -0
  182. package/dist/bundling-sources/shared_libs/runtime-env.ts +6 -0
  183. package/dist/bundling-sources/shared_libs/security/email-blacklist.ts +42 -0
  184. package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +10 -3
  185. package/dist/bundling-sources/shared_libs/security/slack-sanitize.ts +133 -0
  186. package/dist/cli/getting-started.d.mts +52 -0
  187. package/dist/cli/getting-started.d.ts +52 -0
  188. package/dist/cli/getting-started.js +65 -0
  189. package/dist/cli/getting-started.mjs +36 -0
  190. package/dist/cli/index.d.mts +9 -0
  191. package/dist/cli/index.d.ts +9 -0
  192. package/dist/cli/index.js +545 -264
  193. package/dist/cli/index.mjs +543 -271
  194. package/dist/{compiler-manifest-DsLbQaew.d.mts → compiler-manifest-BaJfE-5H.d.mts} +5 -3
  195. package/dist/{compiler-manifest-DsLbQaew.d.ts → compiler-manifest-BaJfE-5H.d.ts} +5 -3
  196. package/dist/index.d.mts +81 -148
  197. package/dist/index.d.ts +81 -148
  198. package/dist/index.js +83 -26
  199. package/dist/index.mjs +83 -26
  200. package/dist/install-integrity.json +130 -2
  201. package/dist/monitor-fleet-contract.d.mts +138 -0
  202. package/dist/monitor-fleet-contract.d.ts +138 -0
  203. package/dist/monitor-fleet-contract.js +404 -0
  204. package/dist/monitor-fleet-contract.mjs +367 -0
  205. package/dist/plays/bundle-play-file.d.mts +2 -2
  206. package/dist/plays/bundle-play-file.d.ts +2 -2
  207. package/dist/release.d.mts +255 -0
  208. package/dist/release.d.ts +255 -0
  209. package/dist/release.js +215 -0
  210. package/dist/release.mjs +188 -0
  211. package/package.json +21 -4
@@ -0,0 +1,1255 @@
1
+ /**
2
+ * Live step/progress ledger reporting for Node runtime workers.
3
+ *
4
+ * Projection of the runner liveNodeProgress event stream: the runner's event stream is
5
+ * folded into a PlayVisualNodeProgressMap and posted as `update_run_status`
6
+ * patches through the app runtime API. The server-side run ledger
7
+ * (buildPlayRunLedgerEventsFromStatusPatch in run-ledger.ts) converts those
8
+ * patches into step.started/step.progress/step.completed events — nothing is
9
+ * reimplemented here beyond producing the same progress patches.
10
+ *
11
+ * PERF: the runner event path is hot. Nothing here awaits in that path:
12
+ * observations merge into the latest state while at most one upload is active.
13
+ * Runtime Sheet row durability belongs to the context-level row writer.
14
+ */
15
+ import { PlayStepLifecycleTracker } from '../step-lifecycle-tracker';
16
+ import {
17
+ updateRunStatusViaAppRuntime,
18
+ type RuntimeStatusUpdate,
19
+ } from '../app-runtime-api';
20
+ import type { PlayVisualNodeProgressMap } from '../worker-api-types';
21
+ import type { PlayExecutionEvent } from '../ctx-types';
22
+ import type {
23
+ PlayRunnerLogEvent,
24
+ PlayRunnerResult,
25
+ } from '../protocol';
26
+ import type { PlaySchedulerSubmitInput } from '../scheduler-backend';
27
+ import type { DocflowObservationRecord } from '../docflow-observation';
28
+ import {
29
+ getTopLevelPipelineSubsteps,
30
+ type PlayStaticPipeline,
31
+ type PlayStaticSubstep,
32
+ } from '../../plays/static-pipeline';
33
+ import { getStaticSubstepNodeId } from '../step-progress';
34
+ import { runtimeApiBaseUrlFromLaunch } from './runtime-api-origin';
35
+
36
+ // Patterns mirrored from runner events — they parse
37
+ // the same Node play-runner log stream.
38
+ const MAP_START_LOG_PATTERN =
39
+ /Starting map over (\d+) items .* \((?:table namespace|key): ([^;)]+)(?:; (\d+) (?:already satisfied|duplicate keys skipped); (\d+) pending)?\)/i;
40
+ const PERF_MAP_SETUP_LOG_PATTERN =
41
+ /^\[perf\] runtime map setup table=([^\s]+) rows=(\d+)\b/i;
42
+ const PLAY_TOOL_BATCH_LOG_PATTERN =
43
+ /(?:^|\]\s*)Executing tool batch\s+([A-Za-z0-9_:-]+):\s+(\d+)\s+calls\b/i;
44
+ const CTX_LOG_TIMESTAMP_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\]\s+/;
45
+ const LIVE_LOG_TAIL_LIMIT = 1_000;
46
+
47
+ function docflowInvocationSequence(invocationId?: string): number {
48
+ if (!invocationId) return -1;
49
+ const sequence = Number(invocationId.split(':').at(-1));
50
+ return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : -1;
51
+ }
52
+
53
+ // PERF: progress flushes
54
+ // here throttle purely on the adaptive interval. The bypass would chain ~800
55
+ // HTTP posts on a 200k-row run and finalize() awaits that chain. Worst case
56
+ // the live ledger lags by one interval; the forced finalize flush lands final
57
+ // truth.
58
+ function getAdaptiveRunEventIntervalMs(totalRows: number): number {
59
+ if (totalRows >= 1_000_000) return 30_000;
60
+ if (totalRows >= 100_000) return 15_000;
61
+ if (totalRows >= 10_000) return 8_000;
62
+ return 1_500;
63
+ }
64
+
65
+ function staticPipelineFromContractSnapshot(
66
+ value: unknown,
67
+ ): PlayStaticPipeline | null {
68
+ if (!value || typeof value !== 'object') return null;
69
+ const staticPipeline = (value as { staticPipeline?: unknown }).staticPipeline;
70
+ if (!staticPipeline || typeof staticPipeline !== 'object') return null;
71
+ return staticPipeline as PlayStaticPipeline;
72
+ }
73
+
74
+ /**
75
+ * Build ordered list of all static pipeline node IDs with their types.
76
+ * Used to track step-level timing for the Gantt waterfall.
77
+ */
78
+ export function buildOrderedNodeList(
79
+ contractSnapshot: unknown,
80
+ ): Array<{ nodeId: string; type: PlayStaticSubstep['type'] }> {
81
+ const substeps = getTopLevelPipelineSubsteps(
82
+ staticPipelineFromContractSnapshot(contractSnapshot),
83
+ );
84
+ return substeps.map((substep, index) => ({
85
+ nodeId: getStaticSubstepNodeId(substep, index),
86
+ type: substep.type,
87
+ }));
88
+ }
89
+
90
+ /** Map logical dataset namespaces to their static-pipeline node IDs. */
91
+ type StaticDatasetNodeRef = {
92
+ nodeId: string;
93
+ logicalNamespace: string;
94
+ };
95
+
96
+ type RuntimeDatasetNodeRef = StaticDatasetNodeRef & {
97
+ artifactTableNamespace: string;
98
+ };
99
+
100
+ function buildMapNodeLookup(
101
+ contractSnapshot: unknown,
102
+ ): Map<string, StaticDatasetNodeRef> {
103
+ const substeps = getTopLevelPipelineSubsteps(
104
+ staticPipelineFromContractSnapshot(contractSnapshot),
105
+ );
106
+ const lookup = new Map<string, StaticDatasetNodeRef>();
107
+ for (let index = 0; index < substeps.length; index += 1) {
108
+ const substep = substeps[index]!;
109
+ if (substep.type !== 'dataset') continue;
110
+ const logicalNamespace =
111
+ substep.tableNamespace?.trim() || substep.field.trim();
112
+ if (!logicalNamespace) continue;
113
+ lookup.set(logicalNamespace, {
114
+ nodeId: getStaticSubstepNodeId(substep, index),
115
+ logicalNamespace,
116
+ });
117
+ }
118
+ return lookup;
119
+ }
120
+
121
+ export function buildMapNodeIdLookup(
122
+ contractSnapshot: unknown,
123
+ ): Map<string, string> {
124
+ return new Map(
125
+ [...buildMapNodeLookup(contractSnapshot).entries()].map(
126
+ ([namespace, ref]) => [namespace, ref.nodeId],
127
+ ),
128
+ );
129
+ }
130
+
131
+ function resolveDatasetNodeRefFromTableNamespace(
132
+ lookup: ReadonlyMap<string, StaticDatasetNodeRef>,
133
+ physicalNamespace: string,
134
+ ): RuntimeDatasetNodeRef | null {
135
+ const trimmed = physicalNamespace.trim();
136
+ if (!trimmed) {
137
+ return null;
138
+ }
139
+ for (const [logicalNamespace, ref] of lookup.entries()) {
140
+ if (
141
+ trimmed === logicalNamespace ||
142
+ trimmed.startsWith(`${logicalNamespace}_`)
143
+ ) {
144
+ return {
145
+ ...ref,
146
+ artifactTableNamespace: trimmed,
147
+ };
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+
153
+ function rowsProcessedMessage(
154
+ completed: number,
155
+ total: number | undefined,
156
+ ): string | undefined {
157
+ return total !== undefined && total > 0
158
+ ? `${Math.max(0, completed).toLocaleString()} / ${total.toLocaleString()} rows processed`
159
+ : undefined;
160
+ }
161
+
162
+ function callsInFlightMessage(input: {
163
+ activeRows: number;
164
+ completedRows: number;
165
+ totalRows: number;
166
+ }): string {
167
+ const active = input.activeRows.toLocaleString();
168
+ return input.totalRows > 0
169
+ ? `${active} calls in flight · ${input.completedRows.toLocaleString()} / ${input.totalRows.toLocaleString()} rows processed`
170
+ : `${active} calls in flight`;
171
+ }
172
+
173
+ export type PostgresSchedulerProgressReporter = {
174
+ /**
175
+ * Consume one raw runner event. Synchronous and cheap on purpose: this is
176
+ * called for EVERY runner event before any stdout sampling.
177
+ */
178
+ onRunnerEvent(event: unknown): void;
179
+ /**
180
+ * Fold durable docflow observations (read from the scheduler observations
181
+ * table at worker finalize, ADR 0016 rule 3) into the live node progress so a
182
+ * completed absurd run persists docflow node I/O even when the live
183
+ * `docflow.node.*` events were dropped mid-run. Terminal reconciliation only:
184
+ * a durable record supersedes the event-fed nodeIo for its node when it is at
185
+ * least as recent (attempt, then invocation sequence).
186
+ */
187
+ applyDurableObservations(observations: DocflowObservationRecord[]): void;
188
+ /**
189
+ * Record the data-plane row-outcome counts read from the runtime sheet
190
+ * tables at worker finalize (ADR 0016 rule 3). Sheets exist regardless of
191
+ * runner mode (a detached Daytona push run streams no events and can omit
192
+ * its terminal checkpoint entirely), so these counts are the reconciliation
193
+ * of record: finalize applies them AFTER the checkpoint mapFrames, and they
194
+ * win whenever the two disagree or frames are missing. Works from an empty
195
+ * liveNodeProgress — a zero-event run still persists real map step progress.
196
+ */
197
+ applyTerminalSheetOutcomes(
198
+ outcomes: Array<{
199
+ tableNamespace: string;
200
+ completed: number;
201
+ failed: number;
202
+ total: number;
203
+ }>,
204
+ ): void;
205
+ /**
206
+ * Apply terminal step transitions from the runner result and await the
207
+ * final ledger flush. The inline executor returns the result instead of
208
+ * emitting a `result` event, so it is passed in here.
209
+ */
210
+ finalize(
211
+ result?: PlayRunnerResult | null,
212
+ options?: {
213
+ status?: 'running' | 'completed' | 'failed';
214
+ /** Propagate a running-state transport failure to the caller. */
215
+ required?: boolean;
216
+ replayAllTerminalLogs?: boolean;
217
+ terminalLogProducerAttempt?: number;
218
+ skipTerminalLogReplay?: boolean;
219
+ },
220
+ ): Promise<void>;
221
+ };
222
+
223
+ export type PostgresSchedulerDurableLogMode =
224
+ | 'progress_reporter'
225
+ | 'direct_worker';
226
+
227
+ function shouldPublishDurableLogEvent(
228
+ mode: PostgresSchedulerDurableLogMode,
229
+ event:
230
+ | Pick<PlayRunnerLogEvent, 'source' | 'line'>
231
+ | { source?: unknown; line?: unknown },
232
+ ): boolean {
233
+ // Daytona host lifecycle logs use source "worker" (sandbox
234
+ // create/upload/execute). Keep those in worker logs while still storing
235
+ // play-authored ctx.log events in the durable Run Log Stream. The one
236
+ // exception is the structured performance marker: it is bounded (one
237
+ // terminal phase observation per runner phase), run-scoped, and is the
238
+ // canonical black-box regression signal for dispatcher throughput. Do not
239
+ // broaden this to arbitrary worker output.
240
+ return (
241
+ mode !== 'direct_worker' ||
242
+ event.source !== 'worker' ||
243
+ (typeof event.line === 'string' && event.line.startsWith('[perf-trace] '))
244
+ );
245
+ }
246
+
247
+ function shouldPublishTerminalResultLog(
248
+ mode: PostgresSchedulerDurableLogMode,
249
+ line: string,
250
+ ): boolean {
251
+ return mode !== 'direct_worker' || CTX_LOG_TIMESTAMP_PREFIX_RE.test(line);
252
+ }
253
+
254
+ type TerminalLogReplayPayload = NonNullable<
255
+ RuntimeStatusUpdate['terminalLogReplay']
256
+ >;
257
+
258
+ function terminalReplayLinesForMode(input: {
259
+ resultLogs: readonly string[];
260
+ durableLogMode: PostgresSchedulerDurableLogMode;
261
+ }): string[] {
262
+ return input.resultLogs
263
+ .map((line) => line.trim())
264
+ .filter(
265
+ (line) =>
266
+ line.length > 0 &&
267
+ shouldPublishTerminalResultLog(input.durableLogMode, line),
268
+ );
269
+ }
270
+
271
+ function terminalReplayPayloadForMode(input: {
272
+ resultLogs: readonly string[];
273
+ durableLogMode: PostgresSchedulerDurableLogMode;
274
+ liveLogs: readonly string[];
275
+ liveLogsTotalCount: number;
276
+ replayAllTerminalLogs: boolean;
277
+ producerAttempt?: number;
278
+ }): TerminalLogReplayPayload | null {
279
+ let lines = terminalReplayLinesForMode(input);
280
+ // Detached recovery is a complete attempt-local buffer. Its total count
281
+ // gives the replay an offset of zero on that attempt's positional channel;
282
+ // producerAttempt fences stale deliveries while the ledger absorbs only an
283
+ // exact retransmission from that same attempt.
284
+ let totalCount = lines.length;
285
+ if (
286
+ input.durableLogMode === 'direct_worker' &&
287
+ !input.replayAllTerminalLogs
288
+ ) {
289
+ const observedCounts = new Map<string, number>();
290
+ for (const line of input.liveLogs) {
291
+ const key = line.trim().replace(CTX_LOG_TIMESTAMP_PREFIX_RE, '');
292
+ if (!key) continue;
293
+ observedCounts.set(key, (observedCounts.get(key) ?? 0) + 1);
294
+ }
295
+ const boundedLines: string[] = [];
296
+ for (const line of lines) {
297
+ const key = line.trim().replace(CTX_LOG_TIMESTAMP_PREFIX_RE, '');
298
+ const remaining = observedCounts.get(key) ?? 0;
299
+ if (remaining > 0) {
300
+ observedCounts.set(key, remaining - 1);
301
+ continue;
302
+ }
303
+ boundedLines.push(line);
304
+ }
305
+ lines = boundedLines;
306
+ totalCount = input.liveLogsTotalCount + lines.length;
307
+ }
308
+ if (lines.length === 0) {
309
+ return null;
310
+ }
311
+ return {
312
+ lines,
313
+ ...(input.replayAllTerminalLogs ? { dedupeMode: 'exact' as const } : {}),
314
+ ...(typeof input.producerAttempt === 'number'
315
+ ? { producerAttempt: input.producerAttempt }
316
+ : {}),
317
+ totalCount,
318
+ };
319
+ }
320
+
321
+ export function createPostgresSchedulerProgressReporter(input: {
322
+ launch: PlaySchedulerSubmitInput;
323
+ durableLogMode?: PostgresSchedulerDurableLogMode;
324
+ producerAttempt?: number;
325
+ isCurrentAttempt?: () => boolean | Promise<boolean>;
326
+ /** Durable log position before this reporter's first event. Push runners own
327
+ * this cursor so successive batches may land on different gateway Machines. */
328
+ initialLiveLogTotalCount?: number;
329
+ }): PostgresSchedulerProgressReporter {
330
+ const launch = input.launch;
331
+ const durableLogMode = input.durableLogMode ?? 'progress_reporter';
332
+ const producerAttempt = input.producerAttempt;
333
+ const isCurrentAttempt = input.isCurrentAttempt ?? (() => true);
334
+ const orderedNodes = buildOrderedNodeList(launch.contractSnapshot);
335
+ const mapNodeLookup = buildMapNodeLookup(launch.contractSnapshot);
336
+ const runtimeApi = {
337
+ baseUrl: runtimeApiBaseUrlFromLaunch(launch),
338
+ executorToken: launch.executorToken,
339
+ vercelProtectionBypassToken: launch.vercelProtectionBypassToken ?? null,
340
+ };
341
+
342
+ let liveNodeProgress: PlayVisualNodeProgressMap = {};
343
+ let liveNodeProgressDirty = false;
344
+ let liveLogs: string[] = [];
345
+ let liveLogsTotalCount = input.initialLiveLogTotalCount ?? 0;
346
+ let liveLogsDirty = false;
347
+ let lastFlushAt = 0;
348
+ let totalRows = 0;
349
+ let activeMapNodeId: string | null = null;
350
+ let pendingDatasetLifecycleEvents: Array<
351
+ Extract<PlayExecutionEvent, { type: 'dataset.lifecycle' }>
352
+ > = [];
353
+ const pendingActivityObservations = new Map<
354
+ string,
355
+ Extract<PlayExecutionEvent, { type: 'activity.observed' }>['observation']
356
+ >();
357
+
358
+ type ProgressFlushRequest = {
359
+ status: 'running' | 'completed' | 'failed';
360
+ required: boolean;
361
+ terminalLogReplay: TerminalLogReplayPayload | null;
362
+ waiters: Array<{
363
+ resolve: () => void;
364
+ reject: (error: unknown) => void;
365
+ }>;
366
+ };
367
+
368
+ let activeProgressUpload: Promise<void> | null = null;
369
+ let pendingProgressUpload: ProgressFlushRequest | null = null;
370
+
371
+ const mergeProgressFlushRequest = (
372
+ current: ProgressFlushRequest | null,
373
+ next: Omit<ProgressFlushRequest, 'waiters'>,
374
+ waiter: ProgressFlushRequest['waiters'][number],
375
+ ): ProgressFlushRequest => ({
376
+ status:
377
+ next.status !== 'running'
378
+ ? next.status
379
+ : (current?.status ?? next.status),
380
+ required: next.required || current?.required === true,
381
+ terminalLogReplay:
382
+ next.terminalLogReplay ?? current?.terminalLogReplay ?? null,
383
+ waiters: [...(current?.waiters ?? []), waiter],
384
+ });
385
+
386
+ const requeueProgressEvents = (input: {
387
+ datasetLifecycleEvents: typeof pendingDatasetLifecycleEvents;
388
+ activityObservations: Array<
389
+ Extract<PlayExecutionEvent, { type: 'activity.observed' }>['observation']
390
+ >;
391
+ }): void => {
392
+ pendingDatasetLifecycleEvents = [
393
+ ...input.datasetLifecycleEvents,
394
+ ...pendingDatasetLifecycleEvents,
395
+ ];
396
+ for (const observation of input.activityObservations) {
397
+ const newer = pendingActivityObservations.get(observation.activityId);
398
+ if (!newer || newer.observedAt < observation.observedAt) {
399
+ pendingActivityObservations.set(observation.activityId, observation);
400
+ }
401
+ }
402
+ };
403
+
404
+ const startProgressUploadPump = (): void => {
405
+ if (activeProgressUpload || !pendingProgressUpload) return;
406
+
407
+ // Start on the next microtask so an attempt-fence change made by the
408
+ // scheduler in the same turn can invalidate queued observations before
409
+ // any transport side effect begins.
410
+ activeProgressUpload = Promise.resolve()
411
+ .then(async () => {
412
+ while (pendingProgressUpload) {
413
+ const request = pendingProgressUpload;
414
+ pendingProgressUpload = null;
415
+
416
+ const nextProgress = { ...liveNodeProgress };
417
+ const nextLogs = liveLogs.slice();
418
+ const nextLogTotalCount = liveLogsTotalCount;
419
+ const nextDatasetLifecycleEvents = pendingDatasetLifecycleEvents;
420
+ pendingDatasetLifecycleEvents = [];
421
+ const nextActivityObservations = [
422
+ ...pendingActivityObservations.values(),
423
+ ];
424
+ pendingActivityObservations.clear();
425
+ liveNodeProgressDirty = false;
426
+ liveLogsDirty = false;
427
+
428
+ try {
429
+ if (await isCurrentAttempt()) {
430
+ await updateRunStatusViaAppRuntime(runtimeApi, {
431
+ playId: launch.runId,
432
+ status: request.status,
433
+ liveNodeProgress: nextProgress,
434
+ liveLogs: nextLogs,
435
+ liveLogTotalCount: nextLogTotalCount,
436
+ liveLogProducerAttempt: producerAttempt,
437
+ datasetLifecycleEvents: nextDatasetLifecycleEvents,
438
+ activityObservations: nextActivityObservations,
439
+ terminalLogReplay: request.terminalLogReplay,
440
+ lastCheckpointAt: Date.now(),
441
+ });
442
+ }
443
+ request.waiters.forEach(({ resolve }) => resolve());
444
+ } catch (error) {
445
+ requeueProgressEvents({
446
+ datasetLifecycleEvents: nextDatasetLifecycleEvents,
447
+ activityObservations: nextActivityObservations,
448
+ });
449
+ console.warn('[postgres-scheduler.progress_flush_failed]', {
450
+ runId: launch.runId,
451
+ error: error instanceof Error ? error.message : String(error),
452
+ });
453
+ if (request.required) {
454
+ request.waiters.forEach(({ reject }) => reject(error));
455
+ } else {
456
+ request.waiters.forEach(({ resolve }) => resolve());
457
+ }
458
+ }
459
+ }
460
+ })
461
+ .finally(() => {
462
+ activeProgressUpload = null;
463
+ if (pendingProgressUpload) startProgressUploadPump();
464
+ });
465
+ };
466
+
467
+ const flush = (input?: {
468
+ force?: boolean;
469
+ status?: 'running' | 'completed' | 'failed';
470
+ required?: boolean;
471
+ terminalLogReplay?: TerminalLogReplayPayload | null;
472
+ }): Promise<void> => {
473
+ const force = input?.force === true;
474
+ const status = input?.status ?? 'running';
475
+ const required = input?.required === true;
476
+ if (
477
+ !force &&
478
+ !liveNodeProgressDirty &&
479
+ !liveLogsDirty &&
480
+ pendingDatasetLifecycleEvents.length === 0 &&
481
+ pendingActivityObservations.size === 0
482
+ ) {
483
+ return activeProgressUpload ?? Promise.resolve();
484
+ }
485
+ const now = Date.now();
486
+ if (
487
+ !force &&
488
+ now - lastFlushAt < getAdaptiveRunEventIntervalMs(totalRows)
489
+ ) {
490
+ return activeProgressUpload ?? Promise.resolve();
491
+ }
492
+ lastFlushAt = now;
493
+ const requested = new Promise<void>((resolve, reject) => {
494
+ pendingProgressUpload = mergeProgressFlushRequest(
495
+ pendingProgressUpload,
496
+ {
497
+ status,
498
+ required,
499
+ terminalLogReplay: input?.terminalLogReplay ?? null,
500
+ },
501
+ { resolve, reject },
502
+ );
503
+ });
504
+ startProgressUploadPump();
505
+ return requested;
506
+ };
507
+
508
+ const scheduleFlush = (force = false) => {
509
+ // A best-effort event flush can coalesce with a required terminal flush.
510
+ // The upload pump logs failure and rejects the terminal caller; consume
511
+ // only this detached observer's rejection to avoid an unhandled promise.
512
+ void flush({ force }).catch(() => undefined);
513
+ };
514
+
515
+ const appendLiveLog = (line: string) => {
516
+ const trimmed = line.trim();
517
+ if (!trimmed) {
518
+ return;
519
+ }
520
+ liveLogsTotalCount += 1;
521
+ liveLogs = [...liveLogs, trimmed];
522
+ if (liveLogs.length > LIVE_LOG_TAIL_LIMIT) {
523
+ liveLogs = liveLogs.slice(liveLogs.length - LIVE_LOG_TAIL_LIMIT);
524
+ }
525
+ liveLogsDirty = true;
526
+ };
527
+
528
+ const setNodeProgress = (
529
+ nodeId: string,
530
+ patch: Partial<PlayVisualNodeProgressMap[string]>,
531
+ ) => {
532
+ const current = liveNodeProgress[nodeId] ?? {};
533
+ const nextCompleted = Number(patch.completed ?? current.completed ?? 0);
534
+ const nextTotal = Number(patch.total ?? current.total ?? 0);
535
+ const clearCompletedAt =
536
+ patch.completedAt === undefined &&
537
+ Number.isFinite(nextCompleted) &&
538
+ Number.isFinite(nextTotal) &&
539
+ nextTotal > 0 &&
540
+ nextCompleted < nextTotal;
541
+ liveNodeProgress = {
542
+ ...liveNodeProgress,
543
+ [nodeId]: {
544
+ ...(current.completed !== undefined
545
+ ? { completed: current.completed }
546
+ : {}),
547
+ ...(current.total !== undefined ? { total: current.total } : {}),
548
+ ...(current.failed !== undefined ? { failed: current.failed } : {}),
549
+ ...(current.startedRows !== undefined
550
+ ? { startedRows: current.startedRows }
551
+ : {}),
552
+ ...(current.activeRows !== undefined
553
+ ? { activeRows: current.activeRows }
554
+ : {}),
555
+ ...(current.waitingRows !== undefined
556
+ ? { waitingRows: current.waitingRows }
557
+ : {}),
558
+ ...(current.completedRows !== undefined
559
+ ? { completedRows: current.completedRows }
560
+ : {}),
561
+ ...(current.message !== undefined ? { message: current.message } : {}),
562
+ ...(current.artifactTableNamespace !== undefined
563
+ ? { artifactTableNamespace: current.artifactTableNamespace }
564
+ : {}),
565
+ ...(current.startedAt !== undefined
566
+ ? { startedAt: current.startedAt }
567
+ : {}),
568
+ ...(current.completedAt !== undefined && !clearCompletedAt
569
+ ? { completedAt: current.completedAt }
570
+ : {}),
571
+ ...(current.nodeIo !== undefined ? { nodeIo: current.nodeIo } : {}),
572
+ ...(patch.completed !== undefined
573
+ ? { completed: patch.completed }
574
+ : {}),
575
+ ...(patch.total !== undefined ? { total: patch.total } : {}),
576
+ ...(patch.failed !== undefined ? { failed: patch.failed } : {}),
577
+ ...(patch.startedRows !== undefined
578
+ ? { startedRows: patch.startedRows }
579
+ : {}),
580
+ ...(patch.activeRows !== undefined
581
+ ? { activeRows: patch.activeRows }
582
+ : {}),
583
+ ...(patch.waitingRows !== undefined
584
+ ? { waitingRows: patch.waitingRows }
585
+ : {}),
586
+ ...(patch.completedRows !== undefined
587
+ ? { completedRows: patch.completedRows }
588
+ : {}),
589
+ ...(patch.message !== undefined ? { message: patch.message } : {}),
590
+ ...(patch.artifactTableNamespace !== undefined
591
+ ? { artifactTableNamespace: patch.artifactTableNamespace }
592
+ : {}),
593
+ ...(patch.startedAt !== undefined
594
+ ? { startedAt: patch.startedAt }
595
+ : {}),
596
+ ...(patch.completedAt !== undefined
597
+ ? { completedAt: patch.completedAt }
598
+ : {}),
599
+ ...(patch.nodeIo !== undefined ? { nodeIo: patch.nodeIo } : {}),
600
+ updatedAt: Date.now(),
601
+ },
602
+ };
603
+ liveNodeProgressDirty = true;
604
+ };
605
+
606
+ // Every lifecycle transition flows through setNodeProgress so the
607
+ // liveNodeProgress patch stream is canonical.
608
+ const stepLifecycle = new PlayStepLifecycleTracker(
609
+ orderedNodes,
610
+ () => liveNodeProgress,
611
+ (event) => {
612
+ const patch: Partial<PlayVisualNodeProgressMap[string]> =
613
+ event.transition === 'started'
614
+ ? { startedAt: event.at }
615
+ : { completedAt: event.at };
616
+ setNodeProgress(event.nodeId, patch);
617
+ scheduleFlush();
618
+ },
619
+ );
620
+ stepLifecycle.markPreDatasetStepsStarted();
621
+
622
+ const markMapStartedFromLog = (line: string) => {
623
+ const match = line.match(MAP_START_LOG_PATTERN);
624
+ if (!match) {
625
+ return;
626
+ }
627
+ const total = Number(match[1] ?? 0);
628
+ const physicalNamespace = match[2]?.trim() ?? '';
629
+ const alreadySatisfied = Number(match[3] ?? 0);
630
+ const nodeRef = resolveDatasetNodeRefFromTableNamespace(
631
+ mapNodeLookup,
632
+ physicalNamespace,
633
+ );
634
+ if (!nodeRef) {
635
+ return;
636
+ }
637
+ activeMapNodeId = nodeRef.nodeId;
638
+ if (Number.isFinite(total) && total > totalRows) {
639
+ totalRows = total;
640
+ }
641
+ stepLifecycle.onMapStarted(nodeRef.nodeId);
642
+ // Streamed maps emit this diagnostic once per source page. The value in
643
+ // the log is page-local duplicate reuse, not cumulative durable progress.
644
+ // Preserve the typed commit-event counters already folded for prior pages;
645
+ // otherwise every new page transiently resets completed to zero and makes
646
+ // waitingRows contradict the canonical total.
647
+ const current = liveNodeProgress[nodeRef.nodeId] ?? {};
648
+ const effectiveTotal = Math.max(
649
+ Number.isFinite(total) && total > 0 ? total : 0,
650
+ Number(current.total ?? 0),
651
+ );
652
+ const completed = Math.max(
653
+ 0,
654
+ Number(current.completed ?? 0),
655
+ Number.isFinite(alreadySatisfied) ? alreadySatisfied : 0,
656
+ );
657
+ const failed = Math.max(0, Number(current.failed ?? 0));
658
+ const settled = completed + failed;
659
+ setNodeProgress(nodeRef.nodeId, {
660
+ completed,
661
+ failed,
662
+ total: effectiveTotal > 0 ? effectiveTotal : undefined,
663
+ startedRows: Math.max(Number(current.startedRows ?? 0), settled),
664
+ activeRows: 0,
665
+ waitingRows:
666
+ effectiveTotal > 0 ? Math.max(0, effectiveTotal - settled) : undefined,
667
+ completedRows: completed,
668
+ artifactTableNamespace: nodeRef.artifactTableNamespace,
669
+ startedAt: current.startedAt ?? Date.now(),
670
+ message: rowsProcessedMessage(completed, effectiveTotal),
671
+ });
672
+ };
673
+
674
+ const markMapStartedFromPerfLog = (line: string) => {
675
+ const normalizedLine = line.replace(CTX_LOG_TIMESTAMP_PREFIX_RE, '');
676
+ const match = normalizedLine.match(PERF_MAP_SETUP_LOG_PATTERN);
677
+ if (!match) {
678
+ return;
679
+ }
680
+ const nodeRef = resolveDatasetNodeRefFromTableNamespace(
681
+ mapNodeLookup,
682
+ match[1] ?? '',
683
+ );
684
+ if (!nodeRef) {
685
+ return;
686
+ }
687
+ const total = Number(match[2] ?? 0);
688
+ activeMapNodeId = nodeRef.nodeId;
689
+ if (Number.isFinite(total) && total > totalRows) {
690
+ totalRows = total;
691
+ }
692
+ stepLifecycle.onMapStarted(nodeRef.nodeId);
693
+ const current = liveNodeProgress[nodeRef.nodeId] ?? {};
694
+ const effectiveTotal = Math.max(
695
+ Number.isFinite(total) && total > 0 ? total : 0,
696
+ Number(current.total ?? 0),
697
+ );
698
+ const completed = Math.max(0, Number(current.completed ?? 0));
699
+ const failed = Math.max(0, Number(current.failed ?? 0));
700
+ const settled = completed + failed;
701
+ setNodeProgress(nodeRef.nodeId, {
702
+ completed,
703
+ failed,
704
+ total: effectiveTotal > 0 ? effectiveTotal : undefined,
705
+ startedRows: Math.max(Number(current.startedRows ?? 0), settled),
706
+ activeRows: 0,
707
+ waitingRows:
708
+ effectiveTotal > 0 ? Math.max(0, effectiveTotal - settled) : undefined,
709
+ completedRows: completed,
710
+ artifactTableNamespace: nodeRef.artifactTableNamespace,
711
+ startedAt: current.startedAt ?? Date.now(),
712
+ message: rowsProcessedMessage(completed, effectiveTotal),
713
+ });
714
+ };
715
+
716
+ const markToolBatchInFlightFromLog = (line: string) => {
717
+ if (!activeMapNodeId) {
718
+ return;
719
+ }
720
+ const match = line.match(PLAY_TOOL_BATCH_LOG_PATTERN);
721
+ if (!match) {
722
+ return;
723
+ }
724
+ const callCount = Number(match[2] ?? 0);
725
+ if (!Number.isFinite(callCount) || callCount <= 0) {
726
+ return;
727
+ }
728
+ const current = liveNodeProgress[activeMapNodeId] ?? {};
729
+ const completed = Number(current.completed ?? 0);
730
+ const failed = Number(current.failed ?? 0);
731
+ const total = Number(current.total ?? 0);
732
+ const settled = completed + failed;
733
+ const activeRows =
734
+ total > 0 ? Math.min(callCount, Math.max(0, total - settled)) : callCount;
735
+ const startedRows = Math.max(
736
+ Number(current.startedRows ?? 0),
737
+ settled + activeRows,
738
+ );
739
+ setNodeProgress(activeMapNodeId, {
740
+ startedRows: total > 0 ? Math.min(total, startedRows) : startedRows,
741
+ activeRows,
742
+ waitingRows:
743
+ total > 0 ? Math.max(0, total - settled - activeRows) : undefined,
744
+ completedRows: completed,
745
+ message: callsInFlightMessage({
746
+ activeRows,
747
+ completedRows: completed,
748
+ totalRows: total,
749
+ }),
750
+ });
751
+ };
752
+
753
+ const applyExecutionEvent = (event: PlayExecutionEvent) => {
754
+ if (event.type === 'docflow.node.hit') {
755
+ setNodeProgress(event.nodeId, {
756
+ startedAt: event.at,
757
+ completedAt: event.at,
758
+ });
759
+ scheduleFlush(true);
760
+ return;
761
+ }
762
+ if (
763
+ event.type === 'docflow.node.started' ||
764
+ event.type === 'docflow.node.completed' ||
765
+ event.type === 'docflow.node.failed'
766
+ ) {
767
+ const current = liveNodeProgress[event.nodeId];
768
+ const currentAttempt = current?.nodeIo?.attempt ?? -1;
769
+ if (event.attempt < currentAttempt) return;
770
+ if (
771
+ event.attempt === currentAttempt &&
772
+ docflowInvocationSequence(event.invocationId) <
773
+ docflowInvocationSequence(current?.nodeIo?.invocationId)
774
+ ) {
775
+ return;
776
+ }
777
+ if (event.type === 'docflow.node.started') {
778
+ liveNodeProgress = {
779
+ ...liveNodeProgress,
780
+ [event.nodeId]: {
781
+ startedAt: event.at,
782
+ updatedAt: event.at,
783
+ nodeIo: {
784
+ attempt: event.attempt,
785
+ invocationId: event.invocationId,
786
+ inputs: event.inputs,
787
+ ...(event.inputsTruncated ? { inputsTruncated: true } : {}),
788
+ },
789
+ },
790
+ };
791
+ liveNodeProgressDirty = true;
792
+ scheduleFlush(true);
793
+ return;
794
+ }
795
+ if (event.type === 'docflow.node.completed') {
796
+ setNodeProgress(event.nodeId, {
797
+ startedAt: current?.startedAt ?? event.at,
798
+ completedAt: event.at,
799
+ nodeIo: {
800
+ attempt: event.attempt,
801
+ invocationId: event.invocationId,
802
+ inputs: event.inputs,
803
+ ...(event.inputsTruncated ? { inputsTruncated: true } : {}),
804
+ outputs: event.outputs,
805
+ ...(event.outputsTruncated ? { outputsTruncated: true } : {}),
806
+ error: null,
807
+ },
808
+ });
809
+ scheduleFlush(true);
810
+ return;
811
+ }
812
+ if (event.type === 'docflow.node.failed') {
813
+ setNodeProgress(event.nodeId, {
814
+ startedAt: current?.startedAt ?? event.at,
815
+ completedAt: event.at,
816
+ nodeIo: {
817
+ attempt: event.attempt,
818
+ invocationId: event.invocationId,
819
+ inputs: event.inputs,
820
+ ...(event.inputsTruncated ? { inputsTruncated: true } : {}),
821
+ error: event.error,
822
+ },
823
+ });
824
+ scheduleFlush(true);
825
+ return;
826
+ }
827
+ }
828
+ if (event.type === 'activity.observed') {
829
+ pendingActivityObservations.set(
830
+ event.observation.activityId,
831
+ event.observation,
832
+ );
833
+ scheduleFlush(true);
834
+ return;
835
+ }
836
+ if (event.type === 'dataset.lifecycle') {
837
+ pendingDatasetLifecycleEvents.push(event);
838
+ scheduleFlush(true);
839
+ return;
840
+ }
841
+ // ADR 0018: tool-node lifecycle is an explicit typed runtime event. It
842
+ // replaces the `Calling tool: <id>` / `Tool <id> failed` stdout regexes
843
+ // this reporter used to run over the customer log channel.
844
+ if (event.type === 'tool.call.started') {
845
+ stepLifecycle.onToolCalled(event.toolId);
846
+ scheduleFlush();
847
+ return;
848
+ }
849
+ if (event.type === 'tool.call.settled') {
850
+ if (event.outcome === 'failed') {
851
+ stepLifecycle.onToolFailed(event.toolId);
852
+ scheduleFlush(true);
853
+ }
854
+ return;
855
+ }
856
+ if (!event.type.startsWith('map.')) {
857
+ return;
858
+ }
859
+ // The inline Node runner emits map events with mapNodeId null (unlike the
860
+ // workers runtime), so fall back to resolving the static-pipeline node
861
+ // from the event's table namespace — same lookup the log parser uses.
862
+ const nodeId =
863
+ event.mapNodeId?.trim() ||
864
+ resolveDatasetNodeRefFromTableNamespace(
865
+ mapNodeLookup,
866
+ event.artifactTableNamespace || event.logicalNamespace,
867
+ )?.nodeId;
868
+ if (!nodeId) {
869
+ return;
870
+ }
871
+ activeMapNodeId = nodeId;
872
+ const eventTotalRows = 'totalRows' in event ? event.totalRows : undefined;
873
+ if (
874
+ typeof eventTotalRows === 'number' &&
875
+ Number.isFinite(eventTotalRows) &&
876
+ eventTotalRows > totalRows
877
+ ) {
878
+ totalRows = eventTotalRows;
879
+ }
880
+ if (
881
+ event.type === 'map.preparing' ||
882
+ event.type === 'map.started' ||
883
+ event.type === 'map.resumed'
884
+ ) {
885
+ stepLifecycle.onMapStarted(nodeId);
886
+ setNodeProgress(nodeId, {
887
+ completed: event.completedRows,
888
+ failed: 0,
889
+ total: eventTotalRows,
890
+ startedRows: event.completedRows,
891
+ activeRows: 0,
892
+ waitingRows:
893
+ typeof eventTotalRows === 'number'
894
+ ? Math.max(0, eventTotalRows - event.completedRows)
895
+ : undefined,
896
+ completedRows: event.completedRows,
897
+ artifactTableNamespace: event.artifactTableNamespace,
898
+ startedAt: Date.now(),
899
+ message:
900
+ event.type === 'map.preparing'
901
+ ? 'Preparing durable table...'
902
+ : rowsProcessedMessage(event.completedRows, eventTotalRows ?? 0),
903
+ });
904
+ return;
905
+ }
906
+ if (event.type === 'map.progress') {
907
+ const current = liveNodeProgress[nodeId] ?? {};
908
+ const settled = Math.max(0, event.completedRows + event.failedRows);
909
+ const progressTotalRows = Math.max(
910
+ typeof event.totalRows === 'number' && Number.isFinite(event.totalRows)
911
+ ? event.totalRows
912
+ : 0,
913
+ Number(current.total ?? 0),
914
+ );
915
+ const startedRows = Math.max(settled, Number(current.startedRows ?? 0));
916
+ const activeRows = Math.max(0, startedRows - settled);
917
+ setNodeProgress(nodeId, {
918
+ completed: event.completedRows,
919
+ failed: event.failedRows,
920
+ total: progressTotalRows > 0 ? progressTotalRows : undefined,
921
+ startedRows,
922
+ activeRows,
923
+ waitingRows:
924
+ progressTotalRows > 0
925
+ ? Math.max(0, progressTotalRows - startedRows)
926
+ : undefined,
927
+ completedRows: event.completedRows,
928
+ artifactTableNamespace: event.artifactTableNamespace,
929
+ message: rowsProcessedMessage(event.completedRows, progressTotalRows),
930
+ });
931
+ return;
932
+ }
933
+ if (event.type === 'map.completed' || event.type === 'map.failed') {
934
+ if (event.type === 'map.failed') {
935
+ stepLifecycle.onMapFailed(nodeId);
936
+ } else {
937
+ stepLifecycle.onMapCompleted(nodeId);
938
+ }
939
+ const processedRows = Math.max(0, event.completedRows + event.failedRows);
940
+ const terminalTotal =
941
+ typeof event.totalRows === 'number' && Number.isFinite(event.totalRows)
942
+ ? event.totalRows
943
+ : 0;
944
+ setNodeProgress(nodeId, {
945
+ completed: event.completedRows,
946
+ failed: event.failedRows,
947
+ total: event.totalRows,
948
+ startedRows:
949
+ terminalTotal > 0
950
+ ? Math.min(terminalTotal, processedRows)
951
+ : processedRows,
952
+ activeRows: 0,
953
+ waitingRows: 0,
954
+ completedRows: event.completedRows,
955
+ artifactTableNamespace: event.artifactTableNamespace,
956
+ completedAt: Date.now(),
957
+ message: rowsProcessedMessage(event.completedRows, event.totalRows),
958
+ });
959
+ activeMapNodeId = null;
960
+ return;
961
+ }
962
+ if (event.type === 'map.suspended') {
963
+ setNodeProgress(nodeId, {
964
+ completed: event.completedRows,
965
+ failed: event.failedRows,
966
+ total: event.totalRows,
967
+ completedRows: event.completedRows,
968
+ artifactTableNamespace: event.artifactTableNamespace,
969
+ message: rowsProcessedMessage(event.completedRows, event.totalRows),
970
+ });
971
+ }
972
+ };
973
+
974
+ const onRunnerEvent = (event: unknown): void => {
975
+ if (!event || typeof event !== 'object') {
976
+ return;
977
+ }
978
+ const type = (event as { type?: unknown }).type;
979
+ if (type === 'log') {
980
+ const logEvent = event as { line?: unknown; source?: unknown };
981
+ const rawLine = logEvent.line;
982
+ if (typeof rawLine !== 'string' || !rawLine) {
983
+ return;
984
+ }
985
+ if (shouldPublishDurableLogEvent(durableLogMode, logEvent)) {
986
+ appendLiveLog(rawLine);
987
+ }
988
+ markMapStartedFromLog(rawLine);
989
+ markMapStartedFromPerfLog(rawLine);
990
+ markToolBatchInFlightFromLog(rawLine);
991
+ scheduleFlush();
992
+ return;
993
+ }
994
+ if (type === 'row_update') {
995
+ // Runtime Sheet rows are owned by the context-level row writer. Progress
996
+ // consumes execution and dataset lifecycle facts only; it never patches
997
+ // the same durable rows through a second write path.
998
+ return;
999
+ }
1000
+ if (type === 'execution_event') {
1001
+ const executionEvent = (event as { event?: PlayExecutionEvent }).event;
1002
+ if (executionEvent && typeof executionEvent.type === 'string') {
1003
+ applyExecutionEvent(executionEvent);
1004
+ scheduleFlush();
1005
+ }
1006
+ return;
1007
+ }
1008
+ };
1009
+
1010
+ // ADR 0016 rule 3: fold durable docflow observations into liveNodeProgress at
1011
+ // terminal so a completed absurd run carries docflow node I/O even when the
1012
+ // live docflow.node.* events were dropped mid-run. A record only supersedes
1013
+ // the current node state when it is at least as recent — later attempt, or
1014
+ // same attempt with a >= invocation sequence — matching the event-path guard
1015
+ // in applyExecutionEvent so live and durable never regress each other.
1016
+ const applyDurableObservations = (
1017
+ observations: DocflowObservationRecord[],
1018
+ ): void => {
1019
+ for (const observation of observations) {
1020
+ const nodeId = observation.nodeId.trim();
1021
+ if (!nodeId) continue;
1022
+ const current = liveNodeProgress[nodeId];
1023
+ const currentAttempt = current?.nodeIo?.attempt ?? -1;
1024
+ if (observation.attempt < currentAttempt) continue;
1025
+ if (
1026
+ observation.attempt === currentAttempt &&
1027
+ observation.invocationSeq <
1028
+ docflowInvocationSequence(current?.nodeIo?.invocationId)
1029
+ ) {
1030
+ continue;
1031
+ }
1032
+ const at = observation.settledAt ?? observation.startedAt ?? Date.now();
1033
+ setNodeProgress(nodeId, {
1034
+ startedAt: current?.startedAt ?? observation.startedAt ?? at,
1035
+ ...(observation.status === 'started' ? {} : { completedAt: at }),
1036
+ nodeIo: {
1037
+ attempt: observation.attempt,
1038
+ ...(observation.invocationId
1039
+ ? { invocationId: observation.invocationId }
1040
+ : {}),
1041
+ ...(observation.inputs ? { inputs: observation.inputs } : {}),
1042
+ ...(observation.inputsTruncated ? { inputsTruncated: true } : {}),
1043
+ ...(observation.status === 'completed' && observation.outputs
1044
+ ? { outputs: observation.outputs }
1045
+ : {}),
1046
+ ...(observation.status === 'completed' && observation.outputsTruncated
1047
+ ? { outputsTruncated: true }
1048
+ : {}),
1049
+ ...(observation.status === 'failed'
1050
+ ? { error: observation.error ?? 'Docflow node failed.' }
1051
+ : observation.status === 'completed'
1052
+ ? { error: null }
1053
+ : {}),
1054
+ },
1055
+ });
1056
+ }
1057
+ liveNodeProgressDirty = true;
1058
+ };
1059
+
1060
+ // Terminal truth for map/dataset nodes is the durable per-map checkpoint
1061
+ // frame carried on the runner result — NOT the event-fed liveNodeProgress
1062
+ // counters, which can be stale or dropped (a missed map.completed frame, a
1063
+ // node-id resolution miss on a docflow-gated run) and then become the
1064
+ // terminal claim. ADR 0016 rule 3: terminal step progress is derived from the
1065
+ // data plane. `checkpoint.mapFrames` is the map executor's authoritative
1066
+ // settled account (`completedRowsCount` successes-only, `failedRowsCount`
1067
+ // settled-but-failed, `totalRows` the settled denominator), written after the
1068
+ // incremental-persistence flush barrier, so it agrees with the runtime
1069
+ // sheets. Reconcile each map node's terminal progress from it before the
1070
+ // final flush so the persisted ledger — and `summarizeRunRowOutcomes` — carry
1071
+ // the truth.
1072
+ const reconcileTerminalMapProgressFromCheckpoint = (
1073
+ result: PlayRunnerResult | null | undefined,
1074
+ ): void => {
1075
+ const mapFrames = result?.checkpoint?.mapFrames;
1076
+ if (!mapFrames || typeof mapFrames !== 'object') {
1077
+ return;
1078
+ }
1079
+ for (const frame of Object.values(mapFrames)) {
1080
+ if (!frame || typeof frame !== 'object') continue;
1081
+ if (frame.status !== 'completed' && frame.status !== 'failed') continue;
1082
+ const nodeId =
1083
+ frame.mapNodeId?.trim() ||
1084
+ resolveDatasetNodeRefFromTableNamespace(
1085
+ mapNodeLookup,
1086
+ frame.artifactTableNamespace || frame.logicalNamespace,
1087
+ )?.nodeId;
1088
+ if (!nodeId) continue;
1089
+ // `completedRowKeys`/`pendingRowKeys` are emptied at map completion, so
1090
+ // trust the *Count fields; fall back to the key-array length only while a
1091
+ // count is absent (never at a settled frame the executor fully accounts).
1092
+ const failed = Math.max(0, Number(frame.failedRowsCount ?? 0) || 0);
1093
+ const completed = Math.max(
1094
+ 0,
1095
+ Number(
1096
+ frame.completedRowsCount ?? frame.completedRowKeys?.length ?? 0,
1097
+ ) || 0,
1098
+ );
1099
+ const total =
1100
+ Number.isFinite(frame.totalRows) && frame.totalRows > 0
1101
+ ? frame.totalRows
1102
+ : completed + failed;
1103
+ const existing = liveNodeProgress[nodeId] ?? {};
1104
+ // Only correct upward-or-equal on the settled denominator and never lose
1105
+ // a larger already-observed success count from a concurrent flush; the
1106
+ // checkpoint frame is authoritative for the settled split.
1107
+ setNodeProgress(nodeId, {
1108
+ completed,
1109
+ failed,
1110
+ total: total > 0 ? total : undefined,
1111
+ startedRows: total > 0 ? total : completed + failed,
1112
+ activeRows: 0,
1113
+ waitingRows: 0,
1114
+ completedRows: completed,
1115
+ artifactTableNamespace:
1116
+ frame.artifactTableNamespace ||
1117
+ existing.artifactTableNamespace ||
1118
+ undefined,
1119
+ completedAt: existing.completedAt ?? Date.now(),
1120
+ message: rowsProcessedMessage(completed, total > 0 ? total : undefined),
1121
+ });
1122
+ }
1123
+ liveNodeProgressDirty = true;
1124
+ };
1125
+
1126
+ // Sheet-table row-outcome counts recorded by the worker before finalize.
1127
+ // These are the reconciliation of record (ADR 0016 rule 3): the runtime
1128
+ // sheet rows exist regardless of runner mode, while checkpoint frames can be
1129
+ // omitted on the detached terminal wire (OMITTED_TERMINAL_REPLAY) and events
1130
+ // never stream while the worker is parked.
1131
+ let terminalSheetOutcomes: Array<{
1132
+ tableNamespace: string;
1133
+ completed: number;
1134
+ failed: number;
1135
+ total: number;
1136
+ }> = [];
1137
+
1138
+ const applyTerminalSheetOutcomes = (
1139
+ outcomes: Array<{
1140
+ tableNamespace: string;
1141
+ completed: number;
1142
+ failed: number;
1143
+ total: number;
1144
+ }>,
1145
+ ): void => {
1146
+ terminalSheetOutcomes = outcomes;
1147
+ };
1148
+
1149
+ // Applied AFTER the checkpoint-frame reconcile so the data plane wins when
1150
+ // the two disagree; also the only terminal source when frames are missing
1151
+ // (detached push runs) and liveNodeProgress is empty (zero-event runs).
1152
+ const reconcileTerminalMapProgressFromSheetOutcomes = (): void => {
1153
+ for (const outcome of terminalSheetOutcomes) {
1154
+ const tableNamespace = outcome.tableNamespace.trim();
1155
+ if (!tableNamespace) continue;
1156
+ const nodeRef = resolveDatasetNodeRefFromTableNamespace(
1157
+ mapNodeLookup,
1158
+ tableNamespace,
1159
+ );
1160
+ if (!nodeRef) continue;
1161
+ const completed = Math.max(0, Math.floor(outcome.completed) || 0);
1162
+ const failed = Math.max(0, Math.floor(outcome.failed) || 0);
1163
+ const total =
1164
+ Number.isFinite(outcome.total) && outcome.total > 0
1165
+ ? Math.floor(outcome.total)
1166
+ : completed + failed;
1167
+ if (total <= 0 && completed <= 0 && failed <= 0) continue;
1168
+ const existing = liveNodeProgress[nodeRef.nodeId] ?? {};
1169
+ const now = Date.now();
1170
+ setNodeProgress(nodeRef.nodeId, {
1171
+ completed,
1172
+ failed,
1173
+ total: total > 0 ? total : undefined,
1174
+ startedRows: total > 0 ? total : completed + failed,
1175
+ activeRows: 0,
1176
+ waitingRows: 0,
1177
+ completedRows: completed,
1178
+ artifactTableNamespace:
1179
+ existing.artifactTableNamespace ?? nodeRef.artifactTableNamespace,
1180
+ startedAt: existing.startedAt ?? now,
1181
+ completedAt: existing.completedAt ?? now,
1182
+ message: rowsProcessedMessage(completed, total > 0 ? total : undefined),
1183
+ });
1184
+ }
1185
+ liveNodeProgressDirty = true;
1186
+ };
1187
+
1188
+ const finalize = async (
1189
+ result?: PlayRunnerResult | null,
1190
+ options: {
1191
+ status?: 'running' | 'completed' | 'failed';
1192
+ required?: boolean;
1193
+ replayAllTerminalLogs?: boolean;
1194
+ terminalLogProducerAttempt?: number;
1195
+ skipTerminalLogReplay?: boolean;
1196
+ } = {},
1197
+ ): Promise<void> => {
1198
+ // Mark started steps terminal with real timestamps. Static branches that
1199
+ // never started remain non-executed; suspended runs stay in-flight.
1200
+ if (result?.status === 'failed') {
1201
+ stepLifecycle.markStartedFailed();
1202
+ } else if (result?.status === 'completed') {
1203
+ stepLifecycle.markAllTerminal();
1204
+ }
1205
+ // Terminal runs derive their per-map step progress from the durable data
1206
+ // plane, overriding any stale/dropped event-fed counters. Checkpoint
1207
+ // frames first (cheap, in-result), then sheet-table counts as the
1208
+ // reconciliation of record. Suspended runs stay mid-flight, so leave their
1209
+ // live counters untouched.
1210
+ if (result?.status === 'completed' || result?.status === 'failed') {
1211
+ reconcileTerminalMapProgressFromCheckpoint(result);
1212
+ reconcileTerminalMapProgressFromSheetOutcomes();
1213
+ }
1214
+ const status = options.status ?? 'running';
1215
+ await flush({
1216
+ force: true,
1217
+ status,
1218
+ required:
1219
+ options.required === true ||
1220
+ status === 'completed' ||
1221
+ status === 'failed',
1222
+ terminalLogReplay:
1223
+ options.skipTerminalLogReplay || !Array.isArray(result?.logs)
1224
+ ? null
1225
+ : terminalReplayPayloadForMode({
1226
+ resultLogs: result.logs,
1227
+ durableLogMode,
1228
+ liveLogs,
1229
+ liveLogsTotalCount,
1230
+ replayAllTerminalLogs: options.replayAllTerminalLogs === true,
1231
+ producerAttempt:
1232
+ options.terminalLogProducerAttempt ?? producerAttempt,
1233
+ }),
1234
+ });
1235
+ };
1236
+
1237
+ return {
1238
+ onRunnerEvent,
1239
+ applyDurableObservations,
1240
+ applyTerminalSheetOutcomes,
1241
+ finalize,
1242
+ };
1243
+ }
1244
+
1245
+ export const createDirectWorkerProgressReporter = (input: {
1246
+ launch: PlaySchedulerSubmitInput;
1247
+ producerAttempt?: number;
1248
+ isCurrentAttempt?: () => boolean | Promise<boolean>;
1249
+ initialLiveLogTotalCount?: number;
1250
+ }) =>
1251
+ createPostgresSchedulerProgressReporter({
1252
+ ...input,
1253
+ durableLogMode: 'direct_worker',
1254
+ });
1255
+ export type DirectWorkerProgressReporter = PostgresSchedulerProgressReporter;