deepline 0.3.286 → 0.3.288

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 (37) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +47 -24
  2. package/dist/bundling-sources/sdk/src/index.ts +4 -0
  3. package/dist/bundling-sources/sdk/src/release.ts +7 -1
  4. package/dist/bundling-sources/sdk/src/types.ts +36 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/cell-provenance.ts +16 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +17 -12
  7. package/dist/bundling-sources/shared_libs/play-runtime/docflow-node-io.ts +22 -54
  8. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +7 -26
  9. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +2 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +38 -3
  11. package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +12 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +43 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +19 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +88 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +3 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +53 -25
  18. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +47 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +83 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  21. package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +1 -89
  22. package/dist/cli/index.js +580 -629
  23. package/dist/cli/index.mjs +457 -507
  24. package/dist/{client-B7ic-kY5.d.ts → client-FR_u0dxT.d.ts} +38 -1
  25. package/dist/{client-Dzf_KovW.d.mts → client-phK1Ixcl.d.mts} +38 -1
  26. package/dist/index.d.mts +2 -2
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +49 -26
  29. package/dist/index.mjs +49 -26
  30. package/dist/install-integrity.json +2 -2
  31. package/dist/plays/run-request.d.mts +1 -1
  32. package/dist/plays/run-request.d.ts +1 -1
  33. package/dist/release.d.mts +5 -1
  34. package/dist/release.d.ts +5 -1
  35. package/dist/release.js +6 -1
  36. package/dist/release.mjs +6 -1
  37. package/package.json +1 -1
@@ -117,6 +117,8 @@ import type {
117
117
  DeeplineAgentModelDescription,
118
118
  InferenceQuote,
119
119
  ChildRunSummary,
120
+ SearchOptions,
121
+ SearchResult,
120
122
  } from './types.js';
121
123
  import { isStopAllPlayRunsDryRunResult } from './types.js';
122
124
  import type { MonitorDefinition } from './monitors.js';
@@ -610,6 +612,29 @@ function chunkRegisterPlayArtifacts<T>(artifacts: T[]): T[][] {
610
612
  return chunks;
611
613
  }
612
614
 
615
+ /** Query parameters shared by every search route. */
616
+ function searchParams(options: ToolSearchOptions): URLSearchParams {
617
+ const params = new URLSearchParams();
618
+ params.set('q', options.query?.trim() ?? '');
619
+ params.set(
620
+ 'include_search_debug',
621
+ options.includeSearchDebug ? 'true' : 'false',
622
+ );
623
+ params.set('search_mode', options.searchMode ?? 'v2');
624
+ if (options.limit !== undefined) params.set('limit', String(options.limit));
625
+ if (options.offset !== undefined) {
626
+ params.set('offset', String(options.offset));
627
+ }
628
+ if (options.categories?.trim()) {
629
+ params.set('categories', options.categories.trim());
630
+ }
631
+ if (options.searchTerms?.trim()) {
632
+ params.set('search_terms', options.searchTerms.trim());
633
+ }
634
+ if (options.task?.trim()) params.set('task', options.task.trim());
635
+ return params;
636
+ }
637
+
613
638
  /** Filters for `client.runs.list(...)`. */
