deepline 0.3.149 → 0.3.151

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.
@@ -1,5 +1,5 @@
1
1
  import { createWriteStream } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { stat } from 'node:fs/promises';
4
4
  import { Readable } from 'node:stream';
5
5
  import { pipeline } from 'node:stream/promises';
@@ -47,6 +47,7 @@ import {
47
47
  LEGACY_RUNTIME_RECEIPT_WIRE_HEADER,
48
48
  } from './/runtime-contract';
49
49
  import { PLAY_RUNTIME_API_COMPAT_PATH } from './/runtime-api-paths';
50
+ import { buildRunLedgerEventsIdempotencyKey } from './run-ledger-projection-contract';
50
51
  import {
51
52
  RUNTIME_API_ACCEPTS_HEADER,
52
53
  gzipRuntimeApiRequestBody,
@@ -157,15 +158,21 @@ type RuntimeApiRequest =
157
158
  action: 'create_db_session';
158
159
  } & CreateDbSessionRequest)
159
160
  | {
160
- action: 'append_run_events';
161
+ action: 'append_run_events' | 'project_run_events';
161
162
  playId: string;
162
163
  events: PlayRunLedgerIngressEvent[];
163
164
  idempotencyKey?: string;
165
+ deliveryIdempotencyKey?: string;
166
+ deliveryLeaseOwner?: string;
164
167
  }
165
168
  | {
166
169
  action: 'start_run';
170
+ deliveryLeaseOwner?: string;
167
171
  idempotencyKey?: string;
168
172
  playName: string;
173
+ playReference?: string | null;
174
+ definitionScope?: 'org' | 'system';
175
+ revisionId?: string | null;
169
176
  runId: string;
170
177
  artifactStorageKey?: string | null;
171
178
  artifactHash?: string | null;
@@ -200,6 +207,7 @@ type RuntimeApiRequest =
200
207
  }
201
208
  | {
202
209
  action: 'project_run_created';
210
+ deliveryLeaseOwner?: string;
203
211
  idempotencyKey: string;
204
212
  playName: string;
205
213
  playReference?: string | null;
@@ -736,7 +744,8 @@ function isRetryableAppRuntimeResponse(input: {
736
744
  // projection. Do not make every 403 retryable: only this exact Vercel HTML
737
745
  // response on a replay-safe action is an intermediary failure.
738
746
  if (
739
- input.action === 'append_run_events' &&
747
+ (input.action === 'append_run_events' ||
748
+ input.action === 'project_run_events') &&
740
749
  isVercelSecurityCheckpointResponse(input)
741
750
  ) {
742
751
  return true;
@@ -964,6 +973,9 @@ function isRetryableAppRuntimeAction(
964
973
  ): boolean {
965
974
  return (
966
975
  action === 'append_run_events' ||
976
+ // Classification remains replay-safe for the durable outbox even though
977
+ // projectRunEventsViaAppRuntime disables in-process transport retries.
978
+ action === 'project_run_events' ||
967
979
  action === 'apply_row_updates' ||
968
980
  action === 'compute_billing_finalize' ||
969
981
  action === 'compute_billing_record_item' ||
@@ -2312,6 +2324,85 @@ async function appendRunEventsViaAppRuntimeRaw(
2312
2324
  });
2313
2325
  }
2314
2326
 
2327
+ /** Leased delivery only. Every transport piece has its own stable receipt. */
2328
+ export async function projectRunEventsViaAppRuntime(
2329
+ context: WorkerRuntimeApiContext,
2330
+ input: {
2331
+ playId: string;
2332
+ events: PlayRunLedgerIngressEvent[];
2333
+ idempotencyKey: string;
2334
+ deliveryLeaseOwner: string;
2335
+ beforeBatch: () => Promise<void>;
2336
+ },
2337
+ ): Promise<void> {
2338
+ const batches = partitionRunEventsForAppRuntime(input.events);
2339
+ const receiptBatch = runLedgerReceiptBatchIndex(batches);
2340
+ for (const [index, events] of batches.entries()) {
2341
+ await input.beforeBatch();
2342
+ // Retain the original receipt for the terminal (or final) piece so an acknowledged
2343
+ // legacy delivery stays acknowledged across the reader rollout.
2344
+ const idempotencyKey =
2345
+ index === receiptBatch
2346
+ ? input.idempotencyKey
2347
+ : buildRunLedgerEventsIdempotencyKey({
2348
+ runId: input.playId,
2349
+ deliveryDigest: createHash('sha256')
2350
+ .update(JSON.stringify([input.idempotencyKey, index, events]))
2351
+ .digest('hex'),
2352
+ });
2353
+ const request = {
2354
+ playId: input.playId,
2355
+ events,
2356
+ idempotencyKey,
2357
+ deliveryIdempotencyKey: input.idempotencyKey,
2358
+ deliveryLeaseOwner: input.deliveryLeaseOwner,
2359
+ };
2360
+ // The durable outbox owns recovery; this helper does not retry OCCs or
2361
+ // other delivery failures inside an already-leased invocation.
2362
+ const deliveryContext = { ...context, retryPolicy: 'none' as const };
2363
+ try {
2364
+ await postAppRuntimeApi(deliveryContext, {
2365
+ action: 'project_run_events',
2366
+ ...request,
2367
+ });
2368
+ } catch (error) {
2369
+ if (
2370
+ !(error instanceof AppRuntimeApiResponseError) ||
2371
+ !isLegacyProjectRunCreatedUnsupportedActionResponse({
2372
+ status: error.status,
2373
+ body: error.detail,
2374
+ })
2375
+ )
2376
+ throw error;
2377
+ // Launch-pinned old apps already implement equivalent keyed projection.
2378
+ // A real auth refusal never selects this compatibility adapter.
2379
+ // The capability probe was a network round trip. Re-check ownership
2380
+ // before the legacy request too; reclamation must stop a stale sender.
2381
+ await input.beforeBatch();
2382
+ await postAppRuntimeApi(deliveryContext, {
2383
+ action: 'append_run_events',
2384
+ ...request,
2385
+ });
2386
+ }
2387
+ }
2388
+ }
2389
+
2390
+ function runLedgerReceiptBatchIndex(
2391
+ batches: PlayRunLedgerIngressEvent[][],
2392
+ ): number {
2393
+ for (let index = batches.length - 1; index >= 0; index -= 1) {
2394
+ if (
2395
+ batches[index]!.some((event) =>
2396
+ ['run.completed', 'run.failed', 'run.cancelled'].includes(
2397
+ event.key ?? event.type ?? '',
2398
+ ),
2399
+ )
2400
+ )
2401
+ return index;
2402
+ }
2403
+ return batches.length - 1;
2404
+ }
2405
+
2315
2406
  /**
2316
2407
  * Append Run Ledger events through the bounded transport used by every runtime
2317
2408
  * producer. Scheduler terminal outbox events can carry a complete log replay,
@@ -2368,7 +2459,11 @@ export async function appendRunEventsViaAppRuntime(
2368
2459
  export async function startRunViaAppRuntime(
2369
2460
  context: WorkerRuntimeApiContext,
2370
2461
  input: {
2462
+ deliveryLeaseOwner?: string;
2371
2463
  playName: string;
2464
+ playReference?: string | null;
2465
+ definitionScope?: 'org' | 'system';
2466
+ revisionId?: string | null;
2372
2467
  runId: string;
2373
2468
  artifactStorageKey?: string | null;
2374
2469
  artifactHash?: string | null;
@@ -10,6 +10,13 @@ const UUID_PATTERN =
10
10
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11
11
  const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
12
12
 
13
+ export class RunLedgerProjectionLeaseLostError extends Error {
14
+ constructor() {
15
+ super('Run Ledger projection requires the current outbox delivery lease.');
16
+ this.name = 'RunLedgerProjectionLeaseLostError';
17
+ }
18
+ }
19
+
13
20
  function requireRunId(runId: string): void {
14
21
  if (!runId.trim()) {
15
22
  throw new Error('Run Ledger projection idempotency key requires a run id.');
@@ -760,6 +760,12 @@ function mergeMonotonicStepProgress(input: {
760
760
  }): PlayRunLedgerStepProgress {
761
761
  const current = input.current ?? {};
762
762
  const next = input.next;
763
+ const supersededRows = maxProgressNumber(
764
+ current.supersededRows,
765
+ next.supersededRows === undefined
766
+ ? undefined
767
+ : Math.max(0, next.supersededRows),
768
+ );
763
769
  return {
764
770
  ...current,
765
771
  ...next,
@@ -786,11 +792,7 @@ function mergeMonotonicStepProgress(input: {
786
792
  ),
787
793
  }
788
794
  : {}),
789
- ...(next.supersededRows !== undefined
790
- ? { supersededRows: Math.max(0, next.supersededRows) }
791
- : current.supersededRows !== undefined
792
- ? { supersededRows: current.supersededRows }
793
- : {}),
795
+ ...(supersededRows !== undefined ? { supersededRows } : {}),
794
796
  };
795
797
  }
796
798
 
@@ -35,6 +35,7 @@ export const RUNTIME_OPERATION_CAPABILITIES = {
35
35
  create_db_session: 'db.session',
36
36
  repair_runtime_storage_grants: 'sheet.write',
37
37
  append_run_events: 'run.progress',
38
+ project_run_events: 'run.progress',
38
39
  save_results: 'run.progress',
39
40
  start_run: 'run.progress',
40
41
  record_first_claim_latency: 'run.progress',
@@ -56,6 +57,10 @@ export const RUNTIME_OPERATION_CONTRACT: DeployedOperationContract =
56
57
  id: 'runtime-api-actions',
57
58
  actions: RUNTIME_OPERATION_CAPABILITIES,
58
59
  olderReceiver: {
60
+ project_run_events: {
61
+ kind: 'equivalent-fallback',
62
+ action: 'append_run_events',
63
+ },
59
64
  // The runner retains the complete result before attempting this
60
65
  // compact completion. An older app receiver reports the action as
61
66
  // unsupported, and the adapter retries the established full-payload
@@ -250,7 +250,7 @@ export type PlaySchedulerRunHandle = {
250
250
  signal?: AbortSignal;
251
251
  }): AsyncIterable<PlaySchedulerProgressEvent>;
252
252
  /** Cooperatively cancel the run. */
253
- cancel(): Promise<void>;
253
+ cancel(reason?: string): Promise<void>;
254
254
  /** Inject an external event (HITL, webhook). */
255
255
  signal(payload: PlaySchedulerSignalPayload): Promise<void>;
256
256
  /** Block until terminal state and return final envelope. */
@@ -216,14 +216,14 @@ function absurdRunHandle(input: {
216
216
  return error;
217
217
  };
218
218
 
219
- const requestCancellation = async () => {
219
+ const requestCancellation = async (reason?: string) => {
220
220
  await runCancellationSingleFlight(cancellationKey, async () => {
221
221
  // First move the durable engine task into its cancellation lane. If the
222
222
  // scheduler tables are briefly locked, the worker reconciler can finish
223
223
  // the terminal write later without another user request piling up.
224
224
  await cancelAbsurdTask();
225
225
  try {
226
- await delegate.cancel();
226
+ await delegate.cancel(reason);
227
227
  } catch (error) {
228
228
  throw cancellationPendingError(error);
229
229
  }