deepline 0.3.212 → 0.3.214

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/client.ts +137 -32
  2. package/dist/bundling-sources/sdk/src/compat.ts +2 -0
  3. package/dist/bundling-sources/sdk/src/deployment-protection.ts +19 -0
  4. package/dist/bundling-sources/sdk/src/http.ts +3 -7
  5. package/dist/bundling-sources/sdk/src/index.ts +7 -0
  6. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  7. package/dist/bundling-sources/sdk/src/types.ts +25 -0
  8. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +33 -7
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -1
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +53 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +187 -6
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +67 -37
  16. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +12 -3
  17. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -2
  18. package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +176 -0
  19. package/dist/cli/index.js +572 -92
  20. package/dist/cli/index.mjs +572 -92
  21. package/dist/index.d.mts +191 -3
  22. package/dist/index.d.ts +191 -3
  23. package/dist/index.js +67 -35
  24. package/dist/index.mjs +67 -35
  25. package/dist/install-integrity.json +2 -0
  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 +3 -2
@@ -1,5 +1,8 @@
1
1
  import { STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS } from '../../../plays/sandbox-runtime-limits';
2
2
  import { RUNTIME_SANDBOX_MEMORY_ABORT_CODE } from '../../run-failure';
3
+ import { PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER } from '../../runtime-contract';
4
+ import { RUNTIME_CLAIM_HEARTBEAT_DIVISOR } from '../../runtime-constants';
5
+ import { RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS } from '../../runner-app/runtime-heartbeat';
3
6
  import {
4
7
  RUNTIME_MEMORY_LEDGER_PATH_ENV,
5
8
  RUNTIME_MEMORY_PRESSURE_PATH_ENV,
@@ -116,11 +119,11 @@ export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
116
119
  RUNTIME_SANDBOX_MEMORY_ABORT_CODE;
117
120
 
118
121
  /**
119
- * Build the tiny parent process that owns the hard runtime deadline and the
120
- * sandbox memory envelope outside the customer-code Node event loop. A
121
- * synchronous infinite loop can block the runner's own timer, but it cannot
122
- * block this separate process; nor can a runner whose heap is thrashing run
123
- * its own memory watch reliably.
122
+ * Build the parent process that owns runner liveness, the hard runtime
123
+ * deadline, and the sandbox memory envelope outside the customer-code Node
124
+ * event loop. A synchronous infinite loop can block the runner's timers, but
125
+ * it cannot block this separate process; nor can a runner whose heap is
126
+ * thrashing run its own memory watch reliably.
124
127
  *
125
128
  * argv (after `node -e <source>`):
126
129
  * 1 runnerPath, 2 configPath, 3 runtimeStartedPath, 4 runtimeCompletedPath,
@@ -185,6 +188,178 @@ let settled = false;
185
188
  let forceKillTimer = null;
186
189
  let runtimeTimer = null;
187
190
  let terminalGraceTimer = null;
191
+ let heartbeatTimer = null;
192
+ let heartbeatController = null;
193
+ let heartbeatInFlight = false;
194
+ let heartbeatStopped = false;
195
+ let heartbeatConfigRead = false;
196
+ let heartbeatConfig = null;
197
+ let heartbeatLastSuccessAtMs = 0;
198
+ let heartbeatAttempt = 0;
199
+ let heartbeatLastErrorCode = null;
200
+ let heartbeatStopBeforeLeaseMs = 0;
201
+ let stopRequested = false;
202
+
203
+ function readRunnerHeartbeatConfig() {
204
+ if (heartbeatConfigRead) return heartbeatConfig;
205
+ heartbeatConfigRead = true;
206
+ try {
207
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
208
+ const context = config && config.context;
209
+ const push = context && context.runnerPushExecution;
210
+ const gatewayBaseUrl = String(context && context.receiptGatewayBaseUrl || '').replace(/\\/$/, '');
211
+ const executorToken = context && context.executorToken;
212
+ const runId = push && push.runId;
213
+ const leaseSeconds = Number(push && push.leaseSeconds);
214
+ if (
215
+ !gatewayBaseUrl ||
216
+ typeof executorToken !== 'string' ||
217
+ !executorToken.trim() ||
218
+ typeof runId !== 'string' ||
219
+ !runId.trim() ||
220
+ !Number.isFinite(leaseSeconds) ||
221
+ leaseSeconds <= 0
222
+ ) {
223
+ return null;
224
+ }
225
+ heartbeatConfig = {
226
+ gatewayBaseUrl,
227
+ executorToken,
228
+ runId,
229
+ leaseMs: Math.max(1_000, Math.floor(leaseSeconds * 1_000)),
230
+ };
231
+ } catch {}
232
+ return heartbeatConfig;
233
+ }
234
+
235
+ function stopRunnerHeartbeat() {
236
+ heartbeatStopped = true;
237
+ if (heartbeatTimer) {
238
+ clearInterval(heartbeatTimer);
239
+ heartbeatTimer = null;
240
+ }
241
+ if (heartbeatController) {
242
+ heartbeatController.abort();
243
+ heartbeatController = null;
244
+ }
245
+ }
246
+
247
+ function runnerHeartbeatErrorCode(error) {
248
+ const code = error && typeof error.code === 'string' ? error.code : '';
249
+ if (/^(?:E[A-Z0-9_]+|ERR_[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+)$/.test(code)) return code;
250
+ const name = error && typeof error.name === 'string' ? error.name : '';
251
+ if (name === 'AbortError' || name === 'TimeoutError') return name;
252
+ return 'unknown';
253
+ }
254
+
255
+ function startRunnerHeartbeatSupervisor() {
256
+ if (heartbeatTimer || heartbeatStopped || settled) return;
257
+ const config = readRunnerHeartbeatConfig();
258
+ if (!config) return;
259
+ let handoff = null;
260
+ try {
261
+ handoff = JSON.parse(fs.readFileSync(runtimeStartedPath, 'utf8'));
262
+ } catch {}
263
+ const handoffAt = Number(handoff && handoff.lastHeartbeatAtMs);
264
+ heartbeatLastSuccessAtMs = Number.isFinite(handoffAt) ? handoffAt : Date.now();
265
+ const cadenceMs = Math.max(250, Math.floor(config.leaseMs / ${RUNTIME_CLAIM_HEARTBEAT_DIVISOR}));
266
+ heartbeatStopBeforeLeaseMs = Math.max(
267
+ 0,
268
+ config.leaseMs - cadenceMs - ${RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS},
269
+ );
270
+ process.stderr.write(
271
+ '[play-runner.heartbeat] watchdog_owner_started ' +
272
+ JSON.stringify({ runId: config.runId, cadenceMs, handoffHeartbeatAgeMs: Math.max(0, Date.now() - heartbeatLastSuccessAtMs) }) +
273
+ '\\n',
274
+ );
275
+ void runnerHeartbeatTick();
276
+ heartbeatTimer = setInterval(runnerHeartbeatTick, cadenceMs);
277
+ heartbeatTimer.unref?.();
278
+ }
279
+
280
+ async function runnerHeartbeatTick() {
281
+ if (heartbeatStopped || heartbeatInFlight || settled || !child || child.exitCode !== null) return;
282
+ const config = readRunnerHeartbeatConfig();
283
+ if (!config) return;
284
+ heartbeatInFlight = true;
285
+ heartbeatAttempt += 1;
286
+ const startedAt = Date.now();
287
+ const controller = new AbortController();
288
+ heartbeatController = controller;
289
+ const timeout = setTimeout(() => controller.abort(), ${RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS});
290
+ try {
291
+ const response = await fetch(
292
+ config.gatewayBaseUrl + '/api/v2/plays/runtime',
293
+ {
294
+ method: 'POST',
295
+ headers: {
296
+ 'content-type': 'application/json',
297
+ authorization: 'Bearer ' + config.executorToken,
298
+ ${JSON.stringify(PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER)}: require('node:crypto').randomUUID(),
299
+ },
300
+ body: JSON.stringify({ action: 'runner_heartbeat', runId: config.runId }),
301
+ redirect: 'manual',
302
+ signal: controller.signal,
303
+ },
304
+ );
305
+ if (response.status >= 300 && response.status < 400) {
306
+ await response.body?.cancel().catch(() => undefined);
307
+ throw Object.assign(new Error('gateway_redirect'), { code: 'redirect_' + response.status });
308
+ }
309
+ if (!response.ok) {
310
+ await response.body?.cancel().catch(() => undefined);
311
+ throw Object.assign(new Error('gateway_http_error'), { code: 'http_' + response.status });
312
+ }
313
+ const body = await response.json().catch(() => null);
314
+ if (body && body.ok === false && ['cancelled', 'failed', 'completed'].includes(body.terminal)) {
315
+ process.stderr.write(
316
+ '[play-runner.heartbeat] watchdog_owner_terminal ' +
317
+ JSON.stringify({ runId: config.runId, terminal: body.terminal, attempt: heartbeatAttempt }) +
318
+ '\\n',
319
+ );
320
+ stopRunnerHeartbeat();
321
+ stopChild('runner_scheduler_terminal', { terminal: body.terminal, heartbeatAttempt });
322
+ return;
323
+ }
324
+ if (!body || body.ok !== true || (body.schedulerStatus !== undefined && !['queued', 'running', 'waiting'].includes(body.schedulerStatus))) {
325
+ throw Object.assign(new Error('gateway_protocol_error'), { code: 'protocol_error' });
326
+ }
327
+ heartbeatLastSuccessAtMs = Date.now();
328
+ heartbeatLastErrorCode = null;
329
+ } catch (error) {
330
+ if (heartbeatStopped || settled) return;
331
+ heartbeatLastErrorCode = controller.signal.aborted
332
+ ? 'TimeoutError'
333
+ : runnerHeartbeatErrorCode(error);
334
+ const heartbeatAgeMs = Math.max(0, Date.now() - heartbeatLastSuccessAtMs);
335
+ process.stderr.write(
336
+ '[play-runner.heartbeat] watchdog_owner_retrying ' +
337
+ JSON.stringify({ runId: config.runId, attempt: heartbeatAttempt, durationMs: Math.max(0, Date.now() - startedAt), heartbeatAgeMs, errorCode: heartbeatLastErrorCode }) +
338
+ '\\n',
339
+ );
340
+ // Stop before the scheduler's lease expires. A failed request can consume
341
+ // its full timeout, and the next cadence would otherwise let the scheduler
342
+ // win the terminal race and report an ambiguous runner loss.
343
+ if (heartbeatAgeMs >= heartbeatStopBeforeLeaseMs) {
344
+ stopRunnerHeartbeat();
345
+ const diagnostic = {
346
+ heartbeatAttempt,
347
+ heartbeatAgeMs,
348
+ leaseMs: config.leaseMs,
349
+ stopBeforeLeaseMs: heartbeatStopBeforeLeaseMs,
350
+ lastErrorCode: heartbeatLastErrorCode,
351
+ };
352
+ process.stderr.write(
353
+ 'RUNTIME_CONTROL_PLANE_UNAVAILABLE: ' + JSON.stringify(diagnostic) + '\\n',
354
+ );
355
+ stopChild('runner_heartbeat_unreachable', diagnostic);
356
+ }
357
+ } finally {
358
+ clearTimeout(timeout);
359
+ if (heartbeatController === controller) heartbeatController = null;
360
+ heartbeatInFlight = false;
361
+ }
362
+ }
188
363
 
189
364
  function writeTerminationDiagnostic(reason, details = {}) {
190
365
  if (!terminationDiagnosticPath) return;
@@ -204,6 +379,8 @@ function writeTerminationDiagnostic(reason, details = {}) {
204
379
  }
205
380
 
206
381
  function stopChild(reason, details) {
382
+ if (settled || stopRequested) return;
383
+ stopRequested = true;
207
384
  writeTerminationDiagnostic(reason, details);
208
385
  child.kill('SIGTERM');
209
386
  forceKillTimer = setTimeout(() => child.kill('SIGKILL'), ${RUNNER_TERMINATION_KILL_GRACE_MS});
@@ -391,7 +568,10 @@ function armTerminalGrace() {
391
568
  }
392
569
 
393
570
  const runtimeBoundaryPoll = setInterval(() => {
394
- if (fs.existsSync(runtimeStartedPath)) armRuntimeDeadline();
571
+ if (fs.existsSync(runtimeStartedPath)) {
572
+ armRuntimeDeadline();
573
+ startRunnerHeartbeatSupervisor();
574
+ }
395
575
  if (fs.existsSync(runtimeCompletedPath)) armTerminalGrace();
396
576
  }, 25);
397
577
  const startupTimer = setTimeout(() => {
@@ -413,6 +593,7 @@ function finish(code) {
413
593
  settled = true;
414
594
  clearInterval(runtimeBoundaryPoll);
415
595
  clearInterval(memoryWatch);
596
+ stopRunnerHeartbeat();
416
597
  clearTimeout(startupTimer);
417
598
  if (runtimeTimer) clearTimeout(runtimeTimer);
418
599
  if (terminalGraceTimer) clearTimeout(terminalGraceTimer);
@@ -60,9 +60,13 @@ const CONCURRENCY_OBSERVATION_DEDUP_RETENTION_MS = 24 * 60 * 60 * 1000;
60
60
  const CONCURRENCY_OBSERVER_CLEANUP_BATCH_SIZE = 128;
61
61
  const ACTIVE_LEASE_TTL_MS = 5 * 60 * 1000;
62
62
  const RATE_RESERVATION_TTL_MS = 5 * 60 * 1000;
63
- // A guessed initial rate must not prevent learning an hourly quota. This is a
64
- // numerical floor (one request/day), not a fraction of the catalog seed.
65
- const MIN_ADAPTIVE_RPS_FLOOR = 1 / 86_400;
63
+ // Known provider estimates should not strand later runs below five requests
64
+ // per minute. Explicit adaptive ceilings remain authoritative.
65
+ const MIN_ADAPTIVE_RPS_FLOOR = 5 / 60;
66
+ // Repeated 429s with no successful work back off to at most one probe/minute.
67
+ const MIN_NO_PROGRESS_PROBE_RPS = 1 / 60;
68
+ // Used only to keep wait-time division finite for persisted legacy state.
69
+ const MIN_SCHEDULE_RPS = 1 / 86_400;
66
70
  /**
67
71
  * Horizon a granted=0 caller is allowed to sleep toward a WHOLE block instead of
68
72
  * a single token. Waiting for one token means one permit per store round trip,
@@ -427,8 +431,8 @@ function normalizeRules(
427
431
  * per second; `capacity` is one window of burst. The adaptive `[min,max]` range
428
432
  * is derived value-first so the same wire shape drives both declared and
429
433
  * default-pacing buckets:
430
- * - min_rps = min(base, 1/86400)
431
- * (low-RPM rules must still be able to back down below their base)
434
+ * - min_rps = min(5/60, max)
435
+ * (a lower explicit adaptive maximum remains authoritative)
432
436
  * - max_rps = max(base, 100) (catalogued-rate recommendation)
433
437
  * - max_rps = explicit adaptiveMaxRps when supplied
434
438
  */
@@ -446,17 +450,16 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
446
450
  // Preserve its fractional refill rate, with dispatch times resolved in ms.
447
451
  const capacity = Math.max(1, rule.requestsPerWindow);
448
452
  const base = (rule.requestsPerWindow / rule.windowMs) * 1000;
449
- // Undeclared providers use the shared fallback rate as their operating
450
- // floor. Explicit provider Retry-After deadlines still block dispatch; a
451
- // prior learned AIMD rate must not leave later runs paced below the fallback.
452
- const minRps =
453
- rule.ruleId === 'default:provider-scope'
454
- ? base
455
- : Math.min(base, MIN_ADAPTIVE_RPS_FLOOR);
456
453
  const maxRps =
457
454
  rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > 0
458
455
  ? Math.max(rule.adaptiveMaxRps, base)
459
456
  : Math.max(base, DEFAULT_ADVISORY_PROVIDER_MAX_RPS);
457
+ // Undeclared providers keep their existing 10-RPS fallback. Other provider
458
+ // scopes use the five-RPM floor, capped by an explicit adaptive maximum.
459
+ const minRps =
460
+ rule.ruleId === 'default:provider-scope'
461
+ ? base
462
+ : Math.min(MIN_ADAPTIVE_RPS_FLOOR, maxRps);
460
463
  return {
461
464
  rule_id: rule.ruleId,
462
465
  capacity,
@@ -467,10 +470,20 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
467
470
  };
468
471
  }