614
639
  export type RunsListOptions = {
615
640
  play?: string;
@@ -3195,34 +3220,32 @@ export class DeeplineClient {
3195
3220
  async searchTools(
3196
3221
  options: ToolSearchOptions = {},
3197
3222
  ): Promise<ToolSearchResult> {
3198
- const params = new URLSearchParams();
3199
- const query = options.query?.trim() ?? '';
3200
- params.set('q', query);
3201
- params.set(
3202
- 'include_search_debug',
3203
- options.includeSearchDebug ? 'true' : 'false',
3204
- );
3205
- params.set('search_mode', options.searchMode ?? 'v2');
3206
- if (options.limit !== undefined) {
3207
- params.set('limit', String(options.limit));
3208
- }
3209
- if (options.offset !== undefined) {
3210
- params.set('offset', String(options.offset));
3211
- }
3212
- if (options.categories?.trim()) {
3213
- params.set('categories', options.categories.trim());
3214
- }
3215
- if (options.searchTerms?.trim()) {
3216
- params.set('search_terms', options.searchTerms.trim());
3217
- }
3218
- if (options.task?.trim()) {
3219
- params.set('task', options.task.trim());
3220
- }
3221
3223
  return this.http.get<ToolSearchResult>(
3222
- `/api/v2/tools/search?${params.toString()}`,
3224
+ `/api/v2/tools/search?${searchParams(options).toString()}`,
3223
3225
  );
3224
3226
  }
3225
3227
 
3228
+ /**
3229
+ * Search everything Deepline can run or deploy in one ranked list: tools,
3230
+ * monitor types, prebuilt plays, and your workspace's own plays.
3231
+ *
3232
+ * @example
3233
+ * ```typescript
3234
+ * const { results } = await client.search({
3235
+ * query: 'find work email',
3236
+ * types: ['tools', 'prebuilts'],
3237
+ * });
3238
+ * ```
3239
+ */
3240
+ async search(options: SearchOptions = {}): Promise<SearchResult> {
3241
+ const params = searchParams(options);
3242
+ if (options.types?.length) params.set('types', options.types.join(','));
3243
+ if (options.exclude?.length) {
3244
+ params.set('exclude', options.exclude.join(','));
3245
+ }
3246
+ return this.http.get<SearchResult>(`/api/v2/search?${params.toString()}`);
3247
+ }
3248
+
3226
3249
  /**
3227
3250
  * Get detailed metadata for a single tool.
3228
3251
  *
@@ -313,6 +313,10 @@ export type {
313
313
  StopPlayRunResult,
314
314
  ToolSearchOptions,
315
315
  ToolSearchResult,
316
+ SearchOptions,
317
+ SearchResult,
318
+ SearchResultItem,
319
+ SearchType,
316
320
  } from './types.js';
317
321
 
318
322
  export type {
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.286',
203
+ version: '0.3.288',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -235,6 +235,12 @@ export const SDK_RELEASE = {
235
235
  minimumSupported: '0.1.53',
236
236
  deprecatedBelow: '0.3.1',
237
237
  commandIntroducedIn: [
238
+ {
239
+ command: 'search',
240
+ introducedIn: '0.3.91',
241
+ reason:
242
+ 'deepline search was added in SDK CLI 0.3.91 as the one search across tools, monitors, and plays. On an older version run deepline tools search or deepline plays search, or update.',
243
+ },
238
244
  {
239
245
  command: 'notifications',
240
246
  introducedIn: '0.2.40',
@@ -488,6 +488,42 @@ export interface ToolSearchOptions {
488
488
  includeSearchDebug?: boolean;
489
489
  }
490
490
 
491
+ /** What a search can be narrowed to. `plays` is `prebuilts` plus `custom`. */
492
+ export type SearchType =
493
+ | 'tools'
494
+ | 'plays'
495
+ | 'monitors'
496
+ | 'prebuilts'
497
+ | 'custom';
498
+
499
+ /** Options for {@link DeeplineClient.search}. */
500
+ export interface SearchOptions extends ToolSearchOptions {
501
+ /** Kinds of result to search. Omit to search everything. */
502
+ types?: readonly SearchType[];
503
+ /** Kinds of result to leave out; the opposite of `types`. */
504
+ exclude?: readonly SearchType[];
505
+ }
506
+
507
+ /** One ranked search result; `kind` says which fields accompany it. */
508
+ export interface SearchResultItem {
509
+ /** Tool id for tools and monitor types, or play reference. */
510
+ id: string;
511
+ kind: 'tool' | 'monitor_type' | 'prebuilt_play' | 'custom_play';
512
+ displayName?: string;
513
+ description?: string;
514
+ [field: string]: unknown;
515
+ }
516
+
517
+ /** Ranked results across tools, monitor types, and plays. */
518
+ export interface SearchResult extends Omit<
519
+ ToolSearchResult,
520
+ 'tools' | 'omitted_plays_hint' | 'commandTemplates'
521
+ > {
522
+ results: SearchResultItem[];
523
+ /** The result kinds this search covered. */
524
+ kinds: SearchResultItem['kind'][];
525
+ }
526
+
491
527
  /**
492
528
  * Ranked tool/provider discovery response.
493
529
  *
@@ -45,6 +45,22 @@ export interface PlayCellDecision {
45
45
  reads: PlayCellReadRef[];
46
46
  }
47
47
 
48
+ /** One persisted outcome per row and predicate, never evaluation attempts. */
49
+ export type PlayConditionCounts = { true: number; false: number };
50
+
51
+ export function normalizePlayConditionCounts(
52
+ value: unknown,
53
+ ): PlayConditionCounts | undefined {
54
+ if (!value || typeof value !== 'object') return undefined;
55
+ const counts = value as PlayConditionCounts;
56
+ return Number.isSafeInteger(counts.true) &&
57
+ counts.true >= 0 &&
58
+ Number.isSafeInteger(counts.false) &&
59
+ counts.false >= 0
60
+ ? { true: counts.true, false: counts.false }
61
+ : undefined;
62
+ }
63
+
48
64
  /** Row-grain birth: the source row the runtime held while building this row. */
49
65
  export type PlayRowBornFromRow = { table: string; rowKey: string };
50
66
 
@@ -371,6 +371,7 @@ import {
371
371
  type PlayExecutionSuspension,
372
372
  } from './suspension';
373
373
  import {
374
+ createRequestHeaderSecretRedactionContext,
374
375
  createSecretRedactionContext,
375
376
  type SecretRedactionContext,
376
377
  } from './secret-redaction';
@@ -14669,6 +14670,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14669
14670
  if (headers['user-agent'] === undefined) {
14670
14671
  headers['user-agent'] = 'Deepline Play Runtime';
14671
14672
  }
14673
+ const requestRedactor = createRequestHeaderSecretRedactionContext(
14674
+ headers,
14675
+ Object.keys(secretHeaderMarkers),
14676
+ );
14672
14677
  const fetchInit = { ...init, headers };
14673
14678
  delete fetchInit.auth;
14674
14679
  const boundaryId = this.durableBoundaryId(
@@ -14755,18 +14760,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14755
14760
  log: (message) => this.runtimeLog(message),
14756
14761
  });
14757
14762
  const rawJson = parseJsonOrNull(bodyText);
14758
- const redactedBodyText = this.secretRedactor.redactString(bodyText);
14759
- const output: PlayFetchResponse = {
14760
- ok: response.ok,
14761
- status: response.status,
14762
- statusText: response.statusText,
14763
- url: response.url,
14764
- headers: this.secretRedactor.redact(
14765
- Object.fromEntries(response.headers.entries()),
14766
- ) as Record<string, string>,
14767
- bodyText: redactedBodyText,
14768
- json: this.secretRedactor.redactKnownSecrets(rawJson),
14769
- };
14763
+ const output: PlayFetchResponse =
14764
+ this.secretRedactor.redactKnownSecrets(
14765
+ requestRedactor.redactKnownSecrets({
14766
+ ok: response.ok,
14767
+ status: response.status,
14768
+ statusText: response.statusText,
14769
+ url: response.url,
14770
+ headers: Object.fromEntries(response.headers.entries()),
14771
+ bodyText,
14772
+ json: rawJson,
14773
+ }),
14774
+ );
14770
14775
 
14771
14776
  if (!output.ok) {
14772
14777
  this.logCtxFetchHttpFailure({
@@ -73,7 +73,7 @@ export type PlayDocflowNodeIoState = {
73
73
  };
74
74
 
75
75
  type PreviewOptions = {
76
- redactor?: SecretRedactionContext;
76
+ redactor?: Pick<SecretRedactionContext, 'redactRegisteredSecrets'>;
77
77
  };
78
78
 
79
79
  const utf8Encoder = new TextEncoder();
@@ -132,26 +132,13 @@ function truncateUtf8(value: string, maxBytes: number): string {
132
132
  return `${output}…`;
133
133
  }
134
134
 
135
- function redactString(
136
- redactor: SecretRedactionContext,
137
- value: string,
138
- key?: string,
139
- ): string {
140
- if (!key) return redactor.redactString(value);
141
- const wrapped = redactor.redact({ [key]: value }) as Record<string, unknown>;
142
- return typeof wrapped[key] === 'string'
143
- ? wrapped[key]
144
- : redactor.redactString(value);
145
- }
146
-
147
135
  function safeString(
148
136
  value: unknown,
149
- redactor: SecretRedactionContext,
150
- key?: string,
137
+ redactor?: PreviewOptions['redactor'],
151
138
  ): string | undefined {
152
139
  if (typeof value !== 'string') return undefined;
153
140
  return truncateUtf8(
154
- redactString(redactor, value, key),
141
+ redactor?.redactRegisteredSecrets(value) ?? value,
155
142
  DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
156
143
  );
157
144
  }
@@ -161,8 +148,7 @@ function previewValue(
161
148
  input: {
162
149
  depth: number;
163
150
  ancestors: WeakSet<object>;
164
- redactor: SecretRedactionContext;
165
- key?: string;
151
+ redactor?: PreviewOptions['redactor'];
166
152
  },
167
153
  ): PlayDocflowNodeValuePreview {
168
154
  if (value === null) return { kind: 'null' };
@@ -170,7 +156,7 @@ function previewValue(
170
156
  if (typeof value === 'string') {
171
157
  return {
172
158
  kind: 'scalar',
173
- value: safeString(value, input.redactor, input.key) ?? '',
159
+ value: safeString(value, input.redactor) ?? '',
174
160
  };
175
161
  }
176
162
  if (typeof value === 'number') {
@@ -207,18 +193,13 @@ function previewValue(
207
193
  ) {
208
194
  return {
209
195
  kind: 'dataset',
210
- datasetId:
211
- safeString(datasetId.value, input.redactor, 'datasetId') ?? 'dataset',
196
+ datasetId: safeString(datasetId.value, input.redactor) ?? 'dataset',
212
197
  datasetKind: datasetKind.value,
213
198
  tableNamespace:
214
199
  tableNamespace.ok && tableNamespace.value === null
215
200
  ? null
216
201
  : tableNamespace.ok
217
- ? safeString(
218
- tableNamespace.value,
219
- input.redactor,
220
- 'tableNamespace',
221
- )
202
+ ? safeString(tableNamespace.value, input.redactor)
222
203
  : undefined,
223
204
  };
224
205
  }
@@ -239,18 +220,13 @@ function previewValue(
239
220
  ) {
240
221
  return {
241
222
  kind: 'dataset',
242
- datasetId:
243
- safeString(datasetId.value, input.redactor, 'datasetId') ?? 'dataset',
223
+ datasetId: safeString(datasetId.value, input.redactor) ?? 'dataset',
244
224
  datasetKind: datasetKind.value,
245
225
  tableNamespace:
246
226
  tableNamespace.ok && tableNamespace.value === null
247
227
  ? null
248
228
  : tableNamespace.ok
249
- ? safeString(
250
- tableNamespace.value,
251
- input.redactor,
252
- 'tableNamespace',
253
- )
229
+ ? safeString(tableNamespace.value, input.redactor)
254
230
  : undefined,
255
231
  rowCount: Math.max(0, Math.floor(count.value)),
256
232
  };
@@ -309,7 +285,9 @@ function previewValue(
309
285
  const fields: Record<string, PlayDocflowNodeValuePreview> = {};
310
286
  for (const [key, descriptor] of selected) {
311
287
  const safeKey = truncateUtf8(
312
- ledgerSafeDocflowPreviewKey(input.redactor.redactString(key)),
288
+ ledgerSafeDocflowPreviewKey(
289
+ input.redactor?.redactRegisteredSecrets(key) ?? key,
290
+ ),
313
291
  DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
314
292
  );
315
293
  fields[safeKey] =
@@ -317,7 +295,6 @@ function previewValue(
317
295
  ? previewValue(descriptor.value, {
318
296
  ...input,
319
297
  depth: input.depth + 1,
320
- key,
321
298
  })
322
299
  : { kind: 'unavailable', reason: 'access_error' };
323
300
  }
@@ -338,7 +315,6 @@ export function buildPlayDocflowNodeIoPreviewMap(
338
315
  values: Record<string, unknown>,
339
316
  options: PreviewOptions = {},
340
317
  ): { values: PlayDocflowNodeIoPreviewMap; truncated: boolean } {
341
- const redactor = options.redactor ?? createSecretRedactionContext();
342
318
  const previews: PlayDocflowNodeIoPreviewMap = {};
343
319
  let truncated = false;
344
320
  let entries: Array<[string, unknown]>;
@@ -356,14 +332,15 @@ export function buildPlayDocflowNodeIoPreviewMap(
356
332
  break;
357
333
  }
358
334
  const safePath = truncateUtf8(
359
- ledgerSafeDocflowPreviewKey(redactor.redactString(path)),
335
+ ledgerSafeDocflowPreviewKey(
336
+ options.redactor?.redactRegisteredSecrets(path) ?? path,
337
+ ),
360
338
  DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
361
339
  );
362
340
  const preview = previewValue(value, {
363
341
  depth: 0,
364
342
  ancestors: new WeakSet(),
365
- redactor,
366
- key: path.split('.').at(-1),
343
+ redactor: options.redactor,
367
344
  });
368
345
  const candidate = { ...previews, [safePath]: preview };
369
346
  if (
@@ -413,7 +390,6 @@ export function buildPlayDocflowNodeInputPreviewMap(
413
390
  captures: readonly PlayDocflowNodeInputCapture[],
414
391
  options: PreviewOptions = {},
415
392
  ): { values: PlayDocflowNodeIoPreviewMap; truncated: boolean } {
416
- const redactor = options.redactor ?? createSecretRedactionContext();
417
393
  const values: PlayDocflowNodeIoPreviewMap = {};
418
394
  let truncated = false;
419
395
  for (const capture of captures.slice(0, DOCFLOW_NODE_IO_LIMITS.maxPaths)) {
@@ -435,15 +411,16 @@ export function buildPlayDocflowNodeInputPreviewMap(
435
411
  }
436
412
  }
437
413
  const safePath = truncateUtf8(
438
- ledgerSafeDocflowPreviewKey(redactor.redactString(capture.path)),
414
+ ledgerSafeDocflowPreviewKey(
415
+ options.redactor?.redactRegisteredSecrets(capture.path) ?? capture.path,
416
+ ),
439
417
  DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
440
418
  );
441
419
  const preview: PlayDocflowNodeValuePreview = available
442
420
  ? previewValue(current, {
443
421
  depth: 0,
444
422
  ancestors: new WeakSet(),
445
- redactor,
446
- key: capture.properties.at(-1) ?? capture.path,
423
+ redactor: options.redactor,
447
424
  })
448
425
  : { kind: 'unavailable', reason: 'access_error' };
449
426
  const candidate = { ...values, [safePath]: preview };
@@ -494,7 +471,6 @@ function normalizePreview(
494
471
  return previewValue(value.value, {
495
472
  depth: 0,
496
473
  ancestors: new WeakSet(),
497
- redactor: createSecretRedactionContext(),
498
474
  });
499
475
  }
500
476
  return null;
@@ -523,11 +499,7 @@ function normalizePreview(
523
499
  : null;
524
500
  }
525
501
  if (value.kind === 'dataset') {
526
- const datasetId = safeString(
527
- value.datasetId,
528
- createSecretRedactionContext(),
529
- 'datasetId',
530
- );
502
+ const datasetId = safeString(value.datasetId);
531
503
  if (
532
504
  !datasetId ||
533
505
  (value.datasetKind !== 'csv' && value.datasetKind !== 'map')
@@ -542,11 +514,7 @@ function normalizePreview(
542
514
  ? { tableNamespace: null }
543
515
  : typeof value.tableNamespace === 'string'
544
516
  ? {
545
- tableNamespace: safeString(
546
- value.tableNamespace,
547
- createSecretRedactionContext(),
548
- 'tableNamespace',
549
- ),
517
+ tableNamespace: safeString(value.tableNamespace),
550
518
  }
551
519
  : {}),
552
520
  ...(typeof value.rowCount === 'number' && Number.isFinite(value.rowCount)
@@ -59,52 +59,33 @@ export function sanitizeOpaqueLedgerPayload(value: unknown): unknown {
59
59
  return redactedIssue ? opaqueLedgerPayloadSummary(redactedIssue) : redacted;
60
60
  }
61
61
 
62
- /** A receipt payload redacted once, serialized once, and measured from that text. */
62
+ /** A customer receipt value serialized once and measured from the stored text. */
63
63
  export type SanitizedRuntimeReceiptPayload = {
64
- /** The redacted value. Callers return or re-project this, never the source. */
65
64
  value: unknown;
66
- /**
67
- * Canonical Postgres JSON text for `value`, byte-identical to what
68
- * `stringifyPostgresJson(value ?? null)` produces. `jsonb` accepts this text
69
- * directly, so the completion path never re-serializes the same output.
70
- */
65
+ /** Canonical Postgres JSON text, passed directly to the jsonb completion write. */
71
66
  serialized: string;
72
- /** UTF-8 byte length of `serialized`; the measured size the limit is enforced on. */
67
+ /** UTF-8 byte length of `serialized`. */
73
68
  bytes: number;
74
69
  };
75
70
 
76
- /**
77
- * Secret-redact a receipt payload without coupling its 10 MiB cell limit to the
78
- * ledger, and serialize it exactly once.
79
- *
80
- * The limit is enforced on the redacted, serialized value that is actually
81
- * persisted. Redaction normally shrinks credential-shaped values but can expand
82
- * short matched strings, so measuring the source instead would be wrong in both
83
- * directions.
84
- *
85
- * Redaction is identity-preserving: a payload carrying no credential comes back
86
- * as the very object that was passed in, with no deep clone, and is serialized
87
- * exactly once. Only a payload that actually redacts pays for a rebuilt value.
88
- */
89
71
  export function sanitizeRuntimeReceiptPayloadForStorage(
90
72
  value: unknown,
91
73
  ): SanitizedRuntimeReceiptPayload {
92
- const redacted = ledgerIngressRedactor.redact(value);
93
74
  // `undefined` is not JSON. The receipt contract stores it as jsonb `null`,
94
75
  // which is exactly what the previous `stringifyPostgresJson(output ?? null)`
95
76
  // parameter produced.
96
- const serialized = stringifyPostgresJson(redacted ?? null);
77
+ const serialized = stringifyPostgresJson(value ?? null);
97
78
  if (serialized === undefined) {
98
79
  throw new TypeError('Runtime receipt output is not JSON-serializable.');
99
80
  }
100
81
  const bytes = assertSerializedRuntimeReceiptOutputWithinLimit({
101
82
  serialized,
102
- path: 'runtime receipt output after secret redaction',
83
+ path: 'runtime receipt output',
103
84
  });
104
- return { value: redacted, serialized, bytes };
85
+ return { value, serialized, bytes };
105
86
  }
106
87
 
107
- /** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
88
+ /** Enforce the customer receipt byte limit without altering its values. */
108
89
  export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
109
90
  return sanitizeRuntimeReceiptPayloadForStorage(value).value;
110
91
  }
@@ -1,6 +1,7 @@
1
1
  import type { PlayRunLifecycleStatus } from './run-lifecycle-policy';
2
2
  import type { PlayDocflowNodeIoState } from './docflow-node-io';
3
3
  import type { RunStopCause } from './run-execution-state';
4
+ import type { PlayConditionCounts } from './cell-provenance';
4
5
  import type {
5
6
  PlayActivityObservation,
6
7
  PlayRunActivityProjection,
@@ -31,6 +32,7 @@ export type PlayVisualNodeProgressSnapshot = {
31
32
  completedAt?: number | null;
32
33
  artifactTableNamespace?: string | null;
33
34
  nodeIo?: PlayDocflowNodeIoState;
35
+ conditionCounts?: PlayConditionCounts;
34
36
  };
35
37
 
36
38
  /**
@@ -20,6 +20,8 @@ import {
20
20
  mergeDatasetBornFrom,
21
21
  normalizeDatasetBornFrom,
22
22
  type PlayDatasetBornFrom,
23
+ normalizePlayConditionCounts,
24
+ type PlayConditionCounts,
23
25
  } from './cell-provenance';
24
26
  import { isLedgerTerminalResultRef } from './output-size-limits';
25
27
  import type { PlayLogContext } from './log-context';
@@ -98,6 +100,7 @@ export type PlayRunLedgerStepProgress = {
98
100
  completedAt?: number | null;
99
101
  updatedAt?: number | null;
100
102
  nodeIo?: PlayDocflowNodeIoState;
103
+ conditionCounts?: PlayConditionCounts;
101
104
  };
102
105
 
103
106
  export type PlayRunLedgerStepSnapshot = {
@@ -1400,6 +1403,9 @@ function normalizeStepProgress(
1400
1403
  ...(normalizePlayDocflowNodeIoState(value.nodeIo)
1401
1404
  ? { nodeIo: normalizePlayDocflowNodeIoState(value.nodeIo) }
1402
1405
  : {}),
1406
+ ...(normalizePlayConditionCounts(value.conditionCounts)
1407
+ ? { conditionCounts: normalizePlayConditionCounts(value.conditionCounts) }
1408
+ : {}),
1403
1409
  };
1404
1410
  }
1405
1411
 
@@ -1760,11 +1766,36 @@ export function reducePlayRunLedgerEvent(
1760
1766
  // Detached runners can race their best-effort progress transport against the
1761
1767
  // terminal outbox, and gateway processes can restart or split that traffic.
1762
1768
  // The Play Run Ledger is the durable projection authority, so late
1763
- // `step.*` facts must be a true no-op here rather than relying on a gateway
1764
- // process to remember terminal state.
1769
+ // lifecycle facts must be a true no-op here rather than relying on a gateway
1770
+ // process to remember terminal state. Settled condition counts may arrive
1771
+ // after that claim and fill an absent summary without changing lifecycle.
1765
1772
  if (isTerminalPlayRunLedgerStatus(snapshot.status)) {
1766
1773
  if (event.type === RUN_EVENT_KIND.stepProgress) {
1767
- return refineTerminalSyntheticStepStart(snapshot, event) ?? snapshot;
1774
+ const refined =
1775
+ refineTerminalSyntheticStepStart(snapshot, event) ?? snapshot;
1776
+ const current = refined.stepsById[event.stepId];
1777
+ const conditionCounts = normalizePlayConditionCounts(
1778
+ event.progress.conditionCounts,
1779
+ );
1780
+ // Sheet reconciliation follows the fenced terminal claim. Accept its
1781
+ // first settled predicate pair without reopening lifecycle or timestamps.
1782
+ if (conditionCounts && !current?.progress?.conditionCounts) {
1783
+ return {
1784
+ ...refined,
1785
+ orderedStepIds: appendOrderedStepId(refined, event.stepId),
1786
+ stepsById: {
1787
+ ...refined.stepsById,
1788
+ [event.stepId]: {
1789
+ ...(current ?? {
1790
+ stepId: event.stepId,
1791
+ status: 'completed' as const,
1792
+ }),
1793
+ progress: { ...current?.progress, conditionCounts },
1794
+ },
1795
+ },
1796
+ };
1797
+ }
1798
+ return refined;
1768
1799
  }
1769
1800
  if (isStepLifecycleEvent(event)) return snapshot;
1770
1801
  }
@@ -2300,6 +2331,7 @@ function progressSignature(
2300
2331
  startedAt: progress?.startedAt ?? null,
2301
2332
  completedAt: progress?.completedAt ?? null,
2302
2333
  nodeIo: progress?.nodeIo ?? null,
2334
+ conditionCounts: progress?.conditionCounts ?? null,
2303
2335
  });
2304
2336
  }
2305
2337
 
@@ -2621,6 +2653,9 @@ export function buildPlayRunLedgerEventsFromStatusPatch(input: {
2621
2653
  ? { completedAt: progress.completedAt }
2622
2654
  : {}),
2623
2655
  ...(progress.nodeIo !== undefined ? { nodeIo: progress.nodeIo } : {}),
2656
+ ...(progress.conditionCounts !== undefined
2657
+ ? { conditionCounts: progress.conditionCounts }
2658
+ : {}),
2624
2659
  updatedAt: progress.updatedAt ?? checkpointAt,
2625
2660
  };
2626
2661
  if (
@@ -9,6 +9,10 @@ import {
9
9
  isTerminalPlayRunLifecycleStatus,
10
10
  type PlayRunPublicStatus,
11
11
  } from './run-lifecycle-policy';
12
+ import {
13
+ normalizePlayConditionCounts,
14
+ type PlayConditionCounts,
15
+ } from './cell-provenance';
12
16
  import type { RunStopCause } from './run-execution-state';
13
17
 
14
18
  /**
@@ -64,6 +68,7 @@ export type RunObservationNode = {
64
68
  * Run Snapshot fact `deriveNodeExecutionState` reads. Absent otherwise.
65
69
  */
66
70
  stoppedBy?: RunStopCause;
71
+ conditionCounts?: PlayConditionCounts;
67
72
  };
68
73
 
69
74
  export type RunObservation = {
@@ -204,6 +209,13 @@ function projectNode(node: PlayRunStreamNodeState): RunObservationNode {
204
209
  total: finite(node.progress?.total),
205
210
  settled: nonNegative(node.progress?.completed),
206
211
  failed: nonNegative(node.progress?.failed),
212
+ ...(normalizePlayConditionCounts(node.progress?.conditionCounts)
213
+ ? {
214
+ conditionCounts: normalizePlayConditionCounts(
215
+ node.progress?.conditionCounts,
216
+ ),
217
+ }
218
+ : {}),
207
219
  ...(node.stoppedBy ? { stoppedBy: node.stoppedBy } : {}),
208
220
  };
209
221
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from './run-lifecycle-policy';
19
19
  import type { PlayDocflowNodeIoState } from './docflow-node-io';
20
20
  import type { RunStopCause } from './run-execution-state';
21
+ import type { PlayConditionCounts } from './cell-provenance';
21
22
  import type { PlayVisualNodeUsageSnapshot } from './live-state-contract';
22
23
  import {
23
24
  projectPlayRunActivity,
@@ -53,6 +54,7 @@ export type PlayRunStreamNodeProgress = {
53
54
  completedAt?: number | null;
54
55
  artifactTableNamespace?: string | null;
55
56
  nodeIo?: PlayDocflowNodeIoState;
57
+ conditionCounts?: PlayConditionCounts;
56
58
  };
57
59
 
58
60
  export type PlayRunStreamNodeState = {
@@ -250,6 +252,7 @@ function buildSnapshotFromLedger(
250
252
  completedAt: step.completedAt ?? null,
251
253
  updatedAt: step.progress.updatedAt ?? step.updatedAt ?? null,
252
254
  nodeIo: step.progress.nodeIo,
255
+ conditionCounts: step.progress.conditionCounts,
253
256
  }
254
257
  : null,
255
258
  startedAt: step.startedAt ?? null,