deepline 0.3.228 → 0.3.230

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 (48) 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-data-plane/contracts.ts +12 -0
  4. package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
  5. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +252 -0
  6. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +29 -8
  7. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +19 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +297 -69
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +5 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +38 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +8 -3
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +39 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +293 -4
  15. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +29 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +56 -11
  17. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +1 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +4 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +4 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +380 -183
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +187 -14
  22. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +52 -2
  23. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +5 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +30 -2
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +118 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +135 -21
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
  28. package/dist/bundling-sources/shared_libs/plays/dataset.ts +16 -2
  29. package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +72 -0
  30. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +1 -3
  31. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +9 -13
  32. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +28 -31
  33. package/dist/cli/index.js +102 -2
  34. package/dist/cli/index.mjs +102 -2
  35. package/dist/{compiler-manifest-Dpd2hEet.d.mts → compiler-manifest-n8ZtPv2h.d.mts} +31 -21
  36. package/dist/{compiler-manifest-Dpd2hEet.d.ts → compiler-manifest-n8ZtPv2h.d.ts} +31 -21
  37. package/dist/index.d.mts +2 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +98 -2
  40. package/dist/index.mjs +98 -2
  41. package/dist/install-integrity.json +4 -2
  42. package/dist/plays/bundle-play-file.d.mts +2 -2
  43. package/dist/plays/bundle-play-file.d.ts +2 -2
  44. package/dist/release.d.mts +1 -1
  45. package/dist/release.d.ts +1 -1
  46. package/dist/release.js +1 -1
  47. package/dist/release.mjs +1 -1
  48. package/package.json +1 -1