469
472
 
473
+ function minimumRateRpsForRules(rules: readonly AcquirePacingRule[]): number {
474
+ if (rules.some((rule) => rule.ruleId === 'default:provider-scope')) {
475
+ return DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS;
476
+ }
477
+ const explicitAdaptiveMaxima = rules.flatMap((rule) => {
478
+ if (rule.adaptiveMaxRps == null || rule.adaptiveMaxRps <= 0) return [];
479
+ const base = (rule.requestsPerWindow / rule.windowMs) * 1_000;
480
+ return [Math.max(rule.adaptiveMaxRps, base)];
481
+ });
482
+ return Math.min(MIN_ADAPTIVE_RPS_FLOOR, ...explicitAdaptiveMaxima);
483
+ }
484
+
470
485
  function minimumRateSqlForRules(rules: readonly AcquirePacingRule[]): string {
471
- return rules.some((rule) => rule.ruleId === 'default:provider-scope')
472
- ? `${DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS}::float8`
473
- : 'NULL::float8';
486
+ return `${minimumRateRpsForRules(rules)}::float8`;
474
487
  }
475
488
 
476
489
  function positiveInt(value: number, label: string): number {
@@ -1053,7 +1066,7 @@ function fairShareSql(
1053
1066
  r.refill_per_sec,
1054
1067
  GREATEST(
1055
1068
  ${ADAPTIVE_BURST_HORIZON_MS},
1056
- CEIL(r.effective_capacity / GREATEST(r.refill_per_sec, ${MIN_ADAPTIVE_RPS_FLOOR}) * 1000)
1069
+ CEIL(r.effective_capacity / GREATEST(r.refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000)
1057
1070
  )::bigint AS window_ms
1058
1071
  FROM refreshed r
1059
1072
  ORDER BY r.refill_per_sec ASC, r.effective_capacity ASC
@@ -1453,14 +1466,14 @@ async function acquirePureRate(
1453
1466
  scheduled_capacity
1454
1467
  / GREATEST(
1455
1468
  next_refill_per_sec,
1456
- ${MIN_ADAPTIVE_RPS_FLOOR}
1469
+ ${MIN_SCHEDULE_RPS}
1457
1470
  )
1458
1471
  * 1000
1459
1472
  )
1460
1473
  )::bigint
1461
1474
  )
1462
1475
  )::bigint AS first_at_ms,
