deepline 0.3.81 → 0.3.83

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.
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.81',
202
+ version: '0.3.83',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {
@@ -18,6 +18,7 @@ import {
18
18
  reducePlayRunLedgerEvents,
19
19
  slicePositionalLogLines,
20
20
  type PlayRunLedgerEvent,
21
+ type PlayRunLedgerIngressEvent,
21
22
  type PlayRunLedgerSnapshot,
22
23
  } from './/run-ledger';
23
24
  import type {
@@ -41,8 +42,8 @@ import type {
41
42
  } from './/protocol';
42
43
  import {
43
44
  PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER,
44
- PLAY_RUNTIME_CONTRACT,
45
- PLAY_RUNTIME_CONTRACT_HEADER,
45
+ LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT,
46
+ LEGACY_RUNTIME_RECEIPT_WIRE_HEADER,
46
47
  } from './/runtime-contract';
47
48
  import { PLAY_RUNTIME_API_COMPAT_PATH } from './/runtime-api-paths';
48
49
  import {
@@ -149,7 +150,7 @@ type RuntimeApiRequest =
149
150
  | {
150
151
  action: 'append_run_events';
151
152
  playId: string;
152
- events: PlayRunLedgerEvent[];
153
+ events: PlayRunLedgerIngressEvent[];
153
154
  idempotencyKey?: string;
154
155
  }
155
156
  | {
@@ -380,18 +381,42 @@ async function suppressBulkClaimResponseUntilTimeout(
380
381
  });
381
382
  }
382
383
 
384
+ function isRecord(value: unknown): value is Record<string, unknown> {
385
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
386
+ }
387
+
383
388
  function splitRunEventForAppRuntime(
384
- event: PlayRunLedgerEvent,
385
- ): PlayRunLedgerEvent[] {
386
- if (event.type !== 'log.appended' || event.lines.length === 0) return [event];
387
- const split: PlayRunLedgerEvent[] = [];
389
+ event: PlayRunLedgerIngressEvent,
390
+ ): PlayRunLedgerIngressEvent[] {
391
+ const eventType =
392
+ typeof event.key === 'string'
393
+ ? event.key
394
+ : typeof event.type === 'string'
395
+ ? event.type
396
+ : null;
397
+ const payload = isRecord(event.payload) ? event.payload : null;
398
+ const payloadLines = payload?.lines;
399
+ const payloadOwnsLines = Array.isArray(payloadLines);
400
+ const rawLines = payloadOwnsLines ? payloadLines : event.lines;
401
+ const lines = Array.isArray(rawLines)
402
+ ? rawLines.filter((line): line is string => typeof line === 'string')
403
+ : [];
404
+ if (eventType !== 'log.appended' || lines.length === 0) return [event];
405
+ const payloadChannelOffset =
406
+ typeof payload?.channelOffset === 'number'
407
+ ? payload.channelOffset
408
+ : undefined;
409
+ const eventChannelOffset =
410
+ typeof event.channelOffset === 'number' ? event.channelOffset : undefined;
411
+ const split: PlayRunLedgerIngressEvent[] = [];
412
+ const { lines: _outerLines, ...eventWithoutOuterLines } = event;
388
413
  let index = 0;
389
- while (index < event.lines.length) {
414
+ while (index < lines.length) {
390
415
  const start = index;
391
416
  let bytes = 2;
392
- while (index < event.lines.length) {
417
+ while (index < lines.length) {
393
418
  const lineBytes = appRuntimeTextEncoder.encode(
394
- JSON.stringify(event.lines[index]!),
419
+ JSON.stringify(lines[index]!),
395
420
  ).length;
396
421
  if (
397
422
  index > start &&
@@ -402,22 +427,40 @@ function splitRunEventForAppRuntime(
402
427
  bytes += lineBytes + 1;
403
428
  index += 1;
404
429
  }
405
- split.push({
406
- ...event,
407
- lines: event.lines.slice(start, index),
408
- ...(typeof event.channelOffset === 'number'
409
- ? { channelOffset: event.channelOffset + start }
410
- : {}),
411
- });
430
+ const batchLines = lines.slice(start, index);
431
+ split.push(
432
+ payload
433
+ ? {
434
+ ...eventWithoutOuterLines,
435
+ ...(payloadOwnsLines ? {} : { lines: batchLines }),
436
+ payload: {
437
+ ...payload,
438
+ ...(payloadOwnsLines ? { lines: batchLines } : {}),
439
+ ...(payloadChannelOffset === undefined
440
+ ? {}
441
+ : { channelOffset: payloadChannelOffset + start }),
442
+ },
443
+ ...(eventChannelOffset === undefined
444
+ ? {}
445
+ : { channelOffset: eventChannelOffset + start }),
446
+ }
447
+ : {
448
+ ...event,
449
+ lines: batchLines,
450
+ ...(eventChannelOffset === undefined
451
+ ? {}
452
+ : { channelOffset: eventChannelOffset + start }),
453
+ },
454
+ );
412
455
  }
413
456
  return split;
414
457
  }
415
458
 
416
459
  export function partitionRunEventsForAppRuntime(
417
- events: readonly PlayRunLedgerEvent[],
418
- ): PlayRunLedgerEvent[][] {
419
- const batches: PlayRunLedgerEvent[][] = [];
420
- let current: PlayRunLedgerEvent[] = [];
460
+ events: readonly PlayRunLedgerIngressEvent[],
461
+ ): PlayRunLedgerIngressEvent[][] {
462
+ const batches: PlayRunLedgerIngressEvent[][] = [];
463
+ let current: PlayRunLedgerIngressEvent[] = [];
421
464
  let currentBytes = 2;
422
465
  for (const event of events.flatMap(splitRunEventForAppRuntime)) {
423
466
  const eventBytes = appRuntimeTextEncoder.encode(
@@ -742,9 +785,15 @@ function appRuntimeRetryAfterMs(
742
785
  }
743
786
  } catch {}
744
787
 
745
- const retryAfterSeconds = Number(response.headers.get('retry-after'));
746
- return Number.isFinite(retryAfterSeconds)
747
- ? Math.max(0, Math.min(retryAfterSeconds * 1_000, 30_000))
788
+ const retryAfter = response.headers.get('retry-after')?.trim();
789
+ if (!retryAfter) return null;
790
+ const retryAfterSeconds = Number(retryAfter);
791
+ if (Number.isFinite(retryAfterSeconds)) {
792
+ return Math.max(0, Math.min(retryAfterSeconds * 1_000, 30_000));
793
+ }
794
+ const retryAt = Date.parse(retryAfter);
795
+ return Number.isFinite(retryAt)
796
+ ? Math.max(0, Math.min(retryAt - Date.now(), 30_000))
748
797
  : null;
749
798
  }
750
799
 
@@ -997,6 +1046,8 @@ export class AppRuntimeApiResponseError extends Error {
997
1046
  readonly code: string | null;
998
1047
  readonly requestId: string | null;
999
1048
  readonly retryable: boolean;
1049
+ /** Server-requested delay, retained for durable callers after local retries end. */
1050
+ readonly retryAfterMs: number | null;
1000
1051
  readonly detail: string;
1001
1052
 
1002
1053
  constructor(input: {
@@ -1005,6 +1056,7 @@ export class AppRuntimeApiResponseError extends Error {
1005
1056
  code?: string | null;
1006
1057
  requestId?: string | null;
1007
1058
  retryable: boolean;
1059
+ retryAfterMs?: number | null;
1008
1060
  detail: string;
1009
1061
  boundaryLabel?: string;
1010
1062
  }) {
@@ -1020,6 +1072,10 @@ export class AppRuntimeApiResponseError extends Error {
1020
1072
  this.code = input.code?.trim() || null;
1021
1073
  this.requestId = input.requestId?.trim() || null;
1022
1074
  this.retryable = input.retryable;
1075
+ this.retryAfterMs =
1076
+ typeof input.retryAfterMs === 'number' && Number.isFinite(input.retryAfterMs)
1077
+ ? Math.max(0, Math.floor(input.retryAfterMs))
1078
+ : null;
1023
1079
  this.detail = input.detail;
1024
1080
  }
1025
1081
  }
@@ -1038,7 +1094,7 @@ const APP_RUNTIME_CAPACITY_ERROR_CODES = new Set([
1038
1094
  * than spending the ordinary request retry ladder.
1039
1095
  */
1040
1096
  export class AppRuntimeApiCapacityError extends AppRuntimeApiResponseError {
1041
- readonly retryAfterMs: number;
1097
+ declare readonly retryAfterMs: number;
1042
1098
 
1043
1099
  constructor(input: {
1044
1100
  action: RuntimeApiRequest['action'];
@@ -1138,7 +1194,9 @@ async function postAppRuntimeApi<TResponse>(
1138
1194
  headers: {
1139
1195
  'content-type': 'application/json',
1140
1196
  authorization: `Bearer ${token}`,
1141
- [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
1197
+ [LEGACY_RUNTIME_RECEIPT_WIRE_HEADER]: String(
1198
+ LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT,
1199
+ ),
1142
1200
  [PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER]: transportAttemptId,
1143
1201
  ...vercelHeaders,
1144
1202
  ...(context.preferredReceiptGatewayMachineId?.trim()
@@ -1372,6 +1430,7 @@ async function postAppRuntimeApi<TResponse>(
1372
1430
  body: responseText,
1373
1431
  contentType: response.headers.get('content-type'),
1374
1432
  }),
1433
+ retryAfterMs: appRuntimeRetryAfterMs(response, responseText),
1375
1434
  detail: summarizeAppRuntimeErrorBody(responseText),
1376
1435
  boundaryLabel,
1377
1436
  });
@@ -1868,7 +1927,7 @@ async function appendRunEventsViaAppRuntimeRaw(
1868
1927
  context: WorkerRuntimeApiContext,
1869
1928
  input: {
1870
1929
  playId: string;
1871
- events: PlayRunLedgerEvent[];
1930
+ events: PlayRunLedgerIngressEvent[];
1872
1931
  idempotencyKey?: string;
1873
1932
  },
1874
1933
  ): Promise<void> {
@@ -1888,7 +1947,7 @@ export async function appendRunEventsViaAppRuntime(
1888
1947
  context: WorkerRuntimeApiContext,
1889
1948
  input: {
1890
1949
  playId: string;
1891
- events: PlayRunLedgerEvent[];
1950
+ events: PlayRunLedgerIngressEvent[];
1892
1951
  idempotencyKey?: string;
1893
1952
  },
1894
1953
  ): Promise<void> {
@@ -1905,10 +1964,14 @@ export async function appendRunEventsViaAppRuntime(
1905
1964
  for (let index = batches.length - 1; index >= 0; index -= 1) {
1906
1965
  if (
1907
1966
  batches[index]!.some(
1908
- (event) =>
1909
- event.type === 'run.completed' ||
1910
- event.type === 'run.failed' ||
1911
- event.type === 'run.cancelled',
1967
+ (event) => {
1968
+ const key = event.key ?? event.type;
1969
+ return (
1970
+ key === 'run.completed' ||
1971
+ key === 'run.failed' ||
1972
+ key === 'run.cancelled'
1973
+ );
1974
+ },
1912
1975
  )
1913
1976
  ) {
1914
1977
  terminalBatchIndex = index;
@@ -64,6 +64,10 @@ import {
64
64
  PLAY_RUNTIME_OPERATION_ATTEMPT_HEADER,
65
65
  PLAY_RUNTIME_OPERATION_ID_HEADER,
66
66
  } from './runtime-api-paths';
67
+ import {
68
+ DURABLE_INVOCATION_FENCE_VERSION,
69
+ durableInvocationFenceExecutePath,
70
+ } from './durable-invocation-fence';
67
71
  import {
68
72
  isRetryableSecretResolutionStatus,
69
73
  secretResolutionRetryDecision,
@@ -1145,7 +1149,7 @@ function isUnmarkedExecutionGateway502(input: {
1145
1149
  return false;
1146
1150
  }
1147
1151
  try {
1148
- return new URL(input.url).pathname.endsWith('/execute-fenced-v1');
1152
+ return /\/execute-fenced-v[12]$/.test(new URL(input.url).pathname);
1149
1153
  } catch {
1150
1154
  return false;
1151
1155
  }
@@ -2110,6 +2114,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2110
2114
  */
2111
2115
  private readonly durableMappedToolResultsBackedByReceipts: boolean;
2112
2116
  private readonly durableDirectToolResultsBackedByReceipts: boolean;
2117
+ private durableInvocationFenceVerification: Promise<boolean> | null = null;
2113
2118
  private steps: PlayStep[] = [];
2114
2119
  private explicitMapInvocationKeys = new Set<string>();
2115
2120
  /** The map step currently being built — substeps go here instead of top-level. */
@@ -13452,17 +13457,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13452
13457
  const uncachedAsyncLifecycleCall =
13453
13458
  options?.uncachedAsyncLifecycleCall === true ||
13454
13459
  shouldWaitForAsyncOperation;
13455
- const usesDurableInvocationFence =
13456
- requestsDurableInvocationFence && !uncachedAsyncLifecycleCall;
13457
- const executeSuffix = usesDurableInvocationFence
13458
- ? 'execute-fenced-v1'
13459
- : 'execute';
13460
13460
  // Keep receipts, runner heartbeats, terminals, sheets, and runtime control
13461
13461
  // on baseUrl (the receipt gateway). Only the potentially long-lived
13462
13462
  // provider request may use the separately managed execution relay.
13463
13463
  const executionBaseUrl =
13464
13464
  this.#options.executionGatewayBaseUrl?.trim() || this.#options.baseUrl;
13465
- const url = `${executionBaseUrl.replace(/\/$/, '')}/api/v2/integrations/${encodeURIComponent(toolId)}/${executeSuffix}`;
13466
13465
  const toolErrorSchemaVersion = this.currentToolErrorSchemaVersion;
13467
13466
  const timeoutMs = resolveToolRuntimeTimeoutMs(
13468
13467
  toolId,
@@ -13551,6 +13550,49 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13551
13550
  retryPolicy?.retrySafeTransientHttp === true;
13552
13551
  const requiresExecutionFence =
13553
13552
  retryPolicy?.requiresExecutionFence === true;
13553
+ // Fenced execution is an operation-specific opt-in. A gateway may
13554
+ // support execute-fenced-v2 while ordinary provider calls continue
13555
+ // through /execute with no durable-operation metadata at all.
13556
+ const usesDurableInvocationFence =
13557
+ requestsDurableInvocationFence &&
13558
+ requiresExecutionFence &&
13559
+ !uncachedAsyncLifecycleCall;
13560
+ if (
13561
+ usesDurableInvocationFence &&
13562
+ this.#options.verifyDurableInvocationFence
13563
+ ) {
13564
+ const verification =
13565
+ this.durableInvocationFenceVerification ??
13566
+ this.#options.verifyDurableInvocationFence();
13567
+ this.durableInvocationFenceVerification = verification;
13568
+ let supportsDurableInvocationFence: boolean;
13569
+ try {
13570
+ supportsDurableInvocationFence = await verification;
13571
+ } catch (error) {
13572
+ if (this.durableInvocationFenceVerification === verification) {
13573
+ this.durableInvocationFenceVerification = null;
13574
+ }
13575
+ throw error;
13576
+ }
13577
+ // A health probe is only a positive capability observation. A
13578
+ // transient probe failure must not poison every later fenced call
13579
+ // in this Play after the gateway has recovered.
13580
+ if (!supportsDurableInvocationFence) {
13581
+ if (this.durableInvocationFenceVerification === verification) {
13582
+ this.durableInvocationFenceVerification = null;
13583
+ }
13584
+ throw new Error(
13585
+ 'The active execution gateway does not support execute-fenced-v2. ' +
13586
+ 'No provider request was submitted; deploy the compatible gateway receiver first.',
13587
+ );
13588
+ }
13589
+ }
13590
+ const executeSuffix = usesDurableInvocationFence
13591
+ ? durableInvocationFenceExecutePath(
13592
+ DURABLE_INVOCATION_FENCE_VERSION,
13593
+ )
13594
+ : 'execute';
13595
+ const url = `${executionBaseUrl.replace(/\/$/, '')}/api/v2/integrations/${encodeURIComponent(toolId)}/${executeSuffix}`;
13554
13596
  const fixtureReplaySafe = this.#options.integrationMode === 'fixture';
13555
13597
  const readOnlyQuery = isQueryResultDatasetReadRequest(
13556
13598
  toolId,
@@ -13631,7 +13673,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13631
13673
  ? { async_operation_wait: true }
13632
13674
  : {}),
13633
13675
  ...(usesDurableInvocationFence
13634
- ? { invocation_fence_version: 1 }
13676
+ ? {
13677
+ invocation_fence_version:
13678
+ DURABLE_INVOCATION_FENCE_VERSION,
13679
+ }
13635
13680
  : {}),
13636
13681
  ...(durableCallReceiptKey
13637
13682
  ? {
@@ -13859,16 +13904,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13859
13904
  (await this.#options.getToolProvider?.(toolId))?.trim() ||
13860
13905
  provider;
13861
13906
  if (shouldRouteFixtureProvider(canonicalProvider)) {
13862
- if (!durableCallReceiptKey) {
13863
- throw new Error(
13864
- 'Configured fixture response timing requires a durable call receipt key.',
13865
- );
13866
- }
13867
13907
  const canonicalOperation =
13868
13908
  (
13869
13909
  await this.#options.getToolOperation?.(toolId)
13870
13910
  )?.trim() || toolId;
13871
- const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${durableCallReceiptKey}`;
13911
+ // A fixture schedule needs a stable logical-call identity,
13912
+ // not a cache receipt. Ordinary /execute calls deliberately
13913
+ // do not create durable receipts; deeplineRequestId remains
13914
+ // stable across their physical transport retries.
13915
+ const fixtureRequestIdentity =
13916
+ durableCallReceiptKey ?? deeplineRequestId;
13917
+ const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${fixtureRequestIdentity}`;
13872
13918
  const behaviorDigest = stableDigest(
13873
13919
  JSON.stringify(fixtureBehavior),
13874
13920
  );
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `execute-fenced-v2` is the first operation that requires semantic provider
3
+ * identity. It is intentionally a named operation rather than a runtime-wide
4
+ * deployment contract: callers can preflight this one receiver capability
5
+ * before they submit a provider request.
6
+ */
7
+ export const DURABLE_INVOCATION_FENCE_VERSION = 2 as const;
8
+ /** Attestation understood by pre-v2 runners during their drain window. */
9
+ export const LEGACY_DURABLE_INVOCATION_FENCE_ATTESTATION_HEADER =
10
+ 'x-deepline-invocation-fence';
11
+ export const LEGACY_DURABLE_INVOCATION_FENCE_ATTESTATION_VALUE = 'v1';
12
+ /**
13
+ * v2 is a distinct receiver capability, not a replacement value for the v1
14
+ * header. Keeping the two headers separate lets a receiver serve sleeping v1
15
+ * runners while new callers prove that execute-fenced-v2 is available.
16
+ */
17
+ export const DURABLE_INVOCATION_FENCE_ATTESTATION_HEADER =
18
+ 'x-deepline-invocation-fence-v2';
19
+ export const DURABLE_INVOCATION_FENCE_ATTESTATION_VALUE = 'v2';
20
+
21
+ export function durableInvocationFenceAttestationHeaders(): Record<
22
+ string,
23
+ string
24
+ > {
25
+ return {
26
+ [LEGACY_DURABLE_INVOCATION_FENCE_ATTESTATION_HEADER]:
27
+ LEGACY_DURABLE_INVOCATION_FENCE_ATTESTATION_VALUE,
28
+ [DURABLE_INVOCATION_FENCE_ATTESTATION_HEADER]:
29
+ DURABLE_INVOCATION_FENCE_ATTESTATION_VALUE,
30
+ };
31
+ }
32
+
33
+ export function durableInvocationFenceExecutePath(version: 1 | 2): string {
34
+ return `execute-fenced-v${version}`;
35
+ }
@@ -1,6 +1,7 @@
1
1
  import { createSecretRedactionContext } from './secret-redaction';
2
2
  import {
3
3
  assertRuntimeReceiptOutputWithinLimit,
4
+ inspectConvexLedgerPayload,
4
5
  jsonByteLengthUpTo,
5
6
  LEDGER_TERMINAL_RESULT_MAX_BYTES,
6
7
  } from './output-size-limits';
@@ -31,6 +32,31 @@ export function sanitizeLedgerPayload(value: unknown): unknown {
31
32
  return ledgerIngressRedactor.redact(value);
32
33
  }
33
34
 
35
+ function opaqueLedgerPayloadSummary(
36
+ issue: NonNullable<ReturnType<typeof inspectConvexLedgerPayload>>,
37
+ ): Record<string, unknown> {
38
+ return {
39
+ __kind: 'deepline.opaque_ledger_payload_summary.v1',
40
+ reason: issue.reason,
41
+ ...(typeof issue.bytes === 'number' ? { bytes: issue.bytes } : {}),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Preserve unfamiliar event payloads when they fit the Convex event contract.
47
+ * If they do not, keep a safe diagnostic summary instead of allowing one newer
48
+ * producer to permanently wedge its ordered ledger outbox behind a document
49
+ * limit, depth, or field-name rejection.
50
+ */
51
+ export function sanitizeOpaqueLedgerPayload(value: unknown): unknown {
52
+ const sourceIssue = inspectConvexLedgerPayload(value);
53
+ if (sourceIssue) return opaqueLedgerPayloadSummary(sourceIssue);
54
+
55
+ const redacted = ledgerIngressRedactor.redact(value);
56
+ const redactedIssue = inspectConvexLedgerPayload(redacted);
57
+ return redactedIssue ? opaqueLedgerPayloadSummary(redactedIssue) : redacted;
58
+ }
59
+
34
60
  /** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
35
61
  export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
36
62
  // Reject an obviously oversized source before doing redaction work, then
@@ -173,6 +173,12 @@ type PlayRunLedgerBaseEvent = {
173
173
  seq?: number;
174
174
  occurredAt: number;
175
175
  source: PlayRunLedgerEventSource;
176
+ /**
177
+ * Producer-owned detail. The ledger preserves this without assigning it
178
+ * snapshot meaning; a known event's projection reads only the fields its
179
+ * reducer understands.
180
+ */
181
+ payload?: unknown;
176
182
  };
177
183
 
178
184
  export type PlayRunLedgerEvent =
@@ -289,6 +295,55 @@ export type PlayRunLedgerEvent =
289
295
  observation: PlayActivityObservation;
290
296
  });
291
297
 
298
+ /**
299
+ * Wire shape accepted by the Run Ledger ingress.
300
+ *
301
+ * Producers can evolve their event vocabulary independently from the current
302
+ * snapshot reducer. The only producer-owned identity is `key` (or the legacy
303
+ * `type` spelling); `payload` and all additional fields are open. The receiver
304
+ * authenticates the delivery and assigns trusted run/time/source fields before
305
+ * an unfamiliar key becomes {@link PlayRunLedgerOpaqueEvent} in storage.
306
+ * Keep the known {@link PlayRunLedgerEvent} union closed so reducer authors
307
+ * retain exhaustive type checking for the semantics they actually own.
308
+ */
309
+ export type PlayRunLedgerIngressEvent = {
310
+ /** Preferred generic event identity. */
311
+ key?: string;
312
+ /** Backward-compatible event identity used by existing runners. */
313
+ type?: string;
314
+ /** Optional redundant target; a supplied value must match the append target. */
315
+ runId?: string;
316
+ /** Optional producer timestamp; the receiver supplies one when omitted. */
317
+ occurredAt?: number;
318
+ /** Advisory producer label; unknown labels normalize to `system`. */
319
+ source?: PlayRunLedgerEventSource | string;
320
+ /** Arbitrary producer-owned detail. */
321
+ payload?: unknown;
322
+ [field: string]: unknown;
323
+ };
324
+
325
+ /**
326
+ * A well-formed event whose type is not understood by this ledger version.
327
+ * The original type and its redacted, bounded payload are durable history;
328
+ * projection deliberately treats this as a strict no-op.
329
+ */
330
+ export type PlayRunLedgerOpaqueEvent = PlayRunLedgerBaseEvent & {
331
+ type: string;
332
+ opaque: true;
333
+ payload: unknown;
334
+ };
335
+
336
+ /** A stored event accepted by this ledger version. */
337
+ export type AcceptedPlayRunLedgerEvent =
338
+ | PlayRunLedgerEvent
339
+ | PlayRunLedgerOpaqueEvent;
340
+
341
+ export function isPlayRunLedgerOpaqueEvent(
342
+ event: AcceptedPlayRunLedgerEvent,
343
+ ): event is PlayRunLedgerOpaqueEvent {
344
+ return 'opaque' in event && event.opaque === true;
345
+ }
346
+
292
347
  export type PlayRunLedgerStatusPatch = {
293
348
  runId: string;
294
349
  playName?: string | null;
@@ -1102,8 +1157,14 @@ function refineTerminalSyntheticStepStart(
1102
1157
 
1103
1158
  export function reducePlayRunLedgerEvent(
1104
1159
  snapshot: PlayRunLedgerSnapshot,
1105
- event: PlayRunLedgerEvent,
1160
+ event: AcceptedPlayRunLedgerEvent,
1106
1161
  ): PlayRunLedgerSnapshot {
1162
+ // Storage compatibility is intentionally wider than reducer semantics. An
1163
+ // unknown event is durable evidence for a newer producer, not permission to
1164
+ // guess at run state or to advance any snapshot timestamp.
1165
+ if (isPlayRunLedgerOpaqueEvent(event)) {
1166
+ return snapshot;
1167
+ }
1107
1168
  if (event.runId !== snapshot.runId) {
1108
1169
  return snapshot;
1109
1170
  }
@@ -1525,12 +1586,18 @@ export function reducePlayRunLedgerEvent(
1525
1586
  : event.observation.activityId,
1526
1587
  });
1527
1588
  }
1589
+ default:
1590
+ // `appendPlayRunEventsInternal` has historical callers that predate the
1591
+ // opaque ingress envelope. Keep the reducer fail-closed even if one
1592
+ // reaches storage with a newer raw type: it is durable history, never a
1593
+ // reason to manufacture snapshot state or return `undefined`.
1594
+ return snapshot;
1528
1595
  }
1529
1596
  }
1530
1597
 
1531
1598
  export function reducePlayRunLedgerEvents(
1532
1599
  snapshot: PlayRunLedgerSnapshot,
1533
- events: readonly PlayRunLedgerEvent[],
1600
+ events: readonly AcceptedPlayRunLedgerEvent[],
1534
1601
  ): PlayRunLedgerSnapshot {
1535
1602
  return events.reduce(reducePlayRunLedgerEvent, snapshot);
1536
1603
  }
@@ -5,10 +5,6 @@ import {
5
5
  RUNNER_TERMINAL_CHECKPOINT_DISPOSITION,
6
6
  type PlayRunnerExecutionConfig,
7
7
  } from '../../protocol';
8
- import {
9
- PLAY_RUNTIME_CONTRACT,
10
- PLAY_RUNTIME_CONTRACT_HEADER,
11
- } from '../../runtime-contract';
12
8
  import {
13
9
  PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
14
10
  PLAY_RUNNER_STARTUP_GRACE_SECONDS,
@@ -380,7 +376,6 @@ async function main() {
380
376
  headers: {
381
377
  'content-type': 'application/json',
382
378
  authorization: 'Bearer ' + token,
383
- '${PLAY_RUNTIME_CONTRACT_HEADER}': '${String(PLAY_RUNTIME_CONTRACT)}',
384
379
  ...(preferredGatewayMachineId
385
380
  ? { 'fly-force-instance-id': preferredGatewayMachineId }
386
381
  : {}),
@@ -97,8 +97,8 @@ import {
97
97
  } from './map-row-outcome';
98
98
  import { DEEPLINE_CELL_META_FIELD } from '../plays/cell-staleness';
99
99
  import {
100
- PLAY_RUNTIME_CONTRACT,
101
- PLAY_RUNTIME_CONTRACT_HEADER,
100
+ LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT,
101
+ LEGACY_RUNTIME_RECEIPT_WIRE_HEADER,
102
102
  } from './runtime-contract';
103
103
  import { PLAY_RUNTIME_API_COMPAT_PATH } from './runtime-api-paths';
104
104
  import {
@@ -615,7 +615,9 @@ function resolveRuntimeApiHeaders(
615
615
  return {
616
616
  'content-type': 'application/json',
617
617
  authorization: `Bearer ${token}`,
618
- [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
618
+ [LEGACY_RUNTIME_RECEIPT_WIRE_HEADER]: String(
619
+ LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT,
620
+ ),
619
621
  // A run has many callback HTTP attempts. Preserve the logical run
620
622
  // correlation in headers while Vercel keeps its own per-attempt request id.
621
623
  ...(context.runId?.trim()
@@ -1,6 +1,16 @@
1
- export const PLAY_RUNTIME_CONTRACT = 6;
2
- export const PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED = 1;
3
- export const PLAY_RUNTIME_CONTRACT_HEADER = 'x-deepline-runtime-contract';
1
+ /**
2
+ * Kept on the old header name so sleeping runners can decode historical
3
+ * receipt presentations. New operations must not use this as a version gate.
4
+ */
5
+ export const LEGACY_RUNTIME_RECEIPT_WIRE_HEADER = 'x-deepline-runtime-contract';
6
+ export const LEGACY_RUNTIME_RECEIPT_WIRE_FORMAT = 3 as const;
7
+ /**
8
+ * Historical workers validate this readiness range before they claim work.
9
+ * It is an output-only compatibility presentation; current callers use named
10
+ * operation capabilities and do not treat it as a global admission contract.
11
+ */
12
+ export const LEGACY_RUNTIME_READINESS_MIN_SUPPORTED = 1 as const;
13
+ export const LEGACY_RUNTIME_READINESS_CURRENT = 6 as const;
4
14
  /** Shared runner/gateway envelope limit for unacknowledged progress recovery. */
5
15
  export const RUNTIME_RUNNER_TERMINAL_PROGRESS_MAX_EVENTS = 2_000;
6
16
  /**
@@ -26,78 +36,26 @@ export function resolvePlayRuntimeTransportAttemptId(
26
36
  return trimmed && RUNTIME_TRANSPORT_ATTEMPT_ID.test(trimmed) ? trimmed : null;
27
37
  }
28
38
 
29
- export const RUNTIME_CONTRACT_CHANGELOG = [
30
- {
31
- contract: 6,
32
- changed:
33
- 'Durable invocation metadata identifies the semantic provider operation and is admitted only by a gateway that declares contract 6 support.',
34
- compatOwner: 'runtime',
35
- compatRemoval: 'contract 5 runtime lane drain',
36
- },
37
- {
38
- contract: 5,
39
- changed:
40
- 'Runtime Sheet completion and lazy launch lookup use bounded, cancellation-aware gateway operations.',
41
- compatOwner: 'runtime',
42
- compatRemoval: 'contract 4 runtime lane drain',
43
- },
44
- {
45
- contract: 4,
46
- changed:
47
- 'Runtime receipt heartbeat requests can carry one positional lease ID per receipt key.',
48
- compatOwner: 'runtime',
49
- compatRemoval: 'contract 3 runtime lane drain',
50
- },
51
- {
52
- contract: 3,
53
- changed:
54
- 'Runtime receipt completion responses are compact acknowledgements; receipt reads remain the payload replay path.',
55
- compatOwner: 'runtime',
56
- compatRemoval: 'contract 2 support window closure',
57
- },
58
- {
59
- contract: 2,
60
- changed:
61
- 'Runtime receipt batch responses use positional nulls and v2 lease-aware receipt semantics.',
62
- compatOwner: 'runtime',
63
- compatRemoval: 'ledger cutover milestone M1',
64
- },
65
- ] as const;
39
+ /**
40
+ * The pre-operation-runtime header is retained only to decode historical
41
+ * receipt response shapes. It never admits or rejects a runtime request.
42
+ */
43
+ export type LegacyRuntimeReceiptWireFormat = 1 | 2 | 3;
44
+
45
+ export function resolveLegacyRuntimeReceiptWireFormat(
46
+ rawContract: string | null | undefined,
47
+ ): LegacyRuntimeReceiptWireFormat {
48
+ const requested = Number(rawContract?.trim());
49
+ if (!Number.isSafeInteger(requested) || requested <= 1) return 1;
50
+ if (requested === 2) return 2;
51
+ return 3;
52
+ }
66
53
 
67
54
  export function runtimeReceiptCompletionResponse<
68
55
  T extends { output?: unknown },
69
- >(runtimeContract: number | null | undefined, receipt: T | null): T | null {
70
- if (!receipt || (runtimeContract ?? 1) < 3) return receipt;
56
+ >(wireFormat: LegacyRuntimeReceiptWireFormat, receipt: T | null): T | null {
57
+ if (!receipt || wireFormat < 3) return receipt;
71
58
  const { output, ...ack } = receipt;
72
59
  void output;
73
60
  return ack as T;
74
61
  }
75
-
76
- export type RuntimeContractResolution =
77
- | { ok: true; contract: number }
78
- | {
79
- ok: false;
80
- requestedContract: number;
81
- currentContract: number;
82
- minSupportedContract: number;
83
- };
84
-
85
- export function resolvePlayRuntimeContract(
86
- rawContract: string | null | undefined,
87
- ): RuntimeContractResolution {
88
- const trimmed = rawContract?.trim();
89
- const requestedContract = trimmed ? Number(trimmed) : 1;
90
- if (
91
- !Number.isInteger(requestedContract) ||
92
- requestedContract < PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED ||
93
- requestedContract > PLAY_RUNTIME_CONTRACT
94
- ) {
95
- return {
96
- ok: false,
97
- requestedContract,
98
- currentContract: PLAY_RUNTIME_CONTRACT,
99
- minSupportedContract: PLAY_RUNTIME_CONTRACT_MIN_SUPPORTED,
100
- };
101
- }
102
- return { ok: true, contract: requestedContract };
103
- }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A named runtime operation is its complete public wire contract. Runtime
3
+ * deployment builds are deliberately not part of this type: a receiver must
4
+ * either understand the named operation or reject that operation before it
5
+ * performs an effect.
6
+ */
7
+ export type RuntimeOperation<
8
+ Name extends string,
9
+ Request,
10
+ Response,
11
+ > = Readonly<{
12
+ name: Name;
13
+ request: Request;
14
+ response: Response;
15
+ }>;
16
+
17
+ export function defineRuntimeOperation<
18
+ const Name extends string,
19
+ Request,
20
+ Response,
21
+ >(
22
+ input: RuntimeOperation<Name, Request, Response>,
23
+ ): RuntimeOperation<Name, Request, Response> {
24
+ return input;
25
+ }
26
+
27
+ /**
28
+ * Author a compatible evolution of one operation.
29
+ *
30
+ * The next request must accept every old request and the next response must
31
+ * remain consumable by every old caller. A breaking wire meaning therefore
32
+ * cannot be hidden behind a global deployment number: it needs a new named
33
+ * operation instead.
34
+ */
35
+ export function evolveRuntimeOperation<
36
+ const Name extends string,
37
+ PreviousRequest,
38
+ PreviousResponse,
39
+ NextRequest,
40
+ NextResponse,
41
+ >(
42
+ previous: RuntimeOperation<Name, PreviousRequest, PreviousResponse>,
43
+ next: PreviousRequest extends NextRequest
44
+ ? NextResponse extends PreviousResponse
45
+ ? RuntimeOperation<Name, NextRequest, NextResponse>
46
+ : never
47
+ : never,
48
+ ): RuntimeOperation<Name, NextRequest, NextResponse> {
49
+ void previous;
50
+ return next;
51
+ }
package/dist/cli/index.js CHANGED
@@ -1225,7 +1225,7 @@ var SDK_RELEASE = {
1225
1225
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1226
1226
  // getters keep their established compatibility behavior.
1227
1227
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1228
- version: "0.3.81",
1228
+ version: "0.3.83",
1229
1229
  updateSummary: "Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.",
1230
1230
  packageCapabilities: {
1231
1231
  updatePreferences: 1
@@ -1210,7 +1210,7 @@ var SDK_RELEASE = {
1210
1210
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1211
1211
  // getters keep their established compatibility behavior.
1212
1212
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1213
- version: "0.3.81",
1213
+ version: "0.3.83",
1214
1214
  updateSummary: "Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.",
1215
1215
  packageCapabilities: {
1216
1216
  updatePreferences: 1
package/dist/index.js CHANGED
@@ -832,7 +832,7 @@ var SDK_RELEASE = {
832
832
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
833
833
  // getters keep their established compatibility behavior.
834
834
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
835
- version: "0.3.81",
835
+ version: "0.3.83",
836
836
  updateSummary: "Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.",
837
837
  packageCapabilities: {
838
838
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -744,7 +744,7 @@ var SDK_RELEASE = {
744
744
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
745
745
  // getters keep their established compatibility behavior.
746
746
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
747
- version: "0.3.81",
747
+ version: "0.3.83",
748
748
  updateSummary: "Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.",
749
749
  packageCapabilities: {
750
750
  updatePreferences: 1
@@ -69,6 +69,7 @@
69
69
  "dist/bundling-sources/shared_libs/play-runtime/durability-store.ts",
70
70
  "dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts",
71
71
  "dist/bundling-sources/shared_libs/play-runtime/durable-call-policy.ts",
72
+ "dist/bundling-sources/shared_libs/play-runtime/durable-invocation-fence.ts",
72
73
  "dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts",
73
74
  "dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts",
74
75
  "dist/bundling-sources/shared_libs/play-runtime/email-status.ts",
@@ -160,6 +161,7 @@
160
161
  "dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts",
161
162
  "dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts",
162
163
  "dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts",
164
+ "dist/bundling-sources/shared_libs/play-runtime/runtime-operation.ts",
163
165
  "dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver-pg.ts",
164
166
  "dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver.ts",
165
167
  "dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.81",
3
+ "version": "0.3.83",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",