deepline 0.3.247 → 0.3.248

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 (65) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +177 -13
  2. package/dist/bundling-sources/sdk/src/index.ts +8 -0
  3. package/dist/bundling-sources/sdk/src/plays/input-example.ts +40 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/runs/overview.ts +457 -0
  6. package/dist/bundling-sources/sdk/src/runs/receipts.ts +23 -0
  7. package/dist/bundling-sources/sdk/src/tool-calls.ts +21 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +16 -1
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +19 -1
  10. package/dist/bundling-sources/shared_libs/play-data-plane/column-names.ts +17 -3
  11. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +40 -8
  12. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +26 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +142 -14
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +6 -3
  16. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +26 -21
  17. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +23 -21
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +17 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +105 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +8 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +40 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +50 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +3 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +28 -1
  26. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +14 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +56 -16
  28. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +184 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +327 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +87 -30
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +24 -13
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +23 -6
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +76 -33
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +90 -35
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +16 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +62 -21
  37. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +368 -66
  38. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +22 -4
  39. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +105 -121
  40. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +1347 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +16 -19
  42. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +14 -6
  43. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +16 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +40 -0
  45. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +5 -2
  46. package/dist/bundling-sources/shared_libs/plays/dataset-summary.ts +11 -7
  47. package/dist/bundling-sources/shared_libs/plays/static-pipeline-storage-codec.ts +5 -2
  48. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +11 -2
  49. package/dist/cli/index.js +3504 -2368
  50. package/dist/cli/index.mjs +3188 -2052
  51. package/dist/{compiler-manifest-n8ZtPv2h.d.mts → compiler-manifest-DavmLa8y.d.mts} +1 -0
  52. package/dist/{compiler-manifest-n8ZtPv2h.d.ts → compiler-manifest-DavmLa8y.d.ts} +1 -0
  53. package/dist/index.d.mts +97 -5
  54. package/dist/index.d.ts +97 -5
  55. package/dist/index.js +135 -15
  56. package/dist/index.mjs +135 -15
  57. package/dist/install-integrity.json +9 -2
  58. package/dist/plays/bundle-play-file.d.mts +2 -2
  59. package/dist/plays/bundle-play-file.d.ts +2 -2
  60. package/dist/plays/bundle-play-file.mjs +1 -1
  61. package/dist/release.d.mts +1 -1
  62. package/dist/release.d.ts +1 -1
  63. package/dist/release.js +1 -1
  64. package/dist/release.mjs +1 -1
  65. package/package.json +1 -1
@@ -235,6 +235,10 @@ export type FlyMachine = {
235
235
  init?: {
236
236
  exec?: string[];
237
237
  };
238
+ stop_config?: {
239
+ signal?: string;
240
+ timeout?: string;
241
+ };
238
242
  };
239
243
  checks?: FlyMachineCheck[];
240
244
  };
@@ -398,6 +402,12 @@ export function buildBootMachineConfig(input: {
398
402
  init: {
399
403
  exec: [...ABSURD_RUNTIME_WORKER_EXEC],
400
404
  },
405
+ // Direct Machines API creation does not load the worker fly.toml.
406
+ // Fly encodes this timeout as a duration string, unlike TOML seconds.
407
+ stop_config: {
408
+ signal: DEFAULT_LANE_REPLACEMENT_STOP_SIGNAL,
409
+ timeout: DEFAULT_LANE_REPLACEMENT_STOP_TIMEOUT,
410
+ },
401
411
  restart: {
402
412
  policy: 'on-failure',
403
413
  max_retries: 3,
@@ -534,6 +544,13 @@ export function buildWorkerSlotCapUpdateConfig(machine: FlyMachine): {
534
544
  ...(existing.init ?? {}),
535
545
  exec: [...ABSURD_RUNTIME_WORKER_EXEC],
536
546
  },
547
+ // Config updates replace the complete Machine config. Apply the same
548
+ // stop policy to reused historical workers while preserving other fields.
549
+ stop_config: {
550
+ ...(existing.stop_config ?? {}),
551
+ signal: DEFAULT_LANE_REPLACEMENT_STOP_SIGNAL,
552
+ timeout: DEFAULT_LANE_REPLACEMENT_STOP_TIMEOUT,
553
+ },
537
554
  },
538
555
  };
539
556
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Small, flat log context values copied into the org-scoped event search
3
+ * document. Keep this contract stable across the runner and gateway packages.
4
+ */
5
+ export type PlayLogContextValue = string | number | boolean;
6
+ export type PlayLogContext = Readonly<Record<string, PlayLogContextValue>>;
7
+
8
+ export const PLAY_LOG_CONTEXT_LIMITS = {
9
+ fields: 8,
10
+ keyBytes: 64,
11
+ stringValueBytes: 256,
12
+ totalBytes: 2_048,
13
+ } as const;
14
+
15
+ const CONTEXT_KEY = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
16
+ const FORBIDDEN_CONTEXT_KEYS = new Set([
17
+ '__proto__',
18
+ 'constructor',
19
+ 'prototype',
20
+ ]);
21
+ const utf8 = new TextEncoder();
22
+
23
+ /** Validate and return a detached, key-sorted JSON-safe context object. */
24
+ export function normalizePlayLogContext(value: unknown): PlayLogContext | null {
25
+ if (value === undefined || value === null) return null;
26
+ if (
27
+ !value ||
28
+ typeof value !== 'object' ||
29
+ Array.isArray(value) ||
30
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))
31
+ ) {
32
+ throw new TypeError('ctx.log context must be a flat key/value object.');
33
+ }
34
+
35
+ const ownKeys = Reflect.ownKeys(value);
36
+ if (ownKeys.some((key) => typeof key !== 'string')) {
37
+ throw new TypeError('ctx.log context keys must be strings.');
38
+ }
39
+ const entries: Array<[string, unknown]> = [];
40
+ for (const key of ownKeys as string[]) {
41
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
42
+ if (
43
+ !descriptor?.enumerable ||
44
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value')
45
+ ) {
46
+ throw new TypeError(
47
+ 'ctx.log context fields must be enumerable data properties.',
48
+ );
49
+ }
50
+ entries.push([key, descriptor.value]);
51
+ }
52
+ if (entries.length > PLAY_LOG_CONTEXT_LIMITS.fields) {
53
+ throw new TypeError(
54
+ `ctx.log context supports at most ${PLAY_LOG_CONTEXT_LIMITS.fields} fields.`,
55
+ );
56
+ }
57
+
58
+ const normalized: Record<string, PlayLogContextValue> = {};
59
+ for (const [key, entry] of entries.sort(([left], [right]) =>
60
+ left < right ? -1 : left > right ? 1 : 0,
61
+ )) {
62
+ if (
63
+ !CONTEXT_KEY.test(key) ||
64
+ utf8.encode(key).byteLength > PLAY_LOG_CONTEXT_LIMITS.keyBytes ||
65
+ FORBIDDEN_CONTEXT_KEYS.has(key)
66
+ ) {
67
+ throw new TypeError(
68
+ `ctx.log context key ${JSON.stringify(key)} is not supported.`,
69
+ );
70
+ }
71
+ if (typeof entry === 'string') {
72
+ if (
73
+ utf8.encode(entry).byteLength > PLAY_LOG_CONTEXT_LIMITS.stringValueBytes
74
+ ) {
75
+ throw new TypeError(
76
+ `ctx.log context value for ${key} exceeds ${PLAY_LOG_CONTEXT_LIMITS.stringValueBytes} UTF-8 bytes.`,
77
+ );
78
+ }
79
+ normalized[key] = entry;
80
+ continue;
81
+ }
82
+ if (typeof entry === 'boolean') {
83
+ normalized[key] = entry;
84
+ continue;
85
+ }
86
+ if (typeof entry === 'number' && Number.isFinite(entry)) {
87
+ normalized[key] = entry;
88
+ continue;
89
+ }
90
+ throw new TypeError(
91
+ `ctx.log context value for ${key} must be a string, finite number, or boolean.`,
92
+ );
93
+ }
94
+
95
+ if (entries.length === 0) return null;
96
+ if (
97
+ utf8.encode(JSON.stringify(normalized)).byteLength >
98
+ PLAY_LOG_CONTEXT_LIMITS.totalBytes
99
+ ) {
100
+ throw new TypeError(
101
+ `ctx.log context exceeds ${PLAY_LOG_CONTEXT_LIMITS.totalBytes} UTF-8 bytes.`,
102
+ );
103
+ }
104
+ return normalized;
105
+ }
@@ -491,7 +491,14 @@ export function buildRunDatasetPreview(
491
491
  const previewRows: unknown[] = [];
492
492
  let truncated = rows.length > RUN_RESPONSE_PREVIEW_LIMITS.maxRows;
493
493
  for (const row of rows.slice(0, RUN_RESPONSE_PREVIEW_LIMITS.maxRows)) {
494
- const compacted = compactRunOutputPreviewValue(row, 0);
494
+ // Debug columns remain persisted/exportable; exclude them before budgeting.
495
+ const visibleRow =
496
+ row && typeof row === 'object' && !Array.isArray(row)
497
+ ? Object.fromEntries(
498
+ Object.entries(row).filter(([name]) => !name.startsWith('_')),
499
+ )
500
+ : row;
501
+ const compacted = compactRunOutputPreviewValue(visibleRow, 0);
495
502
  truncated ||= compacted.truncated;
496
503
  const nextRows = [...previewRows, compacted.value];
497
504
  if (jsonSize(nextRows) > RUN_RESPONSE_PREVIEW_LIMITS.maxBytes) {
@@ -14,6 +14,7 @@ import type {
14
14
  import type { PlayExecutionSuspension } from './suspension';
15
15
  import type { PlayStaticPipeline } from '../plays/static-pipeline';
16
16
  import type { PlayLiveEventSource } from './live-events';
17
+ import type { PlayLogContext } from './log-context';
17
18
  import type { PreloadedRuntimeDbSession } from './db-session';
18
19
  import type { PacingRule } from './governor/rate-state-backend';
19
20
  import type { GovernanceSnapshot } from './governor/governor';
@@ -319,6 +320,8 @@ export type PlayRunnerLogEvent = {
319
320
  at: string;
320
321
  source: PlayLiveEventSource;
321
322
  line: string;
323
+ /** Search-only context; omitted before the compatible Convex projection. */
324
+ context?: PlayLogContext;
322
325
  };
323
326
 
324
327
  export type PlayRunnerEvent =
@@ -408,6 +411,8 @@ export type PlayRunnerResult =
408
411
  outputRowCount?: number;
409
412
  rowOutcomes?: PlayRunnerRowOutcomeSummary;
410
413
  logs: string[];
414
+ /** Search-only context aligned to logs; omitted when every entry is null. */
415
+ logContexts?: Array<PlayLogContext | null>;
411
416
  stats: Record<string, unknown>;
412
417
  steps: PlayStep[];
413
418
  checkpoint: PlayCheckpoint;
@@ -421,6 +426,8 @@ export type PlayRunnerResult =
421
426
  status: 'suspended';
422
427
  suspension: PlayExecutionSuspension;
423
428
  logs: string[];
429
+ /** Search-only context aligned to logs; omitted when every entry is null. */
430
+ logContexts?: Array<PlayLogContext | null>;
424
431
  stats: Record<string, unknown>;
425
432
  steps: PlayStep[];
426
433
  checkpoint: PlayCheckpoint;
@@ -435,6 +442,8 @@ export type PlayRunnerResult =
435
442
  error: string;
436
443
  errors?: PlayRunFailureDetails[];
437
444
  logs: string[];
445
+ /** Search-only context aligned to logs; omitted when every entry is null. */
446
+ logContexts?: Array<PlayLogContext | null>;
438
447
  stats: Record<string, unknown>;
439
448
  steps: PlayStep[];
440
449
  checkpoint?: PlayCheckpoint | null;
@@ -1,4 +1,5 @@
1
1
  import type { PlayRunLedgerIngressEvent } from './run-ledger';
2
+ import type { PlayLogContext } from './log-context';
2
3
  import type { RunLedgerProjectionChunk } from './run-ledger-projection-contract';
3
4
 
4
5
  const APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES = 128 * 1024;
@@ -26,6 +27,25 @@ function splitRunEventForAppRuntime(
26
27
  ? rawLines.filter((line): line is string => typeof line === 'string')
27
28
  : [];
28
29
  if (eventType !== 'log.appended' || lines.length === 0) return [event];
30
+ const payloadOwnsLineContexts =
31
+ payload !== null && Object.hasOwn(payload, 'lineContexts');
32
+ const eventOwnsLineContexts = Object.hasOwn(event, 'lineContexts');
33
+ if (payloadOwnsLineContexts && eventOwnsLineContexts) {
34
+ throw new TypeError('Run log contexts must have one event owner.');
35
+ }
36
+ const rawLineContexts = payloadOwnsLineContexts
37
+ ? payload?.lineContexts
38
+ : event.lineContexts;
39
+ const hasLineContexts = payloadOwnsLineContexts || eventOwnsLineContexts;
40
+ if (
41
+ hasLineContexts &&
42
+ (!Array.isArray(rawLineContexts) || rawLineContexts.length !== lines.length)
43
+ ) {
44
+ throw new TypeError('Run log contexts must align with their lines.');
45
+ }
46
+ const lineContexts = hasLineContexts
47
+ ? (rawLineContexts as Array<PlayLogContext | null>)
48
+ : null;
29
49
  const payloadChannelOffset =
30
50
  typeof payload?.channelOffset === 'number'
31
51
  ? payload.channelOffset
@@ -33,7 +53,11 @@ function splitRunEventForAppRuntime(
33
53
  const eventChannelOffset =
34
54
  typeof event.channelOffset === 'number' ? event.channelOffset : undefined;
35
55
  const split: PlayRunLedgerIngressEvent[] = [];
36
- const { lines: _outerLines, ...eventWithoutOuterLines } = event;
56
+ const {
57
+ lines: _outerLines,
58
+ lineContexts: _outerLineContexts,
59
+ ...eventWithoutOuterLines
60
+ } = event;
37
61
  let index = 0;
38
62
  while (index < lines.length) {
39
63
  const start = index;
@@ -52,14 +76,25 @@ function splitRunEventForAppRuntime(
52
76
  index += 1;
53
77
  }
54
78
  const batchLines = lines.slice(start, index);
79
+ const batchLineContexts = lineContexts?.slice(start, index);
55
80
  split.push(
56
81
  payload
57
82
  ? {
58
83
  ...eventWithoutOuterLines,
84
+ ...(eventOwnsLineContexts && batchLineContexts
85
+ ? { lineContexts: batchLineContexts }
86
+ : {}),
59
87
  ...(payloadOwnsLines ? {} : { lines: batchLines }),
60
88
  payload: {
61
- ...payload,
89
+ ...Object.fromEntries(
90
+ Object.entries(payload).filter(
91
+ ([key]) => key !== 'lineContexts',
92
+ ),
93
+ ),
62
94
  ...(payloadOwnsLines ? { lines: batchLines } : {}),
95
+ ...(payloadOwnsLineContexts && batchLineContexts
96
+ ? { lineContexts: batchLineContexts }
97
+ : {}),
63
98
  ...(payloadChannelOffset === undefined
64
99
  ? {}
65
100
  : { channelOffset: payloadChannelOffset + start }),
@@ -69,8 +104,9 @@ function splitRunEventForAppRuntime(
69
104
  : { channelOffset: eventChannelOffset + start }),
70
105
  }
71
106
  : {
72
- ...event,
107
+ ...eventWithoutOuterLines,
73
108
  lines: batchLines,
109
+ ...(batchLineContexts ? { lineContexts: batchLineContexts } : {}),
74
110
  ...(eventChannelOffset === undefined
75
111
  ? {}
76
112
  : { channelOffset: eventChannelOffset + start }),
@@ -81,7 +117,7 @@ function splitRunEventForAppRuntime(
81
117
  }
82
118
 
83
119
  /**
84
- * Reproduce the exact event and byte boundaries used by the App Runtime API.
120
+ * Reproduce the exact event, KV and byte boundaries used by the App Runtime API.
85
121
  * The outbox lease verifier uses this same function to validate each chunk
86
122
  * against its canonical event prefix before accepting a projection.
87
123
  */
@@ -21,6 +21,7 @@ import {
21
21
  type PlayDatasetBornFrom,
22
22
  } from './cell-provenance';
23
23
  import { isLedgerTerminalResultRef } from './output-size-limits';
24
+ import type { PlayLogContext } from './log-context';
24
25
 
25
26
  export type PlayRunLedgerStatus =
26
27
  | 'queued'
@@ -294,6 +295,8 @@ export type PlayRunLedgerEvent =
294
295
  | (PlayRunLedgerBaseEvent & {
295
296
  type: 'log.appended';
296
297
  lines: string[];
298
+ /** Search-only context aligned to `lines`; stripped before Convex. */
299
+ lineContexts?: Array<PlayLogContext | null>;
297
300
  /**
298
301
  * Absolute per-run sequence (1-based) of `lines[0]`. Assigned by Run
299
302
  * Log Stream ingestion in Convex only; producers never set it.
@@ -402,6 +405,8 @@ export type PlayRunLedgerStatusPatch = {
402
405
  runtimeBackend?: string | null;
403
406
  lastCheckpointAt?: number | null;
404
407
  liveLogs?: readonly string[] | null;
408
+ /** Search-only context aligned to `liveLogs`; stripped before Convex. */
409
+ liveLogContexts?: readonly (PlayLogContext | null)[] | null;
405
410
  /**
406
411
  * Positional cursor for `liveLogs[0]` on this producer's log channel (the
407
412
  * count of lines emitted before it). Computed by the producer-side cursor
@@ -2064,9 +2069,20 @@ function terminalEventTypeForStatus(
2064
2069
  */
2065
2070
  export function slicePositionalLogLines(input: {
2066
2071
  bufferLines: readonly string[];
2072
+ bufferContexts?: readonly (PlayLogContext | null)[];
2067
2073
  bufferTotalCount: number;
2068
2074
  sentCount: number;
2069
- }): { lines: string[]; channelOffset: number } | null {
2075
+ }): {
2076
+ lines: string[];
2077
+ channelOffset: number;
2078
+ lineContexts?: Array<PlayLogContext | null>;
2079
+ } | null {
2080
+ if (
2081
+ input.bufferContexts !== undefined &&
2082
+ input.bufferContexts.length !== input.bufferLines.length
2083
+ ) {
2084
+ throw new TypeError('Run log contexts must align with the retained lines.');
2085
+ }
2070
2086
  const total = Math.max(input.bufferTotalCount, input.bufferLines.length);
2071
2087
  const sent = Math.max(0, input.sentCount);
2072
2088
  if (total <= sent) {
@@ -2078,7 +2094,13 @@ export function slicePositionalLogLines(input: {
2078
2094
  if (lines.length === 0) {
2079
2095
  return null;
2080
2096
  }
2081
- return { lines: [...lines], channelOffset: sendFromOffset };
2097
+ const start = sendFromOffset - bufferFirstOffset;
2098
+ const lineContexts = input.bufferContexts?.slice(start);
2099
+ return {
2100
+ lines: [...lines],
2101
+ channelOffset: sendFromOffset,
2102
+ ...(lineContexts?.some(Boolean) ? { lineContexts: [...lineContexts] } : {}),
2103
+ };
2082
2104
  }
2083
2105
 
2084
2106
  function terminalLogReplayChannelOffset(input: {
@@ -2105,6 +2127,7 @@ function terminalLogReplayChannelOffset(input: {
2105
2127
  export function buildPlayRunLedgerEventsFromLogLines(input: {
2106
2128
  runId: string;
2107
2129
  lines: readonly string[];
2130
+ lineContexts?: readonly (PlayLogContext | null)[];
2108
2131
  source?: PlayRunLedgerEventSource;
2109
2132
  occurredAt?: number;
2110
2133
  channelOffset?: number | null;
@@ -2120,9 +2143,19 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2120
2143
  Number.isFinite(input.channelOffset);
2121
2144
  // Positional batches must keep one entry per emitted line — dropping a
2122
2145
  // blank would shift every later line off its channel position.
2123
- const newLines = positional
2124
- ? input.lines.map((line) => line.trim() || '(blank log line)')
2125
- : input.lines.map((line) => line.trim()).filter((line) => line.length > 0);
2146
+ if (
2147
+ input.lineContexts !== undefined &&
2148
+ input.lineContexts.length !== input.lines.length
2149
+ ) {
2150
+ throw new TypeError('Run log contexts must align with their lines.');
2151
+ }
2152
+ const entries = input.lines
2153
+ .map((line, index) => ({
2154
+ line: positional ? line.trim() || '(blank log line)' : line.trim(),
2155
+ context: input.lineContexts?.[index] ?? null,
2156
+ }))
2157
+ .filter((entry) => positional || entry.line.length > 0);
2158
+ const newLines = entries.map((entry) => entry.line);
2126
2159
  if (newLines.length === 0 || input.includeLogAppend === false) {
2127
2160
  return [];
2128
2161
  }
@@ -2138,6 +2171,15 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2138
2171
  source,
2139
2172
  occurredAt,
2140
2173
  lines: newLines.slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT),
2174
+ ...(entries
2175
+ .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2176
+ .some((entry) => entry.context)
2177
+ ? {
2178
+ lineContexts: entries
2179
+ .slice(index, index + MAX_LEDGER_LOG_LINES_PER_EVENT)
2180
+ .map((entry) => entry.context),
2181
+ }
2182
+ : {}),
2141
2183
  ...(positional ? { channelOffset: input.channelOffset! + index } : {}),
2142
2184
  ...(positional &&
2143
2185
  typeof input.producerAttempt === 'number' &&
@@ -2165,6 +2207,7 @@ export function buildPlayRunLedgerEventsFromLogLines(input: {
2165
2207
  export function buildTerminalLogReplayEvents(input: {
2166
2208
  runId: string;
2167
2209
  lines: readonly string[];
2210
+ lineContexts?: readonly (PlayLogContext | null)[];
2168
2211
  source?: PlayRunLedgerEventSource;
2169
2212
  occurredAt?: number;
2170
2213
  liveLogTotalCount?: number;
@@ -2178,6 +2221,7 @@ export function buildTerminalLogReplayEvents(input: {
2178
2221
  return buildPlayRunLedgerEventsFromLogLines({
2179
2222
  runId: input.runId,
2180
2223
  lines: input.lines,
2224
+ lineContexts: input.lineContexts,
2181
2225
  source: 'coordinator',
2182
2226
  occurredAt: input.occurredAt,
2183
2227
  channelOffset,
@@ -2218,6 +2262,7 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2218
2262
  ? buildPlayRunLedgerEventsFromLogLines({
2219
2263
  runId: patch.runId,
2220
2264
  lines: patch.liveLogs,
2265
+ lineContexts: patch.liveLogContexts ?? undefined,
2221
2266
  source,
2222
2267
  occurredAt: checkpointAt,
2223
2268
  channelOffset: patch.liveLogsChannelOffset ?? null,
@@ -117,5 +117,7 @@ export function isRecoverableDatasetRowStatus(status: unknown): boolean {
117
117
  export function datasetSummaryRowModeForRunStatus(
118
118
  status: unknown,
119
119
  ): 'output' | 'all' {
120
- return isRecoverableDatasetRowStatus(status) ? 'all' : 'output';
120
+ // Completed is a run lifecycle, not proof that every row succeeded. Terminal
121
+ // summaries must count the same persisted successful AND failed rows as export.
122
+ return isTerminalPlayRunLifecycleStatus(status) ? 'all' : 'output';
121
123
  }
@@ -1,7 +1,12 @@
1
+ import type { PlayLogContext } from './log-context';
2
+ import { normalizePlayLogContext } from './log-context';
3
+
1
4
  /** Limits apply before logs enter runner memory or a transport request. */
2
5
  export const RUNNER_LOG_MAX_CHARS = 2_000;
3
6
  export const RUNNER_LOG_MAX_BYTES = 5 * 1024 * 1024;
4
7
  export const RUNNER_LOG_MAX_LINES = 25_000;
8
+ /** Keep structured metadata from multiplying the per-run text-log budget. */
9
+ export const RUNNER_LOG_CONTEXT_MAX_BYTES = 128 * 1024;
5
10
  const TERMINAL_RESERVE_BYTES = 64 * 1024;
6
11
  const TERMINAL_RESERVE_LINES = 32;
7
12
  const encoder = new TextEncoder();
@@ -20,13 +25,24 @@ export function boundRunLogLine(line: string): string {
20
25
  */
21
26
  export class RunLogBuffer {
22
27
  readonly lines: string[] = [];
28
+ readonly contexts: Array<PlayLogContext | null> = [];
23
29
  private bytes = 0;
30
+ private contextBytes = 0;
31
+ private omittedContexts = 0;
24
32
  private omitted = 0;
25
33
  private marked = false;
26
34
 
27
- append(raw: string, terminal = false): string[] {
35
+ append(
36
+ raw: string,
37
+ terminal = false,
38
+ context?: PlayLogContext | null,
39
+ ): string[] {
28
40
  const line = boundRunLogLine(raw);
29
41
  const bytes = encoder.encode(line).byteLength;
42
+ const normalizedContext = normalizePlayLogContext(context);
43
+ const contextBytes = normalizedContext
44
+ ? encoder.encode(JSON.stringify(normalizedContext)).byteLength
45
+ : 0;
30
46
  const byteLimit =
31
47
  RUNNER_LOG_MAX_BYTES - (terminal ? 0 : TERMINAL_RESERVE_BYTES);
32
48
  const lineLimit =
@@ -41,13 +57,24 @@ export class RunLogBuffer {
41
57
  );
42
58
  }
43
59
  this.lines.push(line);
60
+ const retainContext =
61
+ normalizedContext !== null &&
62
+ this.contextBytes + contextBytes <= RUNNER_LOG_CONTEXT_MAX_BYTES;
63
+ this.contexts.push(retainContext ? normalizedContext : null);
44
64
  this.bytes += bytes;
65
+ if (retainContext) {
66
+ this.contextBytes += contextBytes;
67
+ } else if (normalizedContext !== null) {
68
+ this.omittedContexts += 1;
69
+ }
45
70
  return [line];
46
71
  }
47
72
 
48
73
  snapshot() {
49
74
  return {
50
75
  retainedBytes: this.bytes,
76
+ retainedContextBytes: this.contextBytes,
77
+ omittedContexts: this.omittedContexts,
51
78
  retainedLines: this.lines.length,
52
79
  omittedLines: this.omitted,
53
80
  };
@@ -35,6 +35,7 @@ import {
35
35
  DURABLE_INVOCATION_FENCE_ATTESTATION_VALUE,
36
36
  } from '../durable-invocation-fence';
37
37
  import type { PlayRunInputPayload } from '../play-input';
38
+ import type { PlayLogContext } from '../log-context';
38
39
  import { playRunInputObjectView } from '../play-input';
39
40
  import {
40
41
  normalizePlayRunFailure,
@@ -1856,6 +1857,13 @@ function terminalLogsWithPhaseSummary(ctx: {
1856
1857
  ];
1857
1858
  }
1858
1859
 
1860
+ function terminalLogContextsWithPhaseSummary(ctx: {
1861
+ getLogContexts(): Array<PlayLogContext | null>;
1862
+ }): Array<PlayLogContext | null> | undefined {
1863
+ const contexts = ctx.getLogContexts();
1864
+ return contexts.some(Boolean) ? [...contexts, null, null] : undefined;
1865
+ }
1866
+
1859
1867
  function rowOutcomesFromCheckpoint(checkpoint: {
1860
1868
  mapFrames?: Record<
1861
1869
  string,
@@ -2422,7 +2430,7 @@ async function run(
2422
2430
  });
2423
2431
  }
2424
2432
  : undefined,
2425
- onLog: (line) => {
2433
+ onLog: (line, context) => {
2426
2434
  const timestampMatch = line.match(/^\[([^\]]+)\]/);
2427
2435
  const timestamp = timestampMatch?.[1];
2428
2436
  const at =
@@ -2435,6 +2443,7 @@ async function run(
2435
2443
  at,
2436
2444
  source: 'play',
2437
2445
  line,
2446
+ ...(context ? { context } : {}),
2438
2447
  },
2439
2448
  options,
2440
2449
  );
@@ -3060,6 +3069,7 @@ async function run(
3060
3069
  error: runtimeLimitExceededCause(runtimeLimitSeconds),
3061
3070
  errors: [runtimeLimitExceededFailure(runtimeLimitSeconds)],
3062
3071
  logs: terminalLogsWithPhaseSummary(ctx),
3072
+ logContexts: terminalLogContextsWithPhaseSummary(ctx),
3063
3073
  stats: runtimeStats(),
3064
3074
  steps: ctx.getSteps(),
3065
3075
  tableNamespace: null,
@@ -3303,6 +3313,7 @@ async function run(
3303
3313
  : {}),
3304
3314
  outputRowCount,
3305
3315
  logs,
3316
+ logContexts: terminalLogContextsWithPhaseSummary(ctx),
3306
3317
  ...(runtimeDiagnostics.length > 0 ? { runtimeDiagnostics } : {}),
3307
3318
  ...(rowOutcomes ? { rowOutcomes } : {}),
3308
3319
  stats: runtimeStats(),
@@ -3357,6 +3368,7 @@ async function run(
3357
3368
  status: 'suspended',
3358
3369
  suspension: error.suspension,
3359
3370
  logs: terminalLogsWithPhaseSummary(ctx),
3371
+ logContexts: terminalLogContextsWithPhaseSummary(ctx),
3360
3372
  stats: runtimeStats(),
3361
3373
  steps: ctx.getSteps(),
3362
3374
  checkpoint: ctx.getCheckpoint(),
@@ -3392,6 +3404,7 @@ async function run(
3392
3404
  error: formatRunnerFailure(error),
3393
3405
  errors: [failure],
3394
3406
  logs: terminalLogsWithPhaseSummary(ctx),
3407
+ logContexts: terminalLogContextsWithPhaseSummary(ctx),
3395
3408
  ...(rowOutcomes ? { rowOutcomes } : {}),
3396
3409
  stats: runtimeStats(),
3397
3410
  steps: ctx.getSteps(),