1463
- CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_ADAPTIVE_RPS_FLOOR})))::bigint AS spacing_ms,
1476
+ CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_SCHEDULE_RPS})))::bigint AS spacing_ms,
1464
1477
  GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
1465
1478
  FROM refreshed
1466
1479
  ),
@@ -1614,7 +1627,7 @@ async function acquirePureRate(
1614
1627
  FLOOR(new_refill_per_sec * ${BLOCK_WAIT_HORIZON_MS} / 1000.0)
1615
1628
  )
1616
1629
  ) - tokens_after_refill
1617
- ) / GREATEST(new_refill_per_sec, ${MIN_ADAPTIVE_RPS_FLOOR}) * 1000
1630
+ ) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000
1618
1631
  )
1619
1632
  )::bigint
1620
1633
  FROM updated
@@ -1973,14 +1986,14 @@ async function acquireWithConcurrency(
1973
1986
  scheduled_capacity
1974
1987
  / GREATEST(
1975
1988
  next_refill_per_sec,
1976
- ${MIN_ADAPTIVE_RPS_FLOOR}
1989
+ ${MIN_SCHEDULE_RPS}
1977
1990
  )
1978
1991
  * 1000
1979
1992
  )
1980
1993
  )::bigint
1981
1994
  )
1982
1995
  )::bigint AS first_at_ms,