@@ -79,12 +79,17 @@ import {
79
79
  import type { PlayStaticPipeline } from '../../plays/static-pipeline';
80
80
  import {
81
81
  LOG_TAIL_LIMIT,
82
+ normalizePlayRunLedgerTerminalWorkCounterSegment,
82
83
  type AcceptedPlayRunLedgerEvent,
83
84
  type PlayRunLedgerEvent,
84
85
  type PlayRunLedgerIngressEvent,
86
+ type PlayRunLedgerTerminalWorkCounterSegment,
85
87
  } from '../run-ledger';
86
88
  import { stableStringify } from '../../plays/row-identity';
87
- import { runtimeWorkCountersFromResources } from '../runtime-work-counters';
89
+ import {
90
+ runtimeWorkCountersFromResources,
91
+ runtimeWorkCountersFromStats,
92
+ } from '../runtime-work-counters';
88
93
  import {
89
94
  isCtxFetchHttpFailureDiagnostic,
90
95
  parseCtxFetchHttpFailureDiagnostic,
@@ -9456,6 +9461,28 @@ export type PostgresSchedulerDirectRunnerTerminalOutcome =
9456
9461
  | 'wake_required'
9457
9462
  | 'not_found';
9458
9463
 
9464
+ function terminalWorkCounterSegmentFromRunnerResult(
9465
+ result: unknown,
9466
+ attempt: number,
9467
+ ): PlayRunLedgerTerminalWorkCounterSegment | undefined {
9468
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
9469
+ return undefined;
9470
+ }
9471
+ const stats = (result as { stats?: unknown }).stats;
9472
+ if (!stats || typeof stats !== 'object' || Array.isArray(stats)) {
9473
+ return undefined;
9474
+ }
9475
+ const segment = (stats as { runtimeWorkCounterSegment?: unknown })
9476
+ .runtimeWorkCounterSegment;
9477
+ if (!segment || typeof segment !== 'object' || Array.isArray(segment)) {
9478
+ return undefined;
9479
+ }
9480
+ return normalizePlayRunLedgerTerminalWorkCounterSegment({
9481
+ ...segment,
9482
+ attempt,
9483
+ });
9484
+ }
9485
+
9459
9486
  /**
9460
9487
  * Reduce an ordinary detached-runner completion at terminal ingress.
9461
9488
  *
@@ -9522,6 +9549,10 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9522
9549
  input.terminalLogOutbox,
9523
9550
  );
9524
9551
  const runtimeTiming = extractRunnerRuntimeTiming(input.result);
9552
+ const terminalWorkCounterSegment = terminalWorkCounterSegmentFromRunnerResult(
9553
+ input.result,
9554
+ input.attempt,
9555
+ );
9525
9556
  // `workCompletedAtMs` is useful lifecycle telemetry, but it comes from the
9526
9557
  // sandbox process where play code can alter Date.now. Billing must stop on
9527
9558
  // the first database receipt, whose clock and first-write semantics are
@@ -9767,7 +9798,8 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9767
9798
  'output', $6::jsonb,
9768
9799
  'terminalLogLines', $7::jsonb,
9769
9800
  'terminalProgressEvents', $8::jsonb,
9770
- 'resultSummary', $16::jsonb
9801
+ 'resultSummary', $16::jsonb,
9802
+ 'terminalWorkCounterSegment', $17::jsonb
9771
9803
  ),
9772
9804
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
9773
9805
  FROM run_terminal
@@ -9802,6 +9834,7 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9802
9834
  input.engineWake.wakeChannel,
9803
9835
  input.allowCappedCompletion === true,
9804
9836
  stringifyPostgresJson(resultSummary),
9837
+ stringifyPostgresJson(terminalWorkCounterSegment ?? null),
9805
9838
  ],
9806
9839
  );
9807
9840
  if (completed.rows.length > 0) return 'completed';
@@ -11815,6 +11848,7 @@ export async function completePostgresSchedulerAttempt(
11815
11848
  rowOutcomes?: unknown;
11816
11849
  runtimeTiming?: PlayRunnerRuntimeTiming | null;
11817
11850
  terminalLogLines?: readonly string[] | null;
11851
+ terminalWorkCounterSegment?: PlayRunLedgerTerminalWorkCounterSegment;
11818
11852
  },
11819
11853
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
11820
11854
  ): Promise<boolean> {
@@ -12002,7 +12036,8 @@ export async function completePostgresSchedulerAttempt(
12002
12036
  jsonb_build_object(
12003
12037
  'output', $9::jsonb,
12004
12038
  'terminalLogLines', $8::jsonb,
12005
- 'resultSummary', $10::jsonb
12039
+ 'resultSummary', $10::jsonb,
12040
+ 'terminalWorkCounterSegment', $11::jsonb
12006
12041
  ),
12007
12042
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
12008
12043
  FROM run_terminal
@@ -12024,6 +12059,7 @@ export async function completePostgresSchedulerAttempt(
12024
12059
  JSON.stringify(terminalLogLines),
12025
12060
  stringifyPostgresJson(ledgerTerminalResult ?? null),
12026
12061
  stringifyPostgresJson(resultSummary),
12062
+ stringifyPostgresJson(input.terminalWorkCounterSegment ?? null),
12027
12063
  ],
12028
12064
  );
12029
12065
  if (completed.rows.length === 0) {
@@ -12070,6 +12106,10 @@ export async function failPostgresSchedulerAttempt(
12070
12106
  const resultSummary = terminalRowOutcomesSummary(
12071
12107
  (input.result as { rowOutcomes?: unknown } | null)?.rowOutcomes,
12072
12108
  );
12109
+ const terminalWorkCounterSegment = terminalWorkCounterSegmentFromRunnerResult(
12110
+ input.result,
12111
+ input.attempt,
12112
+ );
12073
12113
  // Keep the recovery tail separate from result: large failures are represented
12074
12114
  // by a result reference, but their customer guidance must still be logged.
12075
12115
  const terminalLogLines = (input.terminalLogLines ?? [])
@@ -12311,7 +12351,8 @@ export async function failPostgresSchedulerAttempt(
12311
12351
  'error', $4::jsonb ->> 'message',
12312
12352
  'result', $8::jsonb,
12313
12353
  'terminalLogLines', $12::jsonb,
12314
- 'resultSummary', $13::jsonb
12354
+ 'resultSummary', $13::jsonb,
12355
+ 'terminalWorkCounterSegment', $14::jsonb
12315
12356
  ),
12316
12357
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
12317
12358
  FROM run_terminal
@@ -12336,6 +12377,7 @@ export async function failPostgresSchedulerAttempt(
12336
12377
  input.preserveRunnerTerminal === true,
12337
12378
  JSON.stringify(terminalLogLines),
12338
12379
  stringifyPostgresJson(resultSummary),
12380
+ stringifyPostgresJson(terminalWorkCounterSegment ?? null),
12339
12381
  ],
12340
12382
  );
12341
12383
  return failed.rows.length > 0;
@@ -13111,10 +13153,11 @@ export function attachRunnerWorkProgressMetadata(
13111
13153
  // Keep the two scalar counts when the detailed runtimeResources record
13112
13154
  // exceeds its metadata budget. Compact status must not scan a call log to
13113
13155
  // reconstruct physical work.
13114
- const runtimeCounters = runtimeWorkCountersFromResources(
13115
- (runnerResult as { stats?: { runtimeResources?: unknown } }).stats
13116
- ?.runtimeResources,
13117
- );
13156
+ const stats = (runnerResult as { stats?: { runtimeResources?: unknown } })
13157
+ .stats;
13158
+ const runtimeCounters =
13159
+ runtimeWorkCountersFromStats(stats) ??
13160
+ runtimeWorkCountersFromResources(stats?.runtimeResources);
13118
13161
  const outputWarnings = Array.isArray(
13119
13162
  (runnerResult as { outputWarnings?: unknown }).outputWarnings,
13120
13163
  )
@@ -13294,6 +13337,10 @@ export async function executePostgresSchedulerClaim(
13294
13337
  output: terminalOutput,
13295
13338
  rowOutcomes: (output as { rowOutcomes?: unknown } | null)?.rowOutcomes,
13296
13339
  runtimeTiming: extractRunnerRuntimeTiming(output),
13340
+ terminalWorkCounterSegment: terminalWorkCounterSegmentFromRunnerResult(
13341
+ output,
13342
+ claim.attempt,
13343
+ ),
13297
13344
  terminalLogLines: terminalLogLinesForOutbox(
13298
13345
  output,
13299
13346
  options.terminalLogOutbox,
@@ -16928,6 +16975,10 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
16928
16975
  ];
16929
16976
  }
16930
16977
  if (event.eventType === 'run.completed') {
16978
+ const terminalWorkCounterSegment =
16979
+ normalizePlayRunLedgerTerminalWorkCounterSegment(
16980
+ payloadField(event.payload, 'terminalWorkCounterSegment'),
16981
+ );
16931
16982
  const progressEvents = ledgerEventsPayloadField(
16932
16983
  event.payload,
16933
16984
  'terminalProgressEvents',
@@ -16953,6 +17004,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
16953
17004
  runId: event.runId,
16954
17005
  occurredAt,
16955
17006
  source: 'system',
17007
+ ...(terminalWorkCounterSegment ? { terminalWorkCounterSegment } : {}),
16956
17008
  result: terminalRunResultForLedger(
16957
17009
  payloadField(event.payload, 'output'),
16958
17010
  ),
@@ -16963,6 +17015,10 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
16963
17015
  ];
16964
17016
  }
16965
17017
  if (event.eventType === 'run.failed') {
17018
+ const terminalWorkCounterSegment =
17019
+ normalizePlayRunLedgerTerminalWorkCounterSegment(
17020
+ payloadField(event.payload, 'terminalWorkCounterSegment'),
17021
+ );
16966
17022
  const progressEvents = ledgerEventsPayloadField(
16967
17023
  event.payload,
16968
17024
  'terminalProgressEvents',
@@ -16991,6 +17047,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
16991
17047
  runId: event.runId,
16992
17048
  occurredAt,
16993
17049
  source: 'system',
17050
+ ...(terminalWorkCounterSegment ? { terminalWorkCounterSegment } : {}),
16994
17051
  error: stringPayloadField(event.payload, 'error'),
16995
17052
  ...(result === null || result === undefined ? {} : { result }),
16996
17053
  ...(resultSummary === undefined || resultSummary === null
@@ -17210,17 +17267,18 @@ export async function listPostgresSchedulerOutboxMaterializationPlans(
17210
17267
  }
17211
17268
 
17212
17269
  /**
17213
- * Where the projector gets its runtime-API executor token when one is not
17214
- * supplied per-event via `runtimeApi`.
17270
+ * Where the projector gets its runtime-API executor token.
17215
17271
  *
17216
17272
  * - `local_mint`: sign the token in-process with `BETTER_AUTH_SECRET`. ONLY the
17217
17273
  * app (Vercel) legitimately holds the signing key, so this is app-side only
17218
17274
  * (read-model heal path). It is the AUTHORITATIVE projector.
17219
- * - `gateway`: prove the active outbox lease to the launch-pinned deployment and
17220
- * receive a short-lived executor JWT (`requestRunLedgerDispatchToken`). The
17221
- * Fly worker has no signing key and MUST
17222
- * use this. Delivery failures use the same structured retry-or-block policy
17223
- * as the app-side projector.
17275
+ * - `gateway`: always prove the active outbox lease to the launch-pinned
17276
+ * deployment and receive a short-lived executor JWT
17277
+ * (`requestRunLedgerDispatchToken`), even if the caller supplied a
17278
+ * `runtimeApi`. The Fly worker has no signing key and MUST use this. A
17279
+ * supplied context contributes transport options only; its executor token
17280
+ * cannot bypass the lease-scoped gateway hop. Delivery failures use the same
17281
+ * structured retry-or-block policy as the app-side projector.
17224
17282
  *
17225
17283
  * Explicit by design: the old code sniffed `process.env.BETTER_AUTH_SECRET` to
17226
17284
  * pick the branch, so the worker's projection silently depended on the secret
@@ -17262,15 +17320,20 @@ export function postgresSchedulerCancellationOutboxLease(runId: string): {
17262
17320
  }
17263
17321
 
17264
17322
  export type RunLedgerOutboxDispatchInput = {
17323
+ /**
17324
+ * Optional runtime context. `gateway` authority always replaces its
17325
+ * credentials with a fresh lease-scoped token and keeps only supported
17326
+ * transport options; `local_mint` and the default authority preserve the
17327
+ * explicit context as before.
17328
+ */
17265
17329
  runtimeApi?: WorkerRuntimeApiContext | null;
17266
17330
  limit?: number;
17267
17331
  runId?: string | null;
17268
17332
  leaseOwner?: string | null;
17269
17333
  leaseSeconds?: number;
17270
17334
  /**
17271
- * Token authority for events without a per-event `runtimeApi`. Defaults to
17272
- * `local_mint` for the app-side authoritative projector. The Fly worker MUST
17273
- * pass `gateway`.
17335
+ * Token authority for the outbox dispatch. Defaults to `local_mint` for the
17336
+ * app-side authoritative projector. The Fly worker MUST pass `gateway`.
17274
17337
  */
17275
17338
  tokenAuthority?: RunLedgerOutboxTokenAuthority;
17276
17339
  };
@@ -17568,7 +17631,24 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
17568
17631
 
17569
17632
  try {
17570
17633
  const first = events[0]!;
17634
+ const tokenAuthority = input.tokenAuthority ?? 'local_mint';
17635
+ const gatewayRuntimeApi =
17636
+ tokenAuthority === 'gateway'
17637
+ ? await withClient((client) =>
17638
+ loadPostgresSchedulerRuntimeApiForRun(
17639
+ client,
17640
+ first.runId,
17641
+ first.leaseOwner
17642
+ ? { eventId: first.eventId, leaseOwner: first.leaseOwner }
17643
+ : null,
17644
+ tokenAuthority,
17645
+ options,
17646
+ input.runtimeApi?.signal,
17647
+ ),
17648
+ )
17649
+ : null;
17571
17650
  const resolvedRuntimeApi =
17651
+ gatewayRuntimeApi ??
17572
17652
  input.runtimeApi ??
17573
17653
  (await withClient((client) =>
17574
17654
  loadPostgresSchedulerRuntimeApiForRun(
@@ -17577,11 +17657,36 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
17577
17657
  first.leaseOwner
17578
17658
  ? { eventId: first.eventId, leaseOwner: first.leaseOwner }
17579
17659
  : null,
17580
- input.tokenAuthority ?? 'local_mint',
17660
+ tokenAuthority,
17581
17661
  options,
17582
17662
  ),
17583
17663
  ));
17584
- const runtimeApi = { ...resolvedRuntimeApi, retryPolicy: 'none' as const };
17664
+ const runtimeApi = {
17665
+ ...resolvedRuntimeApi,
17666
+ ...(gatewayRuntimeApi && input.runtimeApi
17667
+ ? {
17668
+ ...(input.runtimeApi.fetch
17669
+ ? { fetch: input.runtimeApi.fetch }
17670
+ : {}),
17671
+ ...(input.runtimeApi.requestTimeoutMs !== undefined
17672
+ ? { requestTimeoutMs: input.runtimeApi.requestTimeoutMs }
17673
+ : {}),
17674
+ ...(input.runtimeApi.onResponseMeta
17675
+ ? { onResponseMeta: input.runtimeApi.onResponseMeta }
17676
+ : {}),
17677
+ ...(input.runtimeApi.runtimeTestFaultHeader !== undefined
17678
+ ? {
17679
+ runtimeTestFaultHeader:
17680
+ input.runtimeApi.runtimeTestFaultHeader,
17681
+ }
17682
+ : {}),
17683
+ ...(input.runtimeApi.signal
17684
+ ? { signal: input.runtimeApi.signal }
17685
+ : {}),
17686
+ }
17687
+ : {}),
17688
+ retryPolicy: 'none' as const,
17689
+ };
17585
17690
  const renewDelivery = () =>
17586
17691
  withClient((client) =>
17587
17692
  renewPostgresRunLedgerDelivery(
@@ -17839,6 +17944,7 @@ async function loadPostgresSchedulerRuntimeApiForRun(
17839
17944
  dispatchLease: { eventId: string; leaseOwner: string } | null,
17840
17945
  tokenAuthority: RunLedgerOutboxTokenAuthority,
17841
17946
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
17947
+ signal?: AbortSignal,
17842
17948
  ): Promise<WorkerRuntimeApiContext> {
17843
17949
  const result = await client.query<{ launch_json: unknown }>(
17844
17950
  `
@@ -17879,6 +17985,7 @@ async function loadPostgresSchedulerRuntimeApiForRun(
17879
17985
  eventId: dispatchLease.eventId,
17880
17986
  leaseOwner: dispatchLease.leaseOwner,
17881
17987
  schedulerSchema: options?.schema ?? null,
17988
+ signal,
17882
17989
  });
17883
17990
  }
17884
17991
  return {
@@ -17893,6 +18000,7 @@ async function requestRunLedgerDispatchToken(input: {
17893
18000
  eventId: string;
17894
18001
  leaseOwner: string;
17895
18002
  schedulerSchema: string | null;
18003
+ signal?: AbortSignal;
17896
18004
  }): Promise<string> {
17897
18005
  const internalToken = process.env.DEEPLINE_INTERNAL_TOKEN?.trim();
17898
18006
  if (!internalToken || !input.eventId || !input.leaseOwner) {
@@ -17904,10 +18012,16 @@ async function requestRunLedgerDispatchToken(input: {
17904
18012
  const vercelProtectionHeaders = await vercelProtectionBypassHeaders({
17905
18013
  baseUrl: runtimeApiBaseUrlFromLaunch(input.launch),
17906
18014
  token: input.launch.vercelProtectionBypassToken,
18015
+ signal: input.signal,
17907
18016
  });
17908
18017
  const response = await fetch(url, {
17909
18018
  method: 'POST',
17910
- signal: AbortSignal.timeout(RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS),
18019
+ signal: input.signal
18020
+ ? AbortSignal.any([
18021
+ input.signal,
18022
+ AbortSignal.timeout(RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS),
18023
+ ])
18024
+ : AbortSignal.timeout(RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS),
17911
18025
  headers: {
17912
18026
  'content-type': 'application/json',
17913
18027
  'x-deepline-internal-token': internalToken,
@@ -1767,6 +1767,7 @@ function createDatasetFromSerializedToolList(
1767
1767
  residentRows: isPartialPreview ? null : sourceRows,
1768
1768
  sourceLabel: serialized.sourceLabel ?? null,
1769
1769
  tableNamespace: serialized.tableNamespace ?? null,
1770
+ columnTypes: serialized.columnTypes,
1770
1771
  workProgress: serialized._metadata?.workProgress,
1771
1772
  resolvers: {
1772
1773
  count: async () => serialized.count,
@@ -1,4 +1,5 @@
1
1
  import type { PlayExecutionFileRef } from './file-refs';
2
+ import type { PlayDatasetColumnTypes } from '../play-data-plane/contracts';
2
3
 
3
4
  const PLAY_DATASET_BRAND = Symbol.for('deepline.play.dataset');
4
5
  const NODE_INSPECT_CUSTOM = Symbol.for('nodejs.util.inspect.custom');
@@ -47,6 +48,7 @@ export interface SerializedPlayDataset<T> {
47
48
  sourceLabel?: string | null;
48
49
  tableNamespace?: string | null;
49
50
  columns?: string[];
51
+ columnTypes?: PlayDatasetColumnTypes;
50
52
  _metadata?: {
51
53
  workProgress?: PlayDatasetWorkProgressSummary;
52
54
  };
@@ -107,8 +109,7 @@ export type PlayDatasetTransformOptions = {
107
109
  * `materialize(limit)` or async iteration only when the dataset is intentionally
108
110
  * small and bounded. `PlayDataset` intentionally does not expose `.rows`,
109
111
  * `.toArray()`, `.length`, numeric indexing, spread, or synchronous iteration;
110
- * those hide the runtime cost of loading persisted rows into memory or make
111
- * behavior depend on whether rows happen to be resident.
112
+ * these hide loading costs or make behavior depend on row residency.
112
113
  *
113
114
  * @sdkReference runtime 190
114
115
  */
@@ -124,6 +125,8 @@ export interface PlayDataset<T> extends AsyncIterable<T> {
124
125
  readonly sourceLabel?: string | null;
125
126
  /** Runtime table name. */
126
127
  readonly tableNamespace?: string | null;
128
+ /** Advisory source types; transformed fields must infer from their values. */
129
+ readonly columnTypes?: PlayDatasetColumnTypes;
127
130
  /** Row count. */
128
131
  count(): Promise<number>;
129
132
  /** Preview rows. */
@@ -161,6 +164,7 @@ export interface PlayDataset<T> extends AsyncIterable<T> {
161
164
  sourceLabel?: string | null;
162
165
  tableNamespace?: string | null;
163
166
  columns?: string[];
167
+ columnTypes?: PlayDatasetColumnTypes;
164
168
  _metadata?: {
165
169
  workProgress?: PlayDatasetWorkProgressSummary;
166
170
  };
@@ -335,6 +339,7 @@ export function deserializePlayDatasetCell<T>(
335
339
  residentRows: rows,
336
340
  sourceLabel: dataset.sourceLabel ?? null,
337
341
  tableNamespace: dataset.tableNamespace ?? null,
342
+ columnTypes: dataset.columnTypes,
338
343
  workProgress: dataset._metadata?.workProgress,
339
344
  resolvers: {
340
345
  count: async () => rows.length,
@@ -374,6 +379,7 @@ export function deserializeLegacyPlayDataset<T>(
374
379
  residentRows: complete ? rows : null,
375
380
  sourceLabel: dataset.sourceLabel ?? null,
376
381
  tableNamespace: dataset.tableNamespace ?? null,
382
+ columnTypes: dataset.columnTypes,
377
383
  workProgress: dataset._metadata?.workProgress,
378
384
  resolvers: {
379
385
  count: async () => dataset.count,
@@ -417,6 +423,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
417
423
  readonly backing?: PlayDatasetBacking;
418
424
  readonly sourceLabel?: string | null;
419
425
  readonly tableNamespace?: string | null;
426
+ readonly columnTypes?: PlayDatasetColumnTypes;
420
427
  private readonly previewRows: readonly T[];
421
428
  private readonly previewColumns?: string[];
422
429
  private readonly workProgress?: PlayDatasetWorkProgressSummary;
@@ -435,6 +442,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
435
442
  residentRows?: readonly T[] | null;
436
443
  sourceLabel?: string | null;
437
444
  tableNamespace?: string | null;
445
+ columnTypes?: PlayDatasetColumnTypes;
438
446
  workProgress?: PlayDatasetWorkProgressSummary;
439
447
  resolvers: PlayDatasetResolvers<T>;
440
448
  }) {
@@ -449,6 +457,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
449
457
  this.previewColumns = inferPreviewColumns(this.previewRows);
450
458
  this.sourceLabel = input.sourceLabel ?? null;
451
459
  this.tableNamespace = input.tableNamespace ?? null;
460
+ this.columnTypes = input.columnTypes;
452
461
  this.workProgress = input.workProgress;
453
462
  this.resolvers = input.resolvers;
454
463
  }
@@ -592,6 +601,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
592
601
  ...(this.sourceLabel ? { sourceLabel: this.sourceLabel } : {}),
593
602
  ...(this.tableNamespace ? { tableNamespace: this.tableNamespace } : {}),
594
603
  ...(this.previewColumns ? { columns: this.previewColumns } : {}),
604
+ ...(this.columnTypes ? { columnTypes: this.columnTypes } : {}),
595
605
  ...(this.workProgress
596
606
  ? { _metadata: { workProgress: this.workProgress } }
597
607
  : {}),
@@ -721,6 +731,7 @@ function createTransformedPlayDataset<T, U>(
721
731
  backing: source.backing,
722
732
  sourceLabel,
723
733
  tableNamespace: options?.key ?? null,
734
+ columnTypes: transform.kind === 'map' ? undefined : source.columnTypes,
724
735
  resolvers: {
725
736
  count,
726
737
  peek: async (limit) => collect(limit),
@@ -747,6 +758,7 @@ export function createDeferredPlayDataset<T>(input: {
747
758
  residentRows?: readonly T[] | null;
748
759
  sourceLabel?: string | null;
749
760
  tableNamespace?: string | null;
761
+ columnTypes?: PlayDatasetColumnTypes;
750
762
  workProgress?: PlayDatasetWorkProgressSummary;
751
763
  resolvers: PlayDatasetResolvers<T>;
752
764
  }): PlayDataset<T> {
@@ -835,6 +847,7 @@ export function createPlayDataset<T>(
835
847
  sourceLabel?: string | null;
836
848
  tableNamespace?: string | null;
837
849
  datasetId?: string;
850
+ columnTypes?: PlayDatasetColumnTypes;
838
851
  },
839
852
  ): PlayDataset<T> {
840
853
  // Test/dev helper only. Production play execution should construct
@@ -850,6 +863,7 @@ export function createPlayDataset<T>(
850
863
  residentRows: materializedRows,
851
864
  sourceLabel: metadata?.sourceLabel ?? null,
852
865
  tableNamespace: metadata?.tableNamespace ?? null,
866
+ columnTypes: metadata?.columnTypes,
853
867
  resolvers: {
854
868
  count: async () => materializedRows.length,
855
869
  peek: async (limit) => materializedRows.slice(0, Math.max(0, limit)),
@@ -0,0 +1,72 @@
1
+ import type { PlayStaticPipeline } from './static-pipeline';
2
+ import { staticPipelineStorageCodec } from './static-pipeline-storage-codec';
3
+
4
+ /**
5
+ * An in-process execution snapshot, deliberately distinct from display graphs
6
+ * and Convex's bounded V1 mirror. Never serialize this wrapper: durable/wire
7
+ * contracts continue to carry the original pipeline fields.
8
+ */
9
+ export class PlayExecutionPipeline {
10
+ readonly #pipeline: PlayStaticPipeline | null;
11
+
12
+ private constructor(pipeline: PlayStaticPipeline | null) {
13
+ this.#pipeline = pipeline;
14
+ }
15
+
16
+ /** Read the entire storage carrier, so an existing V2 can never be dropped. */
17
+ static fromStored(record: {
18
+ staticPipeline?: unknown;
19
+ staticPipelineV2?: unknown;
20
+ }): PlayExecutionPipeline {
21
+ return new PlayExecutionPipeline(staticPipelineStorageCodec.read(record));
22
+ }
23
+
24
+ /** Only for complete compiler/admitted-artifact graphs, never a stored field. */
25
+ static fromAdmitted(pipeline: unknown): PlayExecutionPipeline {
26
+ if (
27
+ pipeline != null &&
28
+ (typeof pipeline !== 'object' ||
29
+ !('fields' in pipeline) ||
30
+ !Array.isArray(pipeline.fields) ||
31
+ !('substeps' in pipeline) ||
32
+ !Array.isArray(pipeline.substeps))
33
+ ) {
34
+ throw new Error('Invalid Play execution pipeline snapshot.');
35
+ }
36
+ // Do not compile or normalize retained contracts under current authoring
37
+ // rules. In particular, preserve additive fields and historical contracts.
38
+ return new PlayExecutionPipeline(
39
+ (pipeline as PlayStaticPipeline | null | undefined) ?? null,
40
+ );
41
+ }
42
+
43
+ get pipeline(): PlayStaticPipeline | null {
44
+ return this.#pipeline;
45
+ }
46
+ }
47
+
48
+ /** Reject accidental raw/serialized inputs before launch has side effects. */
49
+ export function readPlayExecutionPipeline(
50
+ snapshot: PlayExecutionPipeline | undefined,
51
+ ): PlayStaticPipeline | null {
52
+ if (snapshot === undefined) return null;
53
+ if (!(snapshot instanceof PlayExecutionPipeline)) {
54
+ throw new Error('Play launch requires a PlayExecutionPipeline snapshot.');
55
+ }
56
+ return snapshot.pipeline;
57
+ }
58
+
59
+ /** Execution readers cannot see (and accidentally forward) the stored mirror. */
60
+ export function readStoredPlayExecutionSnapshot<
61
+ T extends { staticPipeline?: unknown; staticPipelineV2?: unknown },
62
+ >(
63
+ record: T,
64
+ ): Omit<T, 'staticPipeline' | 'staticPipelineV2'> & {
65
+ executionPipeline: PlayExecutionPipeline;
66
+ } {
67
+ const { staticPipeline: _v1, staticPipelineV2: _v2, ...metadata } = record;
68
+ return {
69
+ ...metadata,
70
+ executionPipeline: PlayExecutionPipeline.fromStored(record),
71
+ };
72
+ }
@@ -61,7 +61,6 @@ export function buildPlayRunContractSnapshot(input: {
61
61
  artifactStorageKey: string;
62
62
  artifact: Pick<PlayBundleArtifact, 'compatibility'>;
63
63
  validatedSnapshot: PlayRunLaunchValidationSnapshot;
64
- transportStaticPipeline?: unknown;
65
64
  }): PreparedPlayRunContractSnapshot {
66
65
  // The runtime backend is launch infrastructure, not an artifact contract.
67
66
  // Copy the admitted artifact compatibility so resumed work cannot acquire
@@ -69,8 +68,7 @@ export function buildPlayRunContractSnapshot(input: {
69
68
  const compatibility = normalizePlayContractCompatibility(
70
69
  input.artifact.compatibility,
71
70
  );
72
- const staticPipeline =
73
- input.transportStaticPipeline ?? input.validatedSnapshot.staticPipeline;
71
+ const staticPipeline = input.validatedSnapshot.staticPipeline;
74
72
  return {
75
73
  source: input.source ?? 'published',
76
74
  revisionVersion: null,
@@ -1,14 +1,13 @@
1
1
  // Full-table view basic filters. A tiny, deliberately closed predicate model
2
2
  // that compiles mechanically to a parameterized SQL WHERE clause against a
3
- // runtime sheet's physical (jsonb) user-data columns.
3
+ // runtime sheet's physical user-data columns.
4
4
  //
5
5
  // This is NOT free-text SQL: operators come from a closed enum, columns must
6
6
  // be on a caller-supplied allowlist (the sheet's real columns), and every value
7
7
  // is bound as a query parameter. Multiple filters AND together. No OR groups.
8
8
  //
9
- // Sheet user-data columns are stored one-per-column as `jsonb` (a JSON scalar
10
- // like "completed", 19991, or an object). `col #>> '{}'` extracts the scalar as
11
- // text; numeric comparisons cast that text to numeric.
9
+ // Sheet user-data columns may be native scalars or JSONB. Normalize each cell
10
+ // through to_jsonb before applying the shared scalar and numeric predicates.
12
11
 
13
12
  // ── The operator set, and why it grew ────────────────────────────────────────
14
13
  //
@@ -204,7 +203,7 @@ function coerceFilterNumber(value: unknown, index: number): number {
204
203
  // → 19991, an object → its JSON text. The single-arg `#>> '{}'` path returns the
205
204
  // whole value as text (Postgres jsonb → text).
206
205
  function columnAsText(quotedColumn: string): string {
207
- return `(${quotedColumn} #>> '{}')`;
206
+ return `(to_jsonb(${quotedColumn}) #>> '{}')`;
208
207
  }
209
208
 
210
209
  function quoteIdentifier(value: string): string {
@@ -302,9 +301,10 @@ export function compileSheetFilterWhere(input: {
302
301
  const num = coerceFilterNumber(filter.value, index);
303
302
  const sqlOp = NUMERIC_SQL_OPERATOR[filter.op];
304
303
  // Guard the numeric cast to jsonb numbers only, so a stray non-numeric
305
- // cell can never abort the whole query with a cast error.
304
+ // cell can never abort the whole query with a cast error. CASE also
305
+ // prevents PostgreSQL from reordering a numeric cast before its guard.
306
306
  clauses.push(
307
- `(jsonb_typeof(${quoted}) = 'number' AND ${asText}::numeric ${sqlOp} $${paramIdx})`,
307
+ `((CASE WHEN jsonb_typeof(to_jsonb(${quoted})) = 'number' THEN ${asText}::numeric END) ${sqlOp} $${paramIdx})`,
308
308
  );
309
309
  params.push(num);
310
310
  paramIdx += 1;
@@ -317,12 +317,8 @@ export function compileSheetFilterWhere(input: {
317
317
  // range control means by it. The guard is the same one the scalar
318
318
  // comparisons use: a non-numeric cell simply does not match, rather
319
319
  // than aborting the query on a cast.
320
- const body = `${asText}::numeric BETWEEN $${paramIdx} AND $${paramIdx + 1}`;
321
- clauses.push(
322
- `(jsonb_typeof(${quoted}) = 'number' AND ${
323
- filter.op === 'between' ? body : `NOT (${body})`
324
- })`,
325
- );
320
+ const body = `(CASE WHEN jsonb_typeof(to_jsonb(${quoted})) = 'number' THEN ${asText}::numeric END) BETWEEN $${paramIdx} AND $${paramIdx + 1}`;
321
+ clauses.push(`(${filter.op === 'between' ? body : `NOT (${body})`})`);
326
322
  params.push(min, max);
327
323
  paramIdx += 2;
328
324
  break;