deepline 0.1.212 → 0.1.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 (41) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +167 -329
  2. package/dist/bundling-sources/sdk/src/client.ts +2 -2
  3. package/dist/bundling-sources/sdk/src/index.ts +2 -0
  4. package/dist/bundling-sources/sdk/src/play.ts +8 -1
  5. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -1
  6. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  7. package/dist/bundling-sources/sdk/src/types.ts +3 -3
  8. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +49 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
  11. package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +209 -123
  13. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +30 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +160 -2
  16. package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +1 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +54 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +477 -85
  25. package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -2
  27. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
  28. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
  30. package/dist/cli/index.js +50 -436
  31. package/dist/cli/index.mjs +50 -436
  32. package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
  33. package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
  34. package/dist/index.d.mts +11 -8
  35. package/dist/index.d.ts +11 -8
  36. package/dist/index.js +4 -4
  37. package/dist/index.mjs +4 -4
  38. package/dist/plays/bundle-play-file.d.mts +2 -2
  39. package/dist/plays/bundle-play-file.d.ts +2 -2
  40. package/dist/plays/bundle-play-file.mjs +1 -1
  41. package/package.json +1 -1
@@ -105,7 +105,13 @@ import {
105
105
  PLAY_RUNTIME_TEST_FAULT_HEADER,
106
106
  type RuntimeTestFaultName,
107
107
  } from './test-runtime-seams';
108
+ import { COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID } from './durable-receipt-execution';
108
109
  import { vercelProtectionBypassHeader } from './vercel-protection';
110
+ import {
111
+ RuntimePostgresAdmission,
112
+ type RuntimePostgresAdmissionSnapshot,
113
+ type RuntimePostgresLane,
114
+ } from './runtime-postgres-admission';
109
115
 
110
116
  type RuntimeApiContext = {
111
117
  baseUrl?: string | null;
@@ -269,6 +275,7 @@ type RuntimeSheetPreparedRowDisposition = {
269
275
  const dbSessionCache = new Map<string, DbSessionCacheEntry>();
270
276
  const dbSessionInFlight = new Map<string, Promise<CreateDbSessionResponse>>();
271
277
  const postgresPools = new Map<string, RuntimePool>();
278
+ const runtimePostgresAdmissions = new Map<string, RuntimePostgresAdmission>();
272
279
  const runtimeWorkReceiptEnsureCache = new Map<string, Promise<void>>();
273
280
  const runtimeSheetEnsureCache = new Map<
274
281
  string,
@@ -327,7 +334,14 @@ const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS = 10_000;
327
334
  // Daytona executes one play per runner process. This is the bounded data-plane
328
335
  // budget for that one run's sheet reads/writes, receipts, and terminal/export
329
336
  // reads over the PgBouncer URL; it is not a scheduler/control-plane fan-out.
330
- const RUNTIME_POSTGRES_POOL_MAX_CONNECTIONS = 8;
337
+ // One runner gets four tenant-gateway clients, split evenly so a large sheet
338
+ // flush cannot starve receipt claims (and receipt traffic cannot block the
339
+ // persistence barrier). The admission queues add a driver-independent bound:
340
+ // Neon Pool and node-postgres may queue internally, but runtime callers never
341
+ // enter those unobservable queues without first holding a lane permit.
342
+ const RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE = 2;
343
+ const RUNTIME_POSTGRES_ADMISSION_MAX_QUEUED_PER_LANE = 2_000;
344
+ const RUNTIME_POSTGRES_ADMISSION_TIMEOUT_MS = 10_000;
331
345
  const RECEIPT_STATUS_QUEUED = RECEIPT_STATUS_CODE.queued;
332
346
  const RECEIPT_STATUS_PENDING = RECEIPT_STATUS_CODE.pending;
333
347
  const RECEIPT_STATUS_RUNNING = RECEIPT_STATUS_CODE.running;
@@ -369,6 +383,7 @@ export type PrepareRuntimeSheetResult = {
369
383
  attemptOwnerRunId?: string;
370
384
  attemptExpiresAt?: string;
371
385
  attemptSeq?: number;
386
+ writeVersion?: number;
372
387
  };
373
388
 
374
389
  export type RuntimeSheetTiming = {
@@ -1292,9 +1307,7 @@ async function deleteRuntimeDbSessionCacheEntry(
1292
1307
  ): Promise<void> {
1293
1308
  dbSessionCache.delete(cacheKey);
1294
1309
  if (session.postgresUrl) {
1295
- const pool = postgresPools.get(session.postgresUrl);
1296
- postgresPools.delete(session.postgresUrl);
1297
- await pool?.end().catch(() => {});
1310
+ await resetRuntimePostgresPool(session.postgresUrl);
1298
1311
  }
1299
1312
  }
1300
1313
 
@@ -1415,18 +1428,10 @@ async function prewarmRuntimePostgresSession(
1415
1428
  attempt >= RUNTIME_POSTGRES_PREWARM_MAX_ATTEMPTS ||
1416
1429
  !isTransientRuntimePostgresConnectionError(error)
1417
1430
  ) {
1418
- const pool = postgresPools.get(session.postgresUrl);
1419
- postgresPools.delete(session.postgresUrl);
1420
- if (pool) {
1421
- await Promise.resolve(pool.end()).catch(() => {});
1422
- }
1431
+ await resetRuntimePostgresPool(session.postgresUrl);
1423
1432
  throw error;
1424
1433
  }
1425
- const pool = postgresPools.get(session.postgresUrl);
1426
- postgresPools.delete(session.postgresUrl);
1427
- if (pool) {
1428
- await Promise.resolve(pool.end()).catch(() => {});
1429
- }
1434
+ await resetRuntimePostgresPool(session.postgresUrl);
1430
1435
  await sleep(
1431
1436
  RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS[attempt - 1] ??
1432
1437
  RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS[
@@ -1491,7 +1496,10 @@ function sleep(ms: number): Promise<void> {
1491
1496
 
1492
1497
  async function connectRuntimePostgresPool(
1493
1498
  pool: RuntimePool,
1499
+ admission: RuntimePostgresAdmission,
1500
+ lane: RuntimePostgresLane,
1494
1501
  ): Promise<RuntimePoolClient> {
1502
+ const releaseAdmission = await admission.acquire(lane);
1495
1503
  let timedOut = false;
1496
1504
  let timeout: ReturnType<typeof setTimeout> | null = null;
1497
1505
  const connectPromise = pool.connect();
@@ -1499,11 +1507,12 @@ async function connectRuntimePostgresPool(
1499
1507
  .then((client) => {
1500
1508
  if (timedOut) {
1501
1509
  client.release();
1510
+ releaseAdmission();
1502
1511
  }
1503
1512
  })
1504
1513
  .catch(() => {});
1505
1514
  try {
1506
- return await Promise.race([
1515
+ const client = await Promise.race([
1507
1516
  connectPromise,
1508
1517
  new Promise<never>((_, reject) => {
1509
1518
  timeout = setTimeout(() => {
@@ -1516,6 +1525,21 @@ async function connectRuntimePostgresPool(
1516
1525
  }, RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS);
1517
1526
  }),
1518
1527
  ]);
1528
+ let released = false;
1529
+ return {
1530
+ query: (text, params) => client.query(text, params),
1531
+ release: () => {
1532
+ if (released) return;
1533
+ released = true;
1534
+ client.release();
1535
+ releaseAdmission();
1536
+ },
1537
+ };
1538
+ } catch (error) {
1539
+ if (!timedOut) {
1540
+ releaseAdmission();
1541
+ }
1542
+ throw error;
1519
1543
  } finally {
1520
1544
  if (timeout) {
1521
1545
  clearTimeout(timeout);
@@ -1523,35 +1547,81 @@ async function connectRuntimePostgresPool(
1523
1547
  }
1524
1548
  }
1525
1549
 
1526
- function getPostgresPool(postgresUrl: string, cachePool = true): RuntimePool {
1550
+ function runtimePostgresLane(
1551
+ session: RuntimePostgresSession,
1552
+ ): RuntimePostgresLane {
1553
+ return session.target.logicalTable === RUNTIME_WORK_RECEIPT_LOGICAL_TABLE
1554
+ ? 'receipts'
1555
+ : 'sheets';
1556
+ }
1557
+
1558
+ function runtimePostgresPoolKey(
1559
+ postgresUrl: string,
1560
+ lane: RuntimePostgresLane,
1561
+ ): string {
1562
+ return `${postgresUrl}::${lane}`;
1563
+ }
1564
+
1565
+ function getRuntimePostgresAdmission(
1566
+ postgresUrl: string,
1567
+ ): RuntimePostgresAdmission {
1568
+ const existing = runtimePostgresAdmissions.get(postgresUrl);
1569
+ if (existing) return existing;
1570
+ const admission = new RuntimePostgresAdmission({
1571
+ maxActivePerLane: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
1572
+ maxQueuedPerLane: RUNTIME_POSTGRES_ADMISSION_MAX_QUEUED_PER_LANE,
1573
+ acquireTimeoutMs: RUNTIME_POSTGRES_ADMISSION_TIMEOUT_MS,
1574
+ });
1575
+ runtimePostgresAdmissions.set(postgresUrl, admission);
1576
+ return admission;
1577
+ }
1578
+
1579
+ export function runtimePostgresAdmissionTelemetry(
1580
+ postgresUrl: string,
1581
+ ): RuntimePostgresAdmissionSnapshot | null {
1582
+ return runtimePostgresAdmissions.get(postgresUrl)?.snapshot() ?? null;
1583
+ }
1584
+
1585
+ function getPostgresPool(
1586
+ postgresUrl: string,
1587
+ lane: RuntimePostgresLane,
1588
+ cachePool = true,
1589
+ ): RuntimePool {
1527
1590
  if (!cachePool) {
1528
1591
  return createRuntimePool({
1529
1592
  connectionString: postgresUrl,
1530
- maxConnections: 1,
1593
+ maxConnections: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
1531
1594
  idleTimeoutMs: 0,
1532
1595
  connectTimeoutMs: RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS,
1533
1596
  });
1534
1597
  }
1535
- const existing = postgresPools.get(postgresUrl);
1598
+ const poolKey = runtimePostgresPoolKey(postgresUrl, lane);
1599
+ const existing = postgresPools.get(poolKey);
1536
1600
  if (existing) {
1537
1601
  return existing;
1538
1602
  }
1539
1603
  const pool = createRuntimePool({
1540
1604
  connectionString: postgresUrl,
1541
- maxConnections: RUNTIME_POSTGRES_POOL_MAX_CONNECTIONS,
1605
+ maxConnections: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
1542
1606
  idleTimeoutMs: 15_000,
1543
1607
  connectTimeoutMs: RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS,
1544
1608
  });
1545
- postgresPools.set(postgresUrl, pool);
1609
+ postgresPools.set(poolKey, pool);
1546
1610
  return pool;
1547
1611
  }
1548
1612
 
1549
1613
  async function resetRuntimePostgresPool(postgresUrl: string): Promise<void> {
1550
- const pool = postgresPools.get(postgresUrl);
1551
- postgresPools.delete(postgresUrl);
1552
- if (pool) {
1553
- await Promise.resolve(pool.end()).catch(() => {});
1554
- }
1614
+ const pools = (['receipts', 'sheets'] as const)
1615
+ .map((lane) => runtimePostgresPoolKey(postgresUrl, lane))
1616
+ .map((key) => {
1617
+ const pool = postgresPools.get(key);
1618
+ postgresPools.delete(key);
1619
+ return pool;
1620
+ })
1621
+ .filter((pool): pool is RuntimePool => pool !== undefined);
1622
+ await Promise.all(
1623
+ pools.map((pool) => Promise.resolve(pool.end()).catch(() => {})),
1624
+ );
1555
1625
  }
1556
1626
 
1557
1627
  async function withRuntimePostgres<T>(
@@ -1567,13 +1637,15 @@ async function withRuntimePostgres<T>(
1567
1637
  options.maxConnectAttempts === undefined
1568
1638
  ? RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS
1569
1639
  : Math.max(1, Math.floor(options.maxConnectAttempts));
1640
+ const lane = runtimePostgresLane(session);
1641
+ const admission = getRuntimePostgresAdmission(session.postgresUrl);
1570
1642
  for (let attempt = 1; attempt <= maxConnectAttempts; attempt += 1) {
1571
1643
  try {
1572
- const pool = getPostgresPool(session.postgresUrl, cachePool);
1644
+ const pool = getPostgresPool(session.postgresUrl, lane, cachePool);
1573
1645
  if (!cachePool) {
1574
1646
  requestLocalPool = pool;
1575
1647
  }
1576
- client = await connectRuntimePostgresPool(pool);
1648
+ client = await connectRuntimePostgresPool(pool, admission, lane);
1577
1649
  if (session.executionRole) {
1578
1650
  await client.query(
1579
1651
  `SET ROLE ${quoteIdentifier(session.executionRole)}`,
@@ -1586,8 +1658,9 @@ async function withRuntimePostgres<T>(
1586
1658
  client = null;
1587
1659
  }
1588
1660
  if (cachePool) {
1589
- const pool = postgresPools.get(session.postgresUrl);
1590
- postgresPools.delete(session.postgresUrl);
1661
+ const poolKey = runtimePostgresPoolKey(session.postgresUrl, lane);
1662
+ const pool = postgresPools.get(poolKey);
1663
+ postgresPools.delete(poolKey);
1591
1664
  if (pool) {
1592
1665
  await Promise.resolve(pool.end()).catch(() => {});
1593
1666
  }
@@ -1889,6 +1962,10 @@ function workReceiptTable(session: RuntimePostgresSession): string {
1889
1962
  );
1890
1963
  }
1891
1964
 
1965
+ function receiptExecutionLockTable(session: RuntimePostgresSession): string {
1966
+ return fqRuntimeTable(session, '_deepline_receipt_execution_locks');
1967
+ }
1968
+
1892
1969
  function postgresUrlCacheKey(value: string): string {
1893
1970
  return createHash('sha256').update(value).digest('hex').slice(0, 24);
1894
1971
  }
@@ -2030,6 +2107,14 @@ async function ensureRuntimeWorkReceiptTable(
2030
2107
  `,
2031
2108
  )
2032
2109
  .then(async () => {
2110
+ await client.query(`
2111
+ CREATE TABLE IF NOT EXISTS ${receiptExecutionLockTable(session)} (
2112
+ k bytea PRIMARY KEY,
2113
+ owner_execution_id text NOT NULL,
2114
+ expires_at timestamptz NOT NULL,
2115
+ updated_at timestamptz NOT NULL DEFAULT now()
2116
+ )
2117
+ `);
2033
2118
  const missingColumns = await missingRuntimeWorkReceiptSelfHealColumns(
2034
2119
  session,
2035
2120
  client,
@@ -2064,6 +2149,77 @@ async function ensureRuntimeWorkReceiptTable(
2064
2149
  }
2065
2150
  }
2066
2151
 
2152
+ export async function acquireRuntimeReceiptExecutionLock(
2153
+ context: RuntimeApiContext,
2154
+ input: {
2155
+ playName: string;
2156
+ key: string;
2157
+ ownerExecutionId: string;
2158
+ ttlMs: number;
2159
+ },
2160
+ ): Promise<{ ownerExecutionId: string; expiresAt: string } | null> {
2161
+ const ttlMs = Math.floor(input.ttlMs);
2162
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > 10 * 60_000) {
2163
+ throw new Error('Runtime receipt execution lock TTL must be 1..600000ms.');
2164
+ }
2165
+ const ownerExecutionId = input.ownerExecutionId.trim();
2166
+ if (!ownerExecutionId) {
2167
+ throw new Error('Runtime receipt execution lock owner is required.');
2168
+ }
2169
+ const session = await getRuntimeWorkReceiptSession(context, input);
2170
+ return await withRuntimeWorkReceiptClient(
2171
+ context,
2172
+ session,
2173
+ async (client) => {
2174
+ const { rows } = await client.query<Record<string, unknown>>(
2175
+ `
2176
+ INSERT INTO ${receiptExecutionLockTable(session)} AS target (
2177
+ k, owner_execution_id, expires_at, updated_at
2178
+ ) VALUES (
2179
+ decode($1, 'hex'), $2, now() + ($3::double precision * interval '1 millisecond'), now()
2180
+ )
2181
+ ON CONFLICT (k) DO UPDATE
2182
+ SET owner_execution_id = EXCLUDED.owner_execution_id,
2183
+ expires_at = EXCLUDED.expires_at,
2184
+ updated_at = now()
2185
+ WHERE target.expires_at <= now()
2186
+ OR target.owner_execution_id = EXCLUDED.owner_execution_id
2187
+ RETURNING owner_execution_id, expires_at
2188
+ `,
2189
+ [workReceiptKeyHex(input.key), ownerExecutionId, ttlMs],
2190
+ );
2191
+ const row = rows[0];
2192
+ return row
2193
+ ? {
2194
+ ownerExecutionId: String(row.owner_execution_id),
2195
+ expiresAt: new Date(String(row.expires_at)).toISOString(),
2196
+ }
2197
+ : null;
2198
+ },
2199
+ );
2200
+ }
2201
+
2202
+ export async function releaseRuntimeReceiptExecutionLock(
2203
+ context: RuntimeApiContext,
2204
+ input: { playName: string; key: string; ownerExecutionId: string },
2205
+ ): Promise<boolean> {
2206
+ const session = await getRuntimeWorkReceiptSession(context, input);
2207
+ return await withRuntimeWorkReceiptClient(
2208
+ context,
2209
+ session,
2210
+ async (client) => {
2211
+ const { rows } = await client.query(
2212
+ `DELETE FROM ${receiptExecutionLockTable(session)}
2213
+ WHERE k = decode($1, 'hex')
2214
+ AND owner_execution_id = $2
2215
+ RETURNING 1`,
2216
+ [workReceiptKeyHex(input.key), input.ownerExecutionId],
2217
+ );
2218
+ return rows.length > 0;
2219
+ },
2220
+ );
2221
+ }
2222
+
2067
2223
  async function withRuntimeWorkReceiptClient<T>(
2068
2224
  context: RuntimeApiContext,
2069
2225
  session: RuntimePostgresSession,
@@ -2273,6 +2429,31 @@ async function mintRuntimeSheetAttemptExpiresAt(
2273
2429
  throw new Error('Runtime sheet attempt expiry mint did not return a value.');
2274
2430
  }
2275
2431
 
2432
+ async function mintRuntimeSheetWriteVersion(
2433
+ client: RuntimeQueryClient,
2434
+ session: RuntimePostgresSession,
2435
+ requested?: number | null,
2436
+ ): Promise<number> {
2437
+ if (requested != null) {
2438
+ if (!Number.isSafeInteger(requested) || requested <= 0) {
2439
+ throw new Error(
2440
+ 'Runtime sheet writeVersion must be a positive safe integer.',
2441
+ );
2442
+ }
2443
+ return requested;
2444
+ }
2445
+ const { rows } = await client.query<{ write_version: number | string }>(
2446
+ `SELECT ${nextRuntimeSheetVersionExpression(session)} AS write_version`,
2447
+ );
2448
+ const value = Number(rows[0]?.write_version);
2449
+ if (!Number.isSafeInteger(value) || value <= 0) {
2450
+ throw new Error(
2451
+ 'Runtime sheet write version allocation did not return a safe integer.',
2452
+ );
2453
+ }
2454
+ return value;
2455
+ }
2456
+
2276
2457
  function parseRuntimeCellMeta(value: unknown): Record<string, unknown> {
2277
2458
  if (value && typeof value === 'object' && !Array.isArray(value)) {
2278
2459
  return value as Record<string, unknown>;
@@ -2773,6 +2954,7 @@ async function prepareRuntimeSheetDatasetRows(
2773
2954
  attemptOwnerRunId: string;
2774
2955
  attemptExpiresAt: string;
2775
2956
  attemptSeq: number;
2957
+ writeVersion: number;
2776
2958
  physicalInsertColumnsSql: string;
2777
2959
  physicalInsertValuesSql: string;
2778
2960
  physicalRefreshSetSql: string;
@@ -2851,6 +3033,44 @@ async function prepareRuntimeSheetDatasetRows(
2851
3033
  ON index_values.ord = key_values.ord
2852
3034
  ORDER BY key_values._key, key_values.ord
2853
3035
  ),
3036
+ preexisting_rows AS (
3037
+ SELECT target._key
3038
+ FROM ${sheetTable(session)} AS target
3039
+ JOIN input_rows ON input_rows._key = target._key
3040
+ ),
3041
+ versioned_completed_rows AS (
3042
+ UPDATE ${sheetTable(session)} AS target
3043
+ SET _run_id = $4::text,
3044
+ _writer_run_id = $4::text,
3045
+ _write_version = $12::bigint,
3046
+ _input_index = input_rows._input_index,
3047
+ _updated_at = now(),
3048
+ _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
3049
+ FROM input_rows
3050
+ WHERE target._key = input_rows._key
3051
+ AND target._status = 'enriched'
3052
+ AND COALESCE(target._write_version, 0) < $12::bigint
3053
+ RETURNING target._key,
3054
+ ($11::boolean OR (${targetMissingOutputSql})) AS needs_recompute
3055
+ ),
3056
+ versioned_failed_rows AS (
3057
+ UPDATE ${sheetTable(session)} AS target
3058
+ SET _run_id = $4::text,
3059
+ _writer_run_id = $4::text,
3060
+ _write_version = $12::bigint,
3061
+ _input_index = input_rows._input_index,
3062
+ _attempt_id = $7::text,
3063
+ _attempt_owner_run_id = $8::text,
3064
+ _attempt_expires_at = NULL,
3065
+ _attempt_seq = $10::integer,
3066
+ _updated_at = now(),
3067
+ _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
3068
+ FROM input_rows
3069
+ WHERE target._key = input_rows._key
3070
+ AND target._status = 'failed'
3071
+ AND COALESCE(target._write_version, 0) < $12::bigint
3072
+ RETURNING target._key
3073
+ ),
2854
3074
  existing_rows AS (
2855
3075
  UPDATE ${sheetTable(session)} AS target
2856
3076
  SET _input_index = input_rows._input_index,
@@ -2858,6 +3078,7 @@ async function prepareRuntimeSheetDatasetRows(
2858
3078
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
2859
3079
  FROM input_rows
2860
3080
  WHERE target._key = input_rows._key
3081
+ AND target._write_version = $12::bigint
2861
3082
  AND target._status <> 'stale'
2862
3083
  AND (
2863
3084
  NOT (${targetNewerTerminalRowSql})
@@ -2881,8 +3102,8 @@ async function prepareRuntimeSheetDatasetRows(
2881
3102
  RETURNING target._key
2882
3103
  ),
2883
3104
  inserted_rows AS (
2884
- INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq${input.physicalInsertColumnsSql})
2885
- SELECT _key, 'pending', $4::text, _input_index, $7::text, $8::text, $9::timestamptz, $10::integer${input.physicalInsertValuesSql}
3105
+ INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id${input.physicalInsertColumnsSql})
3106
+ SELECT _key, 'pending', $4::text, _input_index, $7::text, $8::text, $9::timestamptz, $10::integer, $12::bigint, $4::text${input.physicalInsertValuesSql}
2886
3107
  FROM input_rows
2887
3108
  ON CONFLICT (_key) DO UPDATE SET
2888
3109
  _status = 'pending',
@@ -2892,6 +3113,8 @@ async function prepareRuntimeSheetDatasetRows(
2892
3113
  _attempt_owner_run_id = EXCLUDED._attempt_owner_run_id,
2893
3114
  _attempt_expires_at = EXCLUDED._attempt_expires_at,
2894
3115
  _attempt_seq = EXCLUDED._attempt_seq,
3116
+ _write_version = EXCLUDED._write_version,
3117
+ _writer_run_id = EXCLUDED._writer_run_id,
2895
3118
  _updated_at = now(),
2896
3119
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalUpsertSetSql}
2897
3120
  WHERE ${sheetTable(session)}._status = 'stale'
@@ -2911,10 +3134,30 @@ async function prepareRuntimeSheetDatasetRows(
2911
3134
  FROM input_rows
2912
3135
  WHERE target._key = input_rows._key
2913
3136
  AND target._status = 'enriched'
3137
+ AND target._write_version = $12::bigint
2914
3138
  AND (${targetMissingOutputSql})
2915
3139
  AND COALESCE(target._attempt_seq, 0) <= $10::integer
2916
3140
  RETURNING target._key
2917
3141
  ),
3142
+ superseded_rows AS (
3143
+ UPDATE ${sheetTable(session)} AS target
3144
+ SET _run_id = $4::text,
3145
+ _input_index = input_rows._input_index,
3146
+ _attempt_id = $7::text,
3147
+ _attempt_owner_run_id = $8::text,
3148
+ _attempt_expires_at = NULL,
3149
+ _attempt_seq = $10::integer,
3150
+ _write_version = $12::bigint,
3151
+ _writer_run_id = $4::text,
3152
+ _error = NULL,
3153
+ _updated_at = now(),
3154
+ _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
3155
+ FROM input_rows
3156
+ WHERE target._key = input_rows._key
3157
+ AND target._status IN ('pending', 'running')
3158
+ AND COALESCE(target._write_version, 0) < $12::bigint
3159
+ RETURNING target._key
3160
+ ),
2918
3161
  claimed_existing_rows AS (
2919
3162
  UPDATE ${sheetTable(session)} AS target
2920
3163
  SET _run_id = $4::text,
@@ -2928,6 +3171,7 @@ async function prepareRuntimeSheetDatasetRows(
2928
3171
  FROM input_rows
2929
3172
  WHERE target._key = input_rows._key
2930
3173
  AND target._status IN ('pending', 'running', 'failed')
3174
+ AND target._write_version = $12::bigint
2931
3175
  AND NOT (${targetNewerTerminalRowSql})
2932
3176
  AND (
2933
3177
  ${targetAttemptFenceSql}
@@ -2950,6 +3194,9 @@ async function prepareRuntimeSheetDatasetRows(
2950
3194
  FROM inserted_rows
2951
3195
  UNION
2952
3196
  SELECT _key
3197
+ FROM superseded_rows
3198
+ UNION
3199
+ SELECT _key
2953
3200
  FROM missing_output_rows
2954
3201
  UNION
2955
3202
  SELECT _key
@@ -2959,10 +3206,18 @@ async function prepareRuntimeSheetDatasetRows(
2959
3206
  FROM existing_rows
2960
3207
  WHERE $11::boolean
2961
3208
  UNION
3209
+ SELECT _key
3210
+ FROM versioned_completed_rows
3211
+ WHERE needs_recompute
3212
+ UNION
3213
+ SELECT _key FROM versioned_failed_rows
3214
+ UNION
2962
3215
  SELECT existing._key
2963
3216
  FROM ${sheetTable(session)} AS existing
2964
3217
  JOIN input_rows ON input_rows._key = existing._key
2965
3218
  WHERE (
3219
+ existing._write_version = $12::bigint
3220
+ AND (
2966
3221
  existing._status IN ('pending', 'running', 'failed')
2967
3222
  AND NOT (${existingNewerTerminalRowSql})
2968
3223
  AND (
@@ -2975,8 +3230,10 @@ async function prepareRuntimeSheetDatasetRows(
2975
3230
  AND COALESCE(existing._attempt_seq, 0) < $10::integer
2976
3231
  )
2977
3232
  )
2978
- )
3233
+ ))
2979
3234
  OR (
3235
+ existing._write_version = $12::bigint
3236
+ AND
2980
3237
  existing._status = 'enriched'
2981
3238
  AND (${existingMissingOutputSql})
2982
3239
  AND (
@@ -2995,6 +3252,9 @@ async function prepareRuntimeSheetDatasetRows(
2995
3252
  AND NOT EXISTS (
2996
3253
  SELECT 1 FROM pending_rows WHERE pending_rows._key = existing._key
2997
3254
  )
3255
+ UNION
3256
+ SELECT _key FROM versioned_completed_rows
3257
+ WHERE NOT needs_recompute
2998
3258
  ),
2999
3259
  blocked_rows AS (
3000
3260
  SELECT input_rows._key
@@ -3017,7 +3277,11 @@ async function prepareRuntimeSheetDatasetRows(
3017
3277
  FROM blocked_rows
3018
3278
  ),
3019
3279
  inserted_count_cte AS (
3020
- SELECT count(*)::bigint AS c FROM inserted_rows
3280
+ SELECT count(*)::bigint AS c
3281
+ FROM inserted_rows
3282
+ WHERE NOT EXISTS (
3283
+ SELECT 1 FROM preexisting_rows WHERE preexisting_rows._key = inserted_rows._key
3284
+ )
3021
3285
  ),
3022
3286
  missing_output_count_cte AS (
3023
3287
  SELECT count(*)::bigint AS c FROM missing_output_rows
@@ -3077,6 +3341,7 @@ async function prepareRuntimeSheetDatasetRows(
3077
3341
  input.attemptExpiresAt,
3078
3342
  input.attemptSeq,
3079
3343
  input.force === true,
3344
+ input.writeVersion,
3080
3345
  ],
3081
3346
  );
3082
3347
  inserted += Number(rows[0]?.inserted_count ?? 0);
@@ -3113,6 +3378,7 @@ async function buildRuntimeSheetDatasetStartResult(
3113
3378
  attemptOwnerRunId: string;
3114
3379
  attemptExpiresAt: string;
3115
3380
  attemptSeq: number;
3381
+ writeVersion: number;
3116
3382
  inserted: number;
3117
3383
  rowDispositions: RuntimeSheetPreparedRowDisposition[];
3118
3384
  force?: boolean;
@@ -3148,6 +3414,7 @@ async function buildRuntimeSheetDatasetStartResult(
3148
3414
  completedRows: [],
3149
3415
  blockedRows: [],
3150
3416
  tableNamespace: input.tableNamespace,
3417
+ writeVersion: input.writeVersion,
3151
3418
  };
3152
3419
  }
3153
3420
 
@@ -3167,6 +3434,7 @@ async function buildRuntimeSheetDatasetStartResult(
3167
3434
  attemptOwnerRunId: input.attemptOwnerRunId,
3168
3435
  attemptExpiresAt: input.attemptExpiresAt,
3169
3436
  attemptSeq: input.attemptSeq,
3437
+ writeVersion: input.writeVersion,
3170
3438
  normalizedPlayName: input.normalizedPlayName,
3171
3439
  normalizedTableNamespace: input.normalizedTableNamespace,
3172
3440
  outputFields: input.force === true ? datasetFields : [],
@@ -3197,6 +3465,7 @@ async function buildRuntimeSheetDatasetStartResult(
3197
3465
  completedRows: [],
3198
3466
  blockedRows,
3199
3467
  tableNamespace: input.tableNamespace,
3468
+ writeVersion: input.writeVersion,
3200
3469
  };
3201
3470
  }
3202
3471
  const persistedRows = await readRuntimeRowsByKey(
@@ -3246,6 +3515,7 @@ async function buildRuntimeSheetDatasetStartResult(
3246
3515
  completedRows,
3247
3516
  blockedRows,
3248
3517
  tableNamespace: input.tableNamespace,
3518
+ writeVersion: input.writeVersion ?? undefined,
3249
3519
  };
3250
3520
  }
3251
3521
 
@@ -3259,6 +3529,7 @@ async function markRuntimeRowsPendingForRecompute(
3259
3529
  attemptOwnerRunId: string;
3260
3530
  attemptExpiresAt: string;
3261
3531
  attemptSeq: number;
3532
+ writeVersion: number | null;
3262
3533
  normalizedPlayName: string;
3263
3534
  normalizedTableNamespace: string;
3264
3535
  outputFields?: string[];
@@ -3266,28 +3537,13 @@ async function markRuntimeRowsPendingForRecompute(
3266
3537
  },
3267
3538
  ): Promise<void> {
3268
3539
  if (input.keys.length === 0) return;
3269
- const targetAttemptFenceSql = activeRuntimeSheetAttemptFenceSql(
3270
- 'target',
3271
- '$5::text',
3272
- '$6::text',
3273
- '$7::timestamptz',
3274
- '$8::integer',
3275
- );
3276
- const targetOlderForeignAttemptOwnerSql =
3277
- 'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $6::text AND COALESCE(target._attempt_seq, 0) < $8::integer';
3278
- const targetWritableEnrichedAttemptSql =
3279
- writableEnrichedRuntimeSheetAttemptSql(
3280
- 'target',
3281
- '$6::text',
3282
- '$7::timestamptz',
3283
- '$8',
3284
- );
3285
3540
  const outputFields = [...new Set(input.outputFields ?? [])];
3286
3541
  await client.query(
3287
3542
  `WITH target_rows AS (
3288
3543
  SELECT _key, _status, _cell_meta
3289
3544
  FROM ${sheetTable(session)}
3290
3545
  WHERE _key = ANY($1::text[])
3546
+ AND ($10::boolean OR NOT $10::boolean)
3291
3547
  FOR UPDATE
3292
3548
  ),
3293
3549
  updated AS (
@@ -3298,33 +3554,13 @@ async function markRuntimeRowsPendingForRecompute(
3298
3554
  _attempt_owner_run_id = $6::text,
3299
3555
  _attempt_expires_at = $7::timestamptz,
3300
3556
  _attempt_seq = $8::integer,
3557
+ _write_version = $11::bigint,
3558
+ _writer_run_id = $2::text,
3301
3559
  _updated_at = now(),
3302
3560
  _version = ${nextRuntimeSheetVersionExpression(session)}
3303
3561
  FROM target_rows
3304
3562
  WHERE target._key = target_rows._key
3305
- AND (
3306
- (
3307
- target._status IN ('pending', 'running', 'failed')
3308
- AND (
3309
- ${targetAttemptFenceSql}
3310
- OR ${targetOlderForeignAttemptOwnerSql}
3311
- OR (
3312
- $10::boolean
3313
- AND target._status = 'failed'
3314
- )
3315
- )
3316
- )
3317
- OR ${targetAttemptFenceSql}
3318
- OR (
3319
- $10::boolean
3320
- AND target._status = 'enriched'
3321
- AND COALESCE(target._attempt_seq, 0) <= $8::integer
3322
- )
3323
- OR (
3324
- target._status = 'enriched'
3325
- AND ${targetWritableEnrichedAttemptSql}
3326
- )
3327
- )
3563
+ AND target._write_version = $11::bigint
3328
3564
  AND (
3329
3565
  target._status <> 'pending'
3330
3566
  OR target._run_id IS DISTINCT FROM $2::text
@@ -3414,6 +3650,7 @@ async function markRuntimeRowsPendingForRecompute(
3414
3650
  input.attemptSeq,
3415
3651
  outputFields,
3416
3652
  input.force === true,
3653
+ input.writeVersion,
3417
3654
  ],
3418
3655
  );
3419
3656
  }
@@ -3603,6 +3840,7 @@ export async function claimRuntimeWorkReceipt(
3603
3840
  playName: string;
3604
3841
  runId: string;
3605
3842
  key: string;
3843
+ leaseId?: string | null;
3606
3844
  runAttempt?: number | null;
3607
3845
  leaseAware?: boolean;
3608
3846
  reclaimRunning?: boolean;
@@ -3620,7 +3858,8 @@ export async function claimRuntimeWorkReceipt(
3620
3858
  session,
3621
3859
  async (client) => {
3622
3860
  const leaseId =
3623
- input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null;
3861
+ input.leaseId?.trim() ||
3862
+ (input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null);
3624
3863
  const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
3625
3864
  const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
3626
3865
  input.leaseTtlMs,
@@ -3748,6 +3987,15 @@ export async function claimRuntimeWorkReceipt(
3748
3987
  }
3749
3988
 
3750
3989
  const latest = await readRuntimeWorkReceipt(client, session, input.key);
3990
+ if (
3991
+ latest &&
3992
+ leaseId !== null &&
3993
+ latest.leaseId === leaseId &&
3994
+ (latest.leaseOwnerRunId ?? latest.runId) === input.runId &&
3995
+ (latest.leaseOwnerAttempt ?? 0) === runAttempt
3996
+ ) {
3997
+ return { disposition: 'claimed', receipt: latest };
3998
+ }
3751
3999
  if (
3752
4000
  latest &&
3753
4001
  input.forceRefresh !== true &&
@@ -3809,6 +4057,7 @@ export async function claimRuntimeWorkReceipts(
3809
4057
  playName: string;
3810
4058
  runId: string;
3811
4059
  keys: string[];
4060
+ leaseIds?: string[];
3812
4061
  runAttempt?: number | null;
3813
4062
  leaseAware?: boolean;
3814
4063
  reclaimRunning?: boolean;
@@ -3830,9 +4079,21 @@ export async function claimRuntimeWorkReceipts(
3830
4079
  session,
3831
4080
  async (client) => {
3832
4081
  const keyHexes = keys.map(workReceiptKeyHex);
3833
- const leaseIds = keys.map(() =>
3834
- input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null,
3835
- );
4082
+ if (
4083
+ input.leaseIds !== undefined &&
4084
+ input.leaseIds.length !== keys.length
4085
+ ) {
4086
+ throw new Error(
4087
+ `Runtime receipt bulk claim requires one lease ID per key. Received ${input.leaseIds.length} lease IDs for ${keys.length} keys.`,
4088
+ );
4089
+ }
4090
+ const leaseIds = keys.map((_, index) => {
4091
+ const providedLeaseId = input.leaseIds?.[index]?.trim();
4092
+ return (
4093
+ providedLeaseId ||
4094
+ (input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null)
4095
+ );
4096
+ });
3836
4097
  const claimStatus =
3837
4098
  input.leaseAware === true
3838
4099
  ? RECEIPT_STATUS_QUEUED
@@ -3935,7 +4196,10 @@ export async function claimRuntimeWorkReceipts(
3935
4196
  receipts.lease_expires_at,
3936
4197
  receipts.updated_at,
3937
4198
  input_keys.ord,
3938
- false AS claimed
4199
+ receipts.lease_id = input_keys.lease_id
4200
+ AND receipts.lease_owner_run_id = $4::text
4201
+ AND COALESCE(receipts.lease_owner_attempt, 0) = $9::integer
4202
+ AS claimed
3939
4203
  FROM input_keys
3940
4204
  JOIN ${workReceiptTable(session)} AS receipts
3941
4205
  ON receipts.k = decode(input_keys.key_hex, 'hex')
@@ -4411,7 +4675,48 @@ export async function completeRuntimeWorkReceipt(
4411
4675
  context,
4412
4676
  session,
4413
4677
  async (client) => {
4414
- const leaseId = input.leaseId?.trim() || null;
4678
+ const immutableCacheInsert =
4679
+ input.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID;
4680
+ const leaseId = immutableCacheInsert
4681
+ ? null
4682
+ : input.leaseId?.trim() || null;
4683
+ if (immutableCacheInsert) {
4684
+ const { rows: insertedRows } = await client.query<
4685
+ Record<string, unknown>
4686
+ >(
4687
+ `
4688
+ INSERT INTO ${workReceiptTable(session)} (
4689
+ k, status, output, error, failure_kind, run_id, updated_at
4690
+ ) VALUES (
4691
+ decode($1, 'hex'), $2::smallint, $3::jsonb, NULL, 0, $4, now()
4692
+ )
4693
+ ON CONFLICT (k) DO NOTHING
4694
+ RETURNING convert_from(k, 'UTF8') AS k, status, output, error,
4695
+ failure_kind, run_id, lease_id, lease_owner_run_id,
4696
+ lease_owner_attempt, lease_expires_at, updated_at
4697
+ `,
4698
+ [
4699
+ workReceiptKeyHex(input.key),
4700
+ RECEIPT_STATUS_COMPLETED,
4701
+ input.output === null ? null : stringifyPostgresJson(input.output),
4702
+ input.runId,
4703
+ ],
4704
+ );
4705
+ if (insertedRows[0]) return mapRuntimeWorkReceiptRow(insertedRows[0]);
4706
+ const { rows: winnerRows } = await client.query<
4707
+ Record<string, unknown>
4708
+ >(
4709
+ `SELECT convert_from(k, 'UTF8') AS k, status, output, error,
4710
+ failure_kind, run_id, lease_id, lease_owner_run_id,
4711
+ lease_owner_attempt, lease_expires_at, updated_at
4712
+ FROM ${workReceiptTable(session)}
4713
+ WHERE k = decode($1, 'hex')
4714
+ AND status IN ($2::smallint, ${RECEIPT_STATUS_SKIPPED}::smallint)
4715
+ LIMIT 1`,
4716
+ [workReceiptKeyHex(input.key), RECEIPT_STATUS_COMPLETED],
4717
+ );
4718
+ return winnerRows[0] ? mapRuntimeWorkReceiptRow(winnerRows[0]) : null;
4719
+ }
4415
4720
  const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
4416
4721
  const { rows } = await client.query<Record<string, unknown>>(
4417
4722
  `
@@ -4546,6 +4851,44 @@ export async function completeRuntimeWorkReceipts(
4546
4851
  }
4547
4852
  throw error;
4548
4853
  }
4854
+ const immutableEntries = receipts
4855
+ .map((receipt, index) => ({ receipt, index }))
4856
+ .filter(
4857
+ ({ receipt }) =>
4858
+ receipt.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
4859
+ );
4860
+ if (immutableEntries.length > 0) {
4861
+ const leasedEntries = receipts
4862
+ .map((receipt, index) => ({ receipt, index }))
4863
+ .filter(
4864
+ ({ receipt }) =>
4865
+ receipt.leaseId !== COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
4866
+ );
4867
+ const resolved = Array<WorkReceipt | null>(receipts.length).fill(null);
4868
+ const [immutableResults, leasedResults] = await Promise.all([
4869
+ Promise.all(
4870
+ immutableEntries.map(({ receipt }) =>
4871
+ completeRuntimeWorkReceipt(context, {
4872
+ playName: input.playName,
4873
+ ...receipt,
4874
+ }),
4875
+ ),
4876
+ ),
4877
+ leasedEntries.length > 0
4878
+ ? completeRuntimeWorkReceipts(context, {
4879
+ playName: input.playName,
4880
+ receipts: leasedEntries.map(({ receipt }) => receipt),
4881
+ })
4882
+ : Promise.resolve([]),
4883
+ ]);
4884
+ immutableEntries.forEach(({ index }, resultIndex) => {
4885
+ resolved[index] = immutableResults[resultIndex] ?? null;
4886
+ });
4887
+ leasedEntries.forEach(({ index }, resultIndex) => {
4888
+ resolved[index] = leasedResults[resultIndex] ?? null;
4889
+ });
4890
+ return resolved;
4891
+ }
4549
4892
  const session = await getRuntimeWorkReceiptSessionForKeys(context, {
4550
4893
  playName: input.playName,
4551
4894
  keys: receipts.map((receipt) => receipt.key),
@@ -5195,6 +5538,7 @@ export async function startRuntimeSheetDataset(
5195
5538
  attemptExpiresAt?: string | null;
5196
5539
  attemptSeq?: number | null;
5197
5540
  attemptLeaseTtlMs?: number | null;
5541
+ writeVersion?: number | null;
5198
5542
  inputOffset?: number;
5199
5543
  force?: boolean;
5200
5544
  },
@@ -5341,7 +5685,10 @@ export async function startRuntimeSheetDataset(
5341
5685
  });
5342
5686
  const attemptSeq = normalizeRuntimeRunAttempt(input.attemptSeq);
5343
5687
  const chunks = chunkValues(rowEntries, DIRECT_POSTGRES_BATCH_SIZE);
5344
- const needsTransaction = chunks.length > 1;
5688
+ // Scheduling is one admission decision. Even when batching SQL for the
5689
+ // parameter limit, either every requested row receives this intent version
5690
+ // or none do; a run can never leave 42 pending rows after 8 conflicts.
5691
+ const needsTransaction = true;
5345
5692
 
5346
5693
  const result = await withRuntimeSheetQueryClient(
5347
5694
  context,
@@ -5359,6 +5706,11 @@ export async function startRuntimeSheetDataset(
5359
5706
  client,
5360
5707
  input.attemptLeaseTtlMs,
5361
5708
  );
5709
+ const writeVersion = await mintRuntimeSheetWriteVersion(
5710
+ client,
5711
+ session,
5712
+ input.writeVersion,
5713
+ );
5362
5714
  timings.push({
5363
5715
  phase: 'attempt_expiry_mint',
5364
5716
  ms: Date.now() - attemptExpiryStartedAt,
@@ -5374,6 +5726,7 @@ export async function startRuntimeSheetDataset(
5374
5726
  attemptOwnerRunId,
5375
5727
  attemptExpiresAt,
5376
5728
  attemptSeq,
5729
+ writeVersion,
5377
5730
  physicalInsertColumnsSql,
5378
5731
  physicalInsertValuesSql,
5379
5732
  physicalRefreshSetSql:
@@ -5406,6 +5759,7 @@ export async function startRuntimeSheetDataset(
5406
5759
  attemptOwnerRunId,
5407
5760
  attemptExpiresAt,
5408
5761
  attemptSeq,
5762
+ writeVersion,
5409
5763
  timings,
5410
5764
  force: input.force === true,
5411
5765
  ...prepared,
@@ -5423,6 +5777,7 @@ export async function startRuntimeSheetDataset(
5423
5777
  ...built,
5424
5778
  attemptExpiresAt,
5425
5779
  attemptSeq,
5780
+ writeVersion,
5426
5781
  };
5427
5782
  },
5428
5783
  );
@@ -5442,6 +5797,7 @@ export async function startRuntimeSheetDataset(
5442
5797
  attemptOwnerRunId,
5443
5798
  attemptExpiresAt: result.attemptExpiresAt,
5444
5799
  attemptSeq,
5800
+ writeVersion: result.writeVersion,
5445
5801
  timings,
5446
5802
  };
5447
5803
  }
@@ -5751,6 +6107,7 @@ type CompleteRuntimeMapRowChunksInput = {
5751
6107
  attemptOwnerRunId: string | null;
5752
6108
  attemptExpiresAt: string | null;
5753
6109
  attemptSeq: number;
6110
+ writeVersion: number | null;
5754
6111
  normalizedPlayName: string;
5755
6112
  normalizedTableNamespace: string;
5756
6113
  outputFields: string[];
@@ -5759,6 +6116,8 @@ type CompleteRuntimeMapRowChunksInput = {
5759
6116
  export type RuntimeMapRowsWriteResult = {
5760
6117
  updated: number;
5761
6118
  fencedKeys: string[];
6119
+ staleDropped?: number;
6120
+ staleDroppedKeys?: string[];
5762
6121
  };
5763
6122
 
5764
6123
  function writableEnrichedRuntimeSheetAttemptSql(
@@ -5847,6 +6206,8 @@ async function completeRuntimeMapRowChunks(
5847
6206
  UPDATE ${sheetTable(session)} AS target
5848
6207
  SET _status = 'enriched',
5849
6208
  _run_id = $5::text,
6209
+ _writer_run_id = $5::text,
6210
+ _write_version = COALESCE($13::bigint, target._write_version),
5850
6211
  _error = NULL,
5851
6212
  _attempt_id = $9::text,
5852
6213
  _attempt_owner_run_id = $10::text,
@@ -5857,6 +6218,8 @@ async function completeRuntimeMapRowChunks(
5857
6218
  _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
5858
6219
  FROM matched_updates AS updates
5859
6220
  WHERE target._key = updates.matched_key
6221
+ AND ($13::bigint IS NULL OR target._write_version = $13::bigint)
6222
+ AND ($13::bigint IS NOT NULL OR target._status <> 'enriched')
5860
6223
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
5861
6224
  AND NOT (${sameOwnerTerminalEpochSql})
5862
6225
  AND (
@@ -5893,6 +6256,9 @@ async function completeRuntimeMapRowChunks(
5893
6256
  SELECT 1 FROM applied_rows WHERE applied_rows._key = updates.matched_key
5894
6257
  )
5895
6258
  AND (
6259
+ ($13::bigint IS NOT NULL AND target._write_version <> $13::bigint)
6260
+ OR ($13::bigint IS NULL AND target._status = 'enriched')
6261
+ OR
5896
6262
  (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
5897
6263
  OR NOT (
5898
6264
  ($9::text IS NULL AND target._run_id = $5::text)
@@ -6002,12 +6368,18 @@ async function completeRuntimeMapRowChunks(
6002
6368
  input.attemptOwnerRunId,
6003
6369
  input.attemptExpiresAt,
6004
6370
  input.attemptSeq,
6371
+ input.writeVersion,
6005
6372
  ],
6006
6373
  );
6007
- updated += Number(rows[0]?.updated ?? 0);
6008
- fencedKeys.push(...(rows[0]?.fenced_keys ?? []));
6009
-
6010
6374
  const matchedKeys = new Set(rows[0]?.matched_keys ?? []);
6375
+ const chunkFencedKeys = rows[0]?.fenced_keys ?? [];
6376
+ // An exact-version replay that already produced the same terminal value is
6377
+ // accepted idempotently even when SQL has no physical mutation to apply.
6378
+ // Stale versions remain excluded through fenced_keys.
6379
+ const appliedCount = Number(rows[0]?.updated ?? 0);
6380
+ updated += appliedCount;
6381
+ fencedKeys.push(...chunkFencedKeys);
6382
+
6011
6383
  if (matchedKeys.size === chunk.length) {
6012
6384
  continue;
6013
6385
  }
@@ -6444,6 +6816,7 @@ async function failRuntimeMapRowChunks(
6444
6816
  attemptOwnerRunId: string | null;
6445
6817
  attemptExpiresAt: string | null;
6446
6818
  attemptSeq: number;
6819
+ writeVersion: number | null;
6447
6820
  normalizedPlayName: string;
6448
6821
  normalizedTableNamespace: string;
6449
6822
  },
@@ -6518,6 +6891,8 @@ async function failRuntimeMapRowChunks(
6518
6891
  UPDATE ${sheetTable(session)} AS target
6519
6892
  SET _status = 'failed',
6520
6893
  _run_id = $6::text,
6894
+ _writer_run_id = $6::text,
6895
+ _write_version = COALESCE($16::bigint, target._write_version),
6521
6896
  _error = updates.error,
6522
6897
  _attempt_id = $11::text,
6523
6898
  _attempt_owner_run_id = $12::text,
@@ -6528,6 +6903,7 @@ async function failRuntimeMapRowChunks(
6528
6903
  _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
6529
6904
  FROM matched_updates AS updates, ${sheetTable(session)} AS prev
6530
6905
  WHERE target._key = updates.matched_key
6906
+ AND ($16::bigint IS NULL OR target._write_version = $16::bigint)
6531
6907
  AND prev._key = target._key
6532
6908
  AND ($15::boolean OR target._status <> 'enriched')
6533
6909
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$13::timestamptz', '$14::integer', '$11::text', '$12::text')})
@@ -6553,6 +6929,8 @@ async function failRuntimeMapRowChunks(
6553
6929
  SELECT 1 FROM applied_rows WHERE applied_rows._key = updates.matched_key
6554
6930
  )
6555
6931
  AND (
6932
+ ($16::bigint IS NOT NULL AND target._write_version <> $16::bigint)
6933
+ OR
6556
6934
  (NOT $15::boolean AND target._status = 'enriched')
6557
6935
  OR (${newerTerminalRuntimeSheetRowSql('target', '$13::timestamptz', '$14::integer', '$11::text', '$12::text')})
6558
6936
  OR NOT (
@@ -6639,6 +7017,7 @@ async function failRuntimeMapRowChunks(
6639
7017
  input.attemptExpiresAt,
6640
7018
  input.attemptSeq,
6641
7019
  input.forceTerminal === true,
7020
+ input.writeVersion,
6642
7021
  ],
6643
7022
  );
6644
7023
  updated += Number(rows[0]?.updated ?? 0);
@@ -6649,9 +7028,8 @@ async function failRuntimeMapRowChunks(
6649
7028
 
6650
7029
  /**
6651
7030
  * Mark map rows terminal in the per-run scoped Postgres sheet table by key.
6652
- * Mirrors server-side `store.completeSheetRows` semantics: UPDATE-by-key with
6653
- * the row's enriched output values written into materialized physical
6654
- * columns, _status flipped to 'enriched', and the sheet cursor advanced.
7031
+ * Writes enriched output values into materialized physical columns, flips
7032
+ * _status to 'enriched', and advances the sheet cursor.
6655
7033
  *
6656
7034
  * Rows with `status: 'failed'` instead persist a row-isolated failure
6657
7035
  * (`_status='failed'`, `_error`, per-cell failure meta) while keeping the
@@ -6674,6 +7052,7 @@ export async function completeRuntimeMapRows(
6674
7052
  attemptOwnerRunId?: string | null;
6675
7053
  attemptExpiresAt?: string | null;
6676
7054
  attemptSeq?: number | null;
7055
+ writeVersion?: number | null;
6677
7056
  forceFailedRows?: boolean;
6678
7057
  },
6679
7058
  ): Promise<RuntimeMapRowsWriteResult> {
@@ -6757,6 +7136,15 @@ export async function completeRuntimeMapRows(
6757
7136
  input.attemptOwnerRunId?.trim() || (attemptId ? input.runId : null);
6758
7137
  const attemptExpiresAt = input.attemptExpiresAt?.trim() || null;
6759
7138
  const attemptSeq = normalizeRuntimeRunAttempt(input.attemptSeq);
7139
+ const writeVersion = input.writeVersion == null ? null : input.writeVersion;
7140
+ if (
7141
+ writeVersion !== null &&
7142
+ (!Number.isSafeInteger(writeVersion) || writeVersion <= 0)
7143
+ ) {
7144
+ throw new Error(
7145
+ 'Runtime sheet writeVersion must be a positive safe integer.',
7146
+ );
7147
+ }
6760
7148
 
6761
7149
  return await withRuntimeSheetQueryClient(
6762
7150
  context,
@@ -6780,6 +7168,7 @@ export async function completeRuntimeMapRows(
6780
7168
  attemptOwnerRunId,
6781
7169
  attemptExpiresAt,
6782
7170
  attemptSeq,
7171
+ writeVersion,
6783
7172
  normalizedPlayName: normalizePlayNameForSheet(session.playName),
6784
7173
  normalizedTableNamespace: normalizeTableNamespace(
6785
7174
  input.tableNamespace,
@@ -6813,6 +7202,7 @@ export async function completeRuntimeMapRows(
6813
7202
  attemptOwnerRunId,
6814
7203
  attemptExpiresAt,
6815
7204
  attemptSeq,
7205
+ writeVersion,
6816
7206
  normalizedPlayName: normalizePlayNameForSheet(session.playName),
6817
7207
  normalizedTableNamespace: normalizeTableNamespace(
6818
7208
  input.tableNamespace,
@@ -6822,6 +7212,8 @@ export async function completeRuntimeMapRows(
6822
7212
  return {
6823
7213
  updated: completed.updated + failed.updated,
6824
7214
  fencedKeys: [...completed.fencedKeys, ...failed.fencedKeys],
7215
+ staleDropped: completed.fencedKeys.length + failed.fencedKeys.length,
7216
+ staleDroppedKeys: [...completed.fencedKeys, ...failed.fencedKeys],
6825
7217
  };
6826
7218
  },
6827
7219
  );