1983
- CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_ADAPTIVE_RPS_FLOOR})))::bigint AS spacing_ms,
1996
+ CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_SCHEDULE_RPS})))::bigint AS spacing_ms,
1984
1997
  GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
1985
1998
  FROM refreshed
1986
1999
  ),
@@ -2162,7 +2175,7 @@ async function acquireWithConcurrency(
2162
2175
  WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
2163
2176
  ELSE (
2164
2177
  SELECT CEIL(
2165
- MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, ${MIN_ADAPTIVE_RPS_FLOOR}) * 1000)
2178
+ MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000)
2166
2179
  )::bigint
2167
2180
  FROM updated
2168
2181
  )
@@ -3034,11 +3047,11 @@ async function penalizeRateStateStatements(
3034
3047
  return emptyRateStatePenaltyTelemetry();
3035
3048
  }
3036
3049
  const rules = normalizeRules((input.rules ?? []) as AcquirePacingRule[]);
3037
- const minimumRateRps = rules.some(
3038
- (rule) => rule.ruleId === 'default:provider-scope',
3039
- )
3040
- ? DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS
3041
- : MIN_ADAPTIVE_RPS_FLOOR;
3050
+ const minimumRateRps = minimumRateRpsForRules(rules);
3051
+ const noProgressProbeRateRps = Math.min(
3052
+ MIN_NO_PROGRESS_PROBE_RPS,
3053
+ minimumRateRps,
3054
+ );
3042
3055
  await applyConcurrencyObservations(
3043
3056
  client,
3044
3057
  input,
@@ -3047,8 +3060,10 @@ async function penalizeRateStateStatements(
3047
3060
  concurrencyObservationBatchFrom(input.concurrencyObservationBatch),
3048
3061
  );
3049
3062
  // Lock/update the provider-scope hold before touching rate-rule rows, matching
3050
- // acquire's lock order. The absolute deadline is idempotent across transport
3051
- // retries and gates admission independently of the capped AIMD cooldown.
3063
+ // acquire's lock order. Retry-After remains an uncapped provider deadline;
3064
+ // repeated no-success 429s also extend the separate AIMD cooldown to the
3065
+ // one-minute probe interval so the five-RPM floor cannot turn into a retry
3066
+ // loop.
3052
3067
  // - refill_per_sec: multiplicative decrease, but ONLY once per active
3053
3068
  // cooldown window (halve-once guard: skip if already cooling), floored at
3054
3069
  // min_rps.
@@ -3105,7 +3120,7 @@ async function penalizeRateStateStatements(
3105
3120
  - COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
3106
3121
  END) AS cycle_ms,
3107
3122
  CASE
3108
- WHEN $5::bigint IS NOT NULL AND h.rate_successes <= 0 THEN
3123
+ WHEN h.rate_successes <= 0 AND ($5::bigint IS NOT NULL OR h.seen_backpressure) THEN
3109
3124
  CASE WHEN h.seen_backpressure THEN
3110
3125
  LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
3111
3126
  ELSE 1000 END
@@ -3117,20 +3132,25 @@ async function penalizeRateStateStatements(
3117
3132
  SELECT e.*,
3118
3133
  GREATEST(${minimumRateRps},
3119
3134
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
3120
- CASE WHEN $5::bigint IS NOT NULL
3135
+ CASE WHEN e.no_progress_probe_interval_ms IS NOT NULL
3121
3136
  -- A fresh provider deadline is not a throughput measurement.
3122
3137
  -- Repeated probes with no successful work are evidence that
3123
- -- the scope is still unavailable, however. Space subsequent
3124
- -- probes exponentially (up to one per minute) across every
3125
- -- run sharing this provider credential.
3126
- THEN 1000.0 / e.no_progress_probe_interval_ms
3138
+ -- the scope is still unavailable, even without Retry-After.
3139
+ -- Space them exponentially (up to one per minute) across all
3140
+ -- runs sharing this provider credential.
3141
+ THEN GREATEST(${noProgressProbeRateRps},
3142
+ 1000.0 / e.no_progress_probe_interval_ms)
3127
3143
  ELSE e.refill_before END
3128
3144
  ELSE e.refill_before / 2 END
3129
3145
  ) AS next_ceiling_rps
3130
3146
  FROM evidence e
3131
3147
  ), learned AS (
3132
3148
  SELECT e.*,
3133
- CASE WHEN e.new_episode THEN GREATEST(${minimumRateRps}, LEAST(
3149
+ CASE WHEN e.new_episode THEN GREATEST(
3150
+ CASE WHEN e.seen_backpressure AND e.rate_successes = 0
3151
+ THEN ${noProgressProbeRateRps}
3152
+ ELSE ${minimumRateRps} END,
3153
+ LEAST(
3134
3154
  e.next_ceiling_rps,
3135
3155
  GREATEST(1, e.rate_successes) * 1000.0 / e.cycle_ms
3136
3156
  )) ELSE e.refill_before END AS refill_after
@@ -3140,7 +3160,17 @@ async function penalizeRateStateStatements(
3140
3160
  retry_at_ms = e.deadline,
3141
3161
  refill_per_sec = e.refill_after,
3142
3162
  seen_backpressure = e.seen_backpressure OR e.new_episode,
3143
- cool_until_ms = GREATEST(e.cool_until_ms, LEAST(e.deadline, e.now_ms + LEAST($2::bigint, $4::bigint))),
3163
+ -- A repeated no-success throttle can impose a slower local probe
3164
+ -- interval than the normal adaptive floor. Keep that pause separate
3165
+ -- from retry_at_ms, which remains the provider's absolute deadline.
3166
+ cool_until_ms = GREATEST(e.cool_until_ms, LEAST(
3167
+ e.now_ms + $4::bigint,
3168
+ GREATEST(
3169
+ e.deadline,
3170
+ e.now_ms + LEAST($2::bigint, $4::bigint),
3171
+ e.now_ms + COALESCE(e.no_progress_probe_interval_ms, 0)
3172
+ )
3173
+ )),
3144
3174
  tokens = CASE WHEN e.new_episode THEN 0 ELSE e.tokens END,
3145
3175
  rate_successes = e.rate_successes,
3146
3176
  rate_throttle_ceiling_rps = CASE WHEN e.new_episode
@@ -13891,24 +13891,33 @@ export async function upsertPostgresSchedulerObservation(
13891
13891
  /** Read every observation for a run/attempt, ordered for deterministic folds. */
13892
13892
  export async function getPostgresSchedulerObservations(
13893
13893
  client: PostgresSchedulerQueryClient,
13894
- input: { runId: string; attempt?: number | null },
13894
+ input: {
13895
+ runId: string;
13896
+ attempt?: number | null;
13897
+ /** Return only the newest settled invocation for each node. */
13898
+ latestSettledPerNode?: boolean;
13899
+ },
13895
13900
  options: PostgresSchedulerReceiptOptions,
13896
13901
  ): Promise<DocflowObservationRecord[]> {
13897
13902
  const hasAttempt = input.attempt != null;
13903
+ const latestSettledPerNode = input.latestSettledPerNode === true;
13898
13904
  const params: unknown[] = [receiptOrgId(options), input.runId];
13899
13905
  if (hasAttempt) {
13900
13906
  params.push(normalizePostgresSchedulerReceiptRunAttempt(input.attempt));
13901
13907
  }
13902
13908
  const result = await client.query<ObservationRow>(
13903
13909
  `
13904
- SELECT run_id, attempt, node_id, invocation_seq, invocation_id, status,
13910
+ SELECT ${latestSettledPerNode ? 'DISTINCT ON (node_id)' : ''}
13911
+ run_id, attempt, node_id, invocation_seq, invocation_id, status,
13905
13912
  inputs_json, outputs_json, inputs_truncated, outputs_truncated,
13906
13913
  error, started_at, settled_at
13907
13914
  FROM ${tableName('observations', options)}
13908
13915
  WHERE org_id = $1
13909
13916
  AND run_id = $2
13910
13917
  ${hasAttempt ? 'AND attempt = $3' : ''}
13911
- ORDER BY node_id, attempt, invocation_seq
13918
+ ${latestSettledPerNode ? "AND status IN ('completed', 'failed')" : ''}
13919
+ ORDER BY node_id, attempt ${latestSettledPerNode ? 'DESC' : ''},
13920
+ invocation_seq ${latestSettledPerNode ? 'DESC' : ''}
13912
13921
  `,
13913
13922
  params,
13914
13923
  );
@@ -219,11 +219,14 @@ export function runtimeSheetPageTailHoldCompletedEvent(
219
219
  tableNamespace: string,
220
220
  ): PlayRunnerEvent | null {
221
221
  if (!Number.isInteger(holdMs) || (holdMs ?? 0) <= 0) return null;
222
+ const at = new Date().toISOString();
222
223
  return {
223
224
  type: 'log',
224
- at: new Date().toISOString(),
225
+ at,
225
226
  source: 'play',
226
- line: `[runtime.sheet-page-tail-hold] phase=finish table=${tableNamespace} hold_ms=${holdMs}`,
227
+ // The status projection keeps the line but drops the event envelope's
228
+ // timestamp, so include the emission time for runtime-preview diagnostics.
229
+ line: `[${at}] [runtime.sheet-page-tail-hold] phase=finish table=${tableNamespace} hold_ms=${holdMs}`,
227
230
  };
228
231
  }
229
232