deepline 0.3.229 → 0.3.231

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 (30) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +19 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +47 -32
  5. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +3 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +38 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +8 -3
  8. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +39 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +293 -4
  11. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +29 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +50 -10
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +1 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +4 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +4 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +187 -14
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +52 -2
  18. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +5 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +30 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +118 -6
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +135 -21
  22. package/dist/cli/index.js +109 -7
  23. package/dist/cli/index.mjs +109 -7
  24. package/dist/index.js +93 -2
  25. package/dist/index.mjs +93 -2
  26. package/dist/release.d.mts +1 -1
  27. package/dist/release.d.ts +1 -1
  28. package/dist/release.js +1 -1
  29. package/dist/release.mjs +1 -1
  30. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  * One capacity policy for the Absurd runtime.
3
3
  *
4
4
  * Postgres owns backlog. The active lane adds workers from durable claimable
5
- * demand and oldest-claimable age; historical lanes stay cold unless they own
5
+ * demand; historical lanes stay cold unless they own
6
6
  * runnable work. Keep these values together so the worker, scaler, gateway,
7
7
  * and tests cannot independently invent concurrency or timeout budgets.
8
8
  */
@@ -19,14 +19,20 @@ export const RUNTIME_CAPACITY_POLICY = {
19
19
  /** Warm floor for availability and deploy rollovers. */
20
20
  activeLaneMachines: 2,
21
21
  /**
22
- * Keep the fleet below the PlanetScale direct-session budget. Each worker
22
+ * Bound active-lane burst demand. Each worker
23
23
  * has one elected direct wake listener and a two-client pooled scheduler
24
24
  * budget; scaling past this point amplifies connection pressure faster
25
25
  * than it adds useful control-plane throughput.
26
26
  */
27
27
  maxActiveLaneMachines: 8,
28
+ /**
29
+ * Shared started/pending worker budget across every release lane. Existing
30
+ * claim owners and rollback holds are retained; restoring the active warm
31
+ * floor may temporarily exceed this budget without forcing any worker off.
32
+ */
33
+ maxFleetMachines: 8,
28
34
  workerSlotsPerMachine: 8,
29
- /** React within one control interval when a due backlog is not draining. */
35
+ /** Queue-age diagnostic threshold; age alone never grants worker slots. */
30
36
  scaleUpQueueAgeMs: 1_000,
31
37
  /** Bound one Fly reconciliation without turning backlog into a stampede. */
32
38
  maxScaleUpMachinesPerReconcile: 4,
@@ -75,9 +81,9 @@ export const ABSURD_GLOBAL_RUN_CONCURRENCY =
75
81
  RUNTIME_CAPACITY_POLICY.absurd.workerSlotsPerMachine;
76
82
 
77
83
  /**
78
- * Active releases use pg-boss-style burst semantics: claimable count sets the
79
- * target, an aged claimable queue forces continued growth, and growth per
80
- * observation is bounded. A short/empty observation ends burst growth; the
84
+ * Active releases use claimable plus running count to set the target, with
85
+ * growth per observation bounded. Queue age is diagnostic, not additional
86
+ * runnable demand. A short/empty observation ends burst growth; the
81
87
  * caller supplies idleForMs to hold excess capacity through a quiet window.
82
88
  *
83
89
  * Historical releases stay cold until they have immediately runnable or
@@ -105,14 +111,6 @@ export function desiredAbsurdLaneMachines(input: {
105
111
  );
106
112
  let desired = Math.max(policy.activeLaneMachines, demandedMachines);
107
113
 
108
- if (
109
- claimableRuns > 0 &&
110
- nonNegativeNumber(input.oldestClaimableAgeMs ?? 0) >=
111
- policy.scaleUpQueueAgeMs
112
- ) {
113
- desired = Math.max(desired, currentMachines + 1);
114
- }
115
-
116
114
  desired = Math.min(
117
115
  desired,
118
116
  currentMachines + policy.maxScaleUpMachinesPerReconcile,
@@ -129,6 +127,172 @@ export function desiredAbsurdLaneMachines(input: {
129
127
  return desired;
130
128
  }
131
129
 
130
+ /**
131
+ * Allocate only requested starts/creates against the physical worker fleet.
132
+ * The caller counts all started and pending Machines, including drainers,
133
+ * unhealthy workers, fresh claim owners, and rollback holds. Planned stops do
134
+ * not free budget until Fly confirms them. This function never removes workers.
135
+ *
136
+ * Restore the active warm floor first, even when that reports budget debt.
137
+ * Give freed budget to historical runnable lanes in round-robin order before
138
+ * active burst growth. Persist nextCursor between reconciliations so repeated
139
+ * single-slot openings do not always favor the first historical release.
140
+ */
141
+ export function planAbsurdWorkerFleetGrowth(input: {
142
+ existingMachines: number;
143
+ maxFleetMachines?: number;
144
+ activeWarmFloorMissing: number;
145
+ demands: readonly {
146
+ releaseId: string;
147
+ active: boolean;
148
+ startMachineIds: readonly string[];
149
+ createCount: number;
150
+ }[];
151
+ cursor?: string;
152
+ }): {
153
+ allocations: {
154
+ releaseId: string;
155
+ startMachineIds: string[];
156
+ createCount: number;
157
+ deferredCount: number;
158
+ }[];
159
+ nextCursor: string;
160
+ physicalCount: number;
161
+ projectedPhysicalCount: number;
162
+ budgetDebt: number;
163
+ deferredCount: number;
164
+ warmFloorGrowth: number;
165
+ maxFleetMachines: number;
166
+ } {
167
+ const physicalCount = requireMachineCount(input.existingMachines);
168
+ const maxFleetMachines = requireMachineCount(
169
+ input.maxFleetMachines ?? RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines,
170
+ );
171
+ const warmFloorMissing = requireMachineCount(input.activeWarmFloorMissing);
172
+ const cursor = input.cursor ?? '';
173
+ if (typeof cursor !== 'string') {
174
+ throw new Error('Worker fleet cursor must be a release ID.');
175
+ }
176
+ const releases = new Set<string>();
177
+ const machines = new Set<string>();
178
+ const lanes = input.demands.map((demand) => {
179
+ if (!demand.releaseId || releases.has(demand.releaseId)) {
180
+ throw new Error('Worker fleet demands must have unique release IDs.');
181
+ }
182
+ releases.add(demand.releaseId);
183
+ for (const machineId of demand.startMachineIds) {
184
+ if (!machineId || machines.has(machineId)) {
185
+ throw new Error('Worker fleet starts must have unique Machine IDs.');
186
+ }
187
+ machines.add(machineId);
188
+ }
189
+ const createCount = requireMachineCount(demand.createCount);
190
+ return {
191
+ demand,
192
+ remaining: requireMachineCount(
193
+ demand.startMachineIds.length + createCount,
194
+ ),
195
+ allocation: {
196
+ releaseId: demand.releaseId,
197
+ startMachineIds: [] as string[],
198
+ createCount: 0,
199
+ deferredCount: 0,
200
+ },
201
+ };
202
+ });
203
+ const activeLanes = lanes.filter((lane) => lane.demand.active);
204
+ if (
205
+ activeLanes.length > 1 ||
206
+ (warmFloorMissing > 0 && activeLanes.length !== 1)
207
+ ) {
208
+ throw new Error('Worker fleet warm capacity requires one active release.');
209
+ }
210
+ const activeLane = activeLanes[0];
211
+ const grant = (lane: (typeof lanes)[number]) => {
212
+ const startIndex = lane.allocation.startMachineIds.length;
213
+ if (startIndex < lane.demand.startMachineIds.length) {
214
+ lane.allocation.startMachineIds.push(
215
+ lane.demand.startMachineIds[startIndex],
216
+ );
217
+ } else {
218
+ lane.allocation.createCount += 1;
219
+ }
220
+ lane.remaining -= 1;
221
+ };
222
+ const warmFloorGrowth = Math.min(
223
+ warmFloorMissing,
224
+ activeLane?.remaining ?? 0,
225
+ );
226
+ for (let seat = 0; seat < warmFloorGrowth; seat += 1) grant(activeLane!);
227
+ let available = Math.max(
228
+ 0,
229
+ maxFleetMachines - physicalCount - warmFloorGrowth,
230
+ );
231
+ const historical = lanes
232
+ .filter((lane) => !lane.demand.active && lane.remaining > 0)
233
+ .sort((a, b) =>
234
+ a.demand.releaseId < b.demand.releaseId
235
+ ? -1
236
+ : a.demand.releaseId > b.demand.releaseId
237
+ ? 1
238
+ : 0,
239
+ );
240
+ let nextCursor = cursor;
241
+ let historicalRemaining = historical.reduce(
242
+ (sum, lane) => sum + lane.remaining,
243
+ 0,
244
+ );
245
+ // The requester set changes as lanes run out of work or disappear. A
246
+ // positional cursor would silently move across releases and can repeatedly
247
+ // skip a continuously waiting lane. Resume after the last granted identity.
248
+ const afterCursor = historical.findIndex(
249
+ (lane) => lane.demand.releaseId > cursor,
250
+ );
251
+ let index = afterCursor >= 0 ? afterCursor : 0;
252
+ while (available > 0 && historicalRemaining > 0) {
253
+ const lane = historical[index];
254
+ if (lane.remaining > 0) {
255
+ grant(lane);
256
+ available -= 1;
257
+ historicalRemaining -= 1;
258
+ nextCursor = lane.demand.releaseId;
259
+ }
260
+ index = (index + 1) % historical.length;
261
+ }
262
+ while (available > 0 && activeLane && activeLane.remaining > 0) {
263
+ grant(activeLane);
264
+ available -= 1;
265
+ }
266
+ let allocatedCount = 0;
267
+ let deferredCount = 0;
268
+ const allocations = lanes.map((lane) => {
269
+ allocatedCount +=
270
+ lane.allocation.startMachineIds.length + lane.allocation.createCount;
271
+ deferredCount += lane.remaining;
272
+ return { ...lane.allocation, deferredCount: lane.remaining };
273
+ });
274
+ const projectedPhysicalCount = requireMachineCount(
275
+ physicalCount + allocatedCount,
276
+ );
277
+ return {
278
+ allocations,
279
+ nextCursor,
280
+ physicalCount,
281
+ projectedPhysicalCount,
282
+ budgetDebt: Math.max(0, projectedPhysicalCount - maxFleetMachines),
283
+ deferredCount,
284
+ warmFloorGrowth,
285
+ maxFleetMachines,
286
+ };
287
+ }
288
+
289
+ function requireMachineCount(value: number): number {
290
+ if (!Number.isSafeInteger(value) || value < 0) {
291
+ throw new Error('Worker fleet counts must be nonnegative safe integers.');
292
+ }
293
+ return value;
294
+ }
295
+
132
296
  /**
133
297
  * Execution-relay demand is the exact upstream admission state, not request
134
298
  * count. Active permits consume capacity and waiting requests are an early
@@ -246,6 +410,15 @@ export function assertRuntimeCapacityPolicy(): void {
246
410
  'Active-lane minimum Machines cannot exceed the active-lane maximum.',
247
411
  );
248
412
  }
413
+ if (
414
+ !Number.isSafeInteger(RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines) ||
415
+ RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines <
416
+ RUNTIME_CAPACITY_POLICY.absurd.activeLaneMachines
417
+ ) {
418
+ throw new Error(
419
+ 'Worker fleet budget must cover the active-lane warm floor.',
420
+ );
421
+ }
249
422
  const relay = RUNTIME_CAPACITY_POLICY.executionRelay;
250
423
  if (
251
424
  relay.minimumMachines > relay.maxMachines ||
@@ -2,8 +2,25 @@
2
2
  export type RuntimeWorkCounters = {
3
3
  logicalCalls: number;
4
4
  providerRequests: number;
5
+ /** Older resource snapshots did not retain this scalar. */
6
+ provider429s?: number;
5
7
  };
6
8
 
9
+ /** Sequence actual runner invocations within one scheduler generation. */
10
+ export function nextWorkCounterInvocationSeq(
11
+ checkpoint: unknown,
12
+ attempt: number,
13
+ ): number {
14
+ const previous = record(checkpoint);
15
+ if (previous?.workCounterInvocationAttempt !== attempt) return 1;
16
+ const previousSeq = count(previous.workCounterInvocationSeq);
17
+ if (previousSeq === null) return 1;
18
+ if (previousSeq === Number.MAX_SAFE_INTEGER) {
19
+ throw new Error('Work counter invocation sequence exhausted.');
20
+ }
21
+ return previousSeq + 1;
22
+ }
23
+
7
24
  function record(value: unknown): Record<string, unknown> | null {
8
25
  return value !== null && typeof value === 'object' && !Array.isArray(value)
9
26
  ? (value as Record<string, unknown>)
@@ -16,6 +33,26 @@ function count(value: unknown): number | null {
16
33
  : null;
17
34
  }
18
35
 
36
+ export function runtimeWorkCountersFromStats(
37
+ stats: unknown,
38
+ ): RuntimeWorkCounters | null {
39
+ const root = record(stats);
40
+ const compact = record(root?.runtimeWorkCounters);
41
+ if (compact) {
42
+ const logicalCalls = count(compact.logicalCalls);
43
+ const providerRequests = count(compact.providerRequests);
44
+ const provider429s = count(compact.provider429s);
45
+ if (
46
+ logicalCalls !== null &&
47
+ providerRequests !== null &&
48
+ provider429s !== null
49
+ ) {
50
+ return { logicalCalls, providerRequests, provider429s };
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
19
56
  export function runtimeWorkCountersFromResources(
20
57
  resources: unknown,
21
58
  ): RuntimeWorkCounters | null {
@@ -26,12 +63,25 @@ export function runtimeWorkCountersFromResources(
26
63
  if (logicalCalls === null || !providers) return null;
27
64
 
28
65
  let providerRequests = 0;
66
+ let provider429s = 0;
67
+ let provider429sKnown = true;
29
68
  for (const [key, value] of Object.entries(providers)) {
30
69
  if (!key.startsWith('tool:')) continue;
31
70
  const requests = count(record(record(value)?.residence)?.count);
32
71
  if (requests === null) return null;
33
72
  providerRequests += requests;
34
- if (!Number.isSafeInteger(providerRequests)) return null;
73
+ const rejected = count(record(value)?.provider429Count);
74
+ if (rejected === null) provider429sKnown = false;
75
+ else provider429s += rejected;
76
+ if (
77
+ !Number.isSafeInteger(providerRequests) ||
78
+ !Number.isSafeInteger(provider429s)
79
+ )
80
+ return null;
35
81
  }
36
- return { logicalCalls, providerRequests };
82
+ return {
83
+ logicalCalls,
84
+ providerRequests,
85
+ ...(provider429sKnown ? { provider429s } : {}),
86
+ };
37
87
  }
@@ -12,6 +12,7 @@ import type {
12
12
  } from '../ctx-types';
13
13
  import type {
14
14
  PlayRunnerExecutionConfig,
15
+ PlayRunnerEvent,
15
16
  PlayRunnerLogEvent,
16
17
  PlayRunnerResult,
17
18
  } from '../protocol';
@@ -37,6 +38,9 @@ export type SandboxRunnerProgress = {
37
38
  onCheckpoint?: (checkpoint: PlayCheckpoint) => void;
38
39
  onRowUpdate?: (update: PlayRowUpdate) => void;
39
40
  onExecutionEvent?: (event: PlayExecutionEvent) => void;
41
+ onWorkCounters?: (
42
+ event: Extract<PlayRunnerEvent, { type: 'work_counters' }>,
43
+ ) => void;
40
44
  };
41
45
 
42
46
  /**
@@ -147,6 +151,7 @@ function createRunnerCallbacks(input: {
147
151
  onCheckpoint: input.progress?.onCheckpoint,
148
152
  onRowUpdate: input.progress?.onRowUpdate,
149
153
  onExecutionEvent: input.progress?.onExecutionEvent,
154
+ onWorkCounters: input.progress?.onWorkCounters,
150
155
  readDetachedRunnerReadiness: async (baselineHeartbeatAt) =>
151
156
  await input.readiness({ baselineHeartbeatAt }),
152
157
  onRuntimeResourceAcquired: async (resource) => {
@@ -2764,8 +2764,34 @@ async function executeAssignedAbsurdAttempt(input: {
2764
2764
  replayAllTerminalLogs: input.fromDetachedRunner,
2765
2765
  terminalLogProducerAttempt: input.terminalLogProducerAttempt,
2766
2766
  });
2767
- const suspendedCheckpoint =
2768
- input.runnerResult.checkpoint ?? checkpoint ?? null;
2767
+ const suspendedCheckpoint = absurdCheckpointBase(
2768
+ input.runnerResult.checkpoint ?? checkpoint ?? null,
2769
+ );
2770
+ const segment = input.runnerResult.stats?.runtimeWorkCounterSegment as
2771
+ | { invocationSeq?: unknown; segmentId?: unknown }
2772
+ | undefined;
2773
+ if (
2774
+ typeof segment?.invocationSeq === 'number' &&
2775
+ Number.isSafeInteger(segment.invocationSeq) &&
2776
+ segment.invocationSeq >= 1 &&
2777
+ typeof segment.segmentId === 'string' &&
2778
+ segment.segmentId.length > 0
2779
+ ) {
2780
+ // The checkpoint is persisted by the existing suspension transaction.
2781
+ // A resumed runner in this generation gets the next segment sequence;
2782
+ // a crash before that commit cannot silently reuse a finalized segment.
2783
+ const previous = suspendedCheckpoint.workCounterInvocationSeq;
2784
+ suspendedCheckpoint.workCounterInvocationSeq = Math.max(
2785
+ suspendedCheckpoint.workCounterInvocationAttempt === generation &&
2786
+ typeof previous === 'number' &&
2787
+ Number.isSafeInteger(previous) &&
2788
+ previous >= 0
2789
+ ? previous
2790
+ : 0,
2791
+ segment.invocationSeq,
2792
+ );
2793
+ suspendedCheckpoint.workCounterInvocationAttempt = generation;
2794
+ }
2769
2795
  const engine = createAbsurdEngineSuspensionAdapter({
2770
2796
  ctx,
2771
2797
  runId,
@@ -3439,6 +3465,8 @@ async function executeAssignedAbsurdAttempt(input: {
3439
3465
  type: 'execution_event',
3440
3466
  event,
3441
3467
  }),
3468
+ onWorkCounters: (event) =>
3469
+ progressReporter.onRunnerEvent(event),
3442
3470
  },
3443
3471
  interpretResult: ({ result }) => {
3444
3472
  throwRetryableRuntimeInitializationFailure(result);
@@ -26,6 +26,7 @@ import type {
26
26
  } from '../protocol';
27
27
  import type { PlaySchedulerSubmitInput } from '../scheduler-backend';
28
28
  import type { DocflowObservationRecord } from '../docflow-observation';
29
+ import { runtimeWorkCountersFromStats } from '../runtime-work-counters';
29
30
  import {
30
31
  buildPlayRunLedgerEventsFromStatusPatch,
31
32
  createEmptyPlayRunLedgerSnapshot,
@@ -231,8 +232,7 @@ export type PostgresSchedulerProgressReporter = {
231
232
  };
232
233
 
233
234
  export type PostgresSchedulerDurableLogMode =
234
- | 'progress_reporter'
235
- | 'direct_worker';
235
+ 'progress_reporter' | 'direct_worker';
236
236
 
237
237
  function shouldPublishDurableLogEvent(
238
238
  mode: PostgresSchedulerDurableLogMode,
@@ -387,7 +387,10 @@ export function createPostgresSchedulerProgressReporter(input: {
387
387
  let liveLogs: string[] = [];
388
388
  let liveLogsTotalCount = input.initialLiveLogTotalCount ?? 0;
389
389
  let liveLogsDirty = false;
390
+ let workCounters: RuntimeStatusUpdate['workCounters'];
391
+ let workCountersDirty = false;
390
392
  let lastFlushAt = 0;
393
+ let delayedFlushTimer: ReturnType<typeof setTimeout> | null = null;
391
394
  let totalRows = 0;
392
395
  let activeMapNodeId: string | null = null;
393
396
  let pendingDatasetLifecycleEvents: Array<
@@ -464,9 +467,11 @@ export function createPostgresSchedulerProgressReporter(input: {
464
467
  const nextActivityObservations = [
465
468
  ...pendingActivityObservations.values(),
466
469
  ];
470
+ const nextWorkCounters = workCountersDirty ? workCounters : undefined;
467
471
  pendingActivityObservations.clear();
468
472
  liveNodeProgressDirty = false;
469
473
  liveLogsDirty = false;
474
+ workCountersDirty = false;
470
475
 
471
476
  try {
472
477
  if (await isCurrentAttempt()) {
@@ -479,6 +484,7 @@ export function createPostgresSchedulerProgressReporter(input: {
479
484
  liveLogProducerAttempt: producerAttempt,
480
485
  datasetLifecycleEvents: nextDatasetLifecycleEvents,
481
486
  activityObservations: nextActivityObservations,
487
+ workCounters: nextWorkCounters,
482
488
  terminalLogReplay: request.terminalLogReplay,
483
489
  lastCheckpointAt: Date.now(),
484
490
  });
@@ -489,6 +495,7 @@ export function createPostgresSchedulerProgressReporter(input: {
489
495
  datasetLifecycleEvents: nextDatasetLifecycleEvents,
490
496
  activityObservations: nextActivityObservations,
491
497
  });
498
+ workCountersDirty = true;
492
499
  console.warn('[postgres-scheduler.progress_flush_failed]', {
493
500
  runId: launch.runId,
494
501
  error: error instanceof Error ? error.message : String(error),
@@ -520,18 +527,35 @@ export function createPostgresSchedulerProgressReporter(input: {
520
527
  !force &&
521
528
  !liveNodeProgressDirty &&
522
529
  !liveLogsDirty &&
530
+ !workCountersDirty &&
523
531
  pendingDatasetLifecycleEvents.length === 0 &&
524
532
  pendingActivityObservations.size === 0
525
533
  ) {
534
+ if (delayedFlushTimer) {
535
+ clearTimeout(delayedFlushTimer);
536
+ delayedFlushTimer = null;
537
+ }
526
538
  return activeProgressUpload ?? Promise.resolve();
527
539
  }
528
540
  const now = Date.now();
529
- if (
530
- !force &&
531
- now - lastFlushAt < getAdaptiveRunEventIntervalMs(totalRows)
532
- ) {
541
+ const intervalMs = getAdaptiveRunEventIntervalMs(totalRows);
542
+ if (!force && now - lastFlushAt < intervalMs) {
543
+ if (!delayedFlushTimer) {
544
+ delayedFlushTimer = setTimeout(
545
+ () => {
546
+ delayedFlushTimer = null;
547
+ void flush().catch(() => undefined);
548
+ },
549
+ Math.max(1, intervalMs - (now - lastFlushAt)),
550
+ );
551
+ delayedFlushTimer.unref?.();
552
+ }
533
553
  return activeProgressUpload ?? Promise.resolve();
534
554
  }
555
+ if (delayedFlushTimer) {
556
+ clearTimeout(delayedFlushTimer);
557
+ delayedFlushTimer = null;
558
+ }
535
559
  lastFlushAt = now;
536
560
  const requested = new Promise<void>((resolve, reject) => {
537
561
  pendingProgressUpload = mergeProgressFlushRequest(
@@ -1057,6 +1081,62 @@ export function createPostgresSchedulerProgressReporter(input: {
1057
1081
  return;
1058
1082
  }
1059
1083
  const type = (event as { type?: unknown }).type;
1084
+ if (type === 'work_counters') {
1085
+ const observed = event as {
1086
+ attempt?: unknown;
1087
+ invocationSeq?: unknown;
1088
+ segmentId?: unknown;
1089
+ logicalCalls?: unknown;
1090
+ providerRequests?: unknown;
1091
+ provider429s?: unknown;
1092
+ };
1093
+ if (
1094
+ typeof observed.attempt !== 'number' ||
1095
+ !Number.isSafeInteger(observed.attempt) ||
1096
+ observed.attempt < 0 ||
1097
+ typeof observed.invocationSeq !== 'number' ||
1098
+ !Number.isSafeInteger(observed.invocationSeq) ||
1099
+ observed.invocationSeq < 1 ||
1100
+ typeof observed.segmentId !== 'string' ||
1101
+ observed.segmentId.length === 0 ||
1102
+ (producerAttempt !== undefined &&
1103
+ observed.attempt !== producerAttempt) ||
1104
+ ![
1105
+ observed.logicalCalls,
1106
+ observed.providerRequests,
1107
+ observed.provider429s,
1108
+ ].every(
1109
+ (value) =>
1110
+ typeof value === 'number' &&
1111
+ Number.isSafeInteger(value) &&
1112
+ value >= 0,
1113
+ )
1114
+ )
1115
+ return;
1116
+ const next = {
1117
+ attempt: observed.attempt,
1118
+ invocationSeq: observed.invocationSeq,
1119
+ segmentId: observed.segmentId,
1120
+ logicalCalls: observed.logicalCalls as number,
1121
+ providerRequests: observed.providerRequests as number,
1122
+ provider429s: observed.provider429s as number,
1123
+ final: false,
1124
+ };
1125
+ if (
1126
+ workCounters &&
1127
+ workCounters.attempt === next.attempt &&
1128
+ workCounters.invocationSeq === next.invocationSeq &&
1129
+ workCounters.segmentId === next.segmentId &&
1130
+ workCounters.logicalCalls >= next.logicalCalls &&
1131
+ workCounters.providerRequests >= next.providerRequests &&
1132
+ workCounters.provider429s >= next.provider429s
1133
+ )
1134
+ return;
1135
+ workCounters = next;
1136
+ workCountersDirty = true;
1137
+ scheduleFlush();
1138
+ return;
1139
+ }
1060
1140
  if (type === 'log') {
1061
1141
  const logEvent = event as { line?: unknown; source?: unknown };
1062
1142
  const rawLine = logEvent.line;
@@ -1291,6 +1371,29 @@ export function createPostgresSchedulerProgressReporter(input: {
1291
1371
  skipTerminalLogReplay?: boolean;
1292
1372
  } = {},
1293
1373
  ): Promise<void> => {
1374
+ const terminalCounters = runtimeWorkCountersFromStats(result?.stats);
1375
+ const terminalSegment = result?.stats?.runtimeWorkCounterSegment as
1376
+ { invocationSeq?: unknown; segmentId?: unknown } | undefined;
1377
+ if (
1378
+ terminalCounters &&
1379
+ terminalCounters.provider429s !== undefined &&
1380
+ typeof terminalSegment?.invocationSeq === 'number' &&
1381
+ Number.isSafeInteger(terminalSegment.invocationSeq) &&
1382
+ terminalSegment.invocationSeq >= 1 &&
1383
+ typeof terminalSegment.segmentId === 'string' &&
1384
+ terminalSegment.segmentId.length > 0
1385
+ ) {
1386
+ workCounters = {
1387
+ attempt: producerAttempt ?? workCounters?.attempt ?? 0,
1388
+ invocationSeq: terminalSegment.invocationSeq,
1389
+ segmentId: terminalSegment.segmentId,
1390
+ logicalCalls: terminalCounters.logicalCalls,
1391
+ providerRequests: terminalCounters.providerRequests,
1392
+ provider429s: terminalCounters.provider429s,
1393
+ final: true,
1394
+ };
1395
+ workCountersDirty = true;
1396
+ }
1294
1397
  // Mark started steps terminal with real timestamps. Static branches that
1295
1398
  // never started remain non-executed; suspended runs stay in-flight.
1296
1399
  if (result?.status === 'failed') {
@@ -1409,6 +1512,15 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1409
1512
  }),
1410
1513
  source: 'worker',
1411
1514
  });
1515
+ if (finalUpdate.workCounters) {
1516
+ progressEvents.push({
1517
+ type: 'work.counters',
1518
+ runId: input.launch.runId,
1519
+ source: 'worker',
1520
+ occurredAt: finalUpdate.lastCheckpointAt ?? Date.now(),
1521
+ ...finalUpdate.workCounters,
1522
+ });
1523
+ }
1412
1524
  const datasetEvents: PlayRunLedgerEvent[] = (
1413
1525
  finalUpdate.datasetLifecycleEvents ?? []
1414
1526
  ).map((event) => ({