deepline 0.1.80 → 0.1.81

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.
@@ -61,10 +61,6 @@ import {
61
61
  type WorkflowStepLike,
62
62
  } from './child-play-await';
63
63
  import type { AnyBatchOperationStrategy } from '../../../shared_libs/play-runtime/batching-types';
64
- import {
65
- createToolBatchExecutor,
66
- type ToolBatchRequest,
67
- } from '../../../shared_libs/play-runtime/tool-batch-executor';
68
64
  import {
69
65
  adaptV2ExecuteResponseToToolResult,
70
66
  createToolExecuteResult,
@@ -137,7 +133,6 @@ import {
137
133
  import { createHarnessWorkerReceiptStore } from './runtime/harness-receipt-store';
138
134
  import {
139
135
  applyCsvRenameProjection,
140
- stripCsvProjectedFields,
141
136
  stripCsvProjectionMetadata,
142
137
  cloneCsvAliasedRow,
143
138
  type CsvRenameOptions,
@@ -162,7 +157,6 @@ import type {
162
157
  LiveNodeProgressSnapshot,
163
158
  } from './runtime/live-progress';
164
159
  import {
165
- ToolHttpError,
166
160
  extractErrorBilling,
167
161
  isHardBillingToolHttpError,
168
162
  normalizeToolHttpErrorMessage,
@@ -597,6 +591,7 @@ type WorkerCtxCallbacks = {
597
591
  onNodeProgress?: (input: {
598
592
  nodeId: string;
599
593
  progress: LiveNodeProgressSnapshot;
594
+ forceFlush?: boolean;
600
595
  }) => void;
601
596
  onMapStarted?: (nodeId: string, at?: number) => void;
602
597
  onMapCompleted?: (nodeId: string, at?: number) => void;
@@ -685,12 +680,17 @@ function makeRequestId(): string {
685
680
  }
686
681
 
687
682
  function publicCsvInputRow<T extends Record<string, unknown>>(row: T): T {
688
- const stripped = stripCsvProjectedFields(row) as Record<string, unknown>;
689
- return Object.fromEntries(
690
- Object.entries(stripped).filter(
691
- ([fieldName]) => !fieldName.startsWith('__deepline'),
692
- ),
693
- ) as T;
683
+ const restored = stripCsvProjectionMetadata(row) as Record<string, unknown>;
684
+ const publicRow: Record<string, unknown> = {};
685
+ for (const fieldName of Reflect.ownKeys(restored)) {
686
+ if (typeof fieldName === 'string' && fieldName.startsWith('__deepline')) {
687
+ continue;
688
+ }
689
+ const descriptor = Object.getOwnPropertyDescriptor(restored, fieldName);
690
+ if (!descriptor) continue;
691
+ Object.defineProperty(publicRow, fieldName, descriptor);
692
+ }
693
+ return publicRow as T;
694
694
  }
695
695
 
696
696
  function publicCsvOutputRow<T extends Record<string, unknown>>(row: T): T {
@@ -707,6 +707,27 @@ function publicCsvOutputRow<T extends Record<string, unknown>>(row: T): T {
707
707
  return publicRow as T;
708
708
  }
709
709
 
710
+ function publicCsvStorageRow<T extends Record<string, unknown>>(row: T): T {
711
+ const publicRow = publicCsvInputRow(row) as Record<string, unknown>;
712
+ const storageRow: Record<string, unknown> = {};
713
+ for (const fieldName of Reflect.ownKeys(publicRow)) {
714
+ if (typeof fieldName !== 'string') continue;
715
+ const descriptor = Object.getOwnPropertyDescriptor(publicRow, fieldName);
716
+ if (!descriptor) continue;
717
+ storageRow[fieldName] =
718
+ 'value' in descriptor ? descriptor.value : publicRow[fieldName];
719
+ }
720
+ for (const runtimeField of [
721
+ '__deeplineRowKey',
722
+ '__deeplineCellMetaPatch',
723
+ ]) {
724
+ if (Object.prototype.hasOwnProperty.call(row, runtimeField)) {
725
+ storageRow[runtimeField] = row[runtimeField];
726
+ }
727
+ }
728
+ return storageRow as T;
729
+ }
730
+
710
731
  /**
711
732
  * Strip credentials and JWT-shaped tokens from any string before it lands in
712
733
  * a log buffer or upstream error message. The harness routinely echoes
@@ -1245,50 +1266,11 @@ async function callToolDirect(
1245
1266
  onRetryAttempt?: () => void,
1246
1267
  ): Promise<ToolExecuteResult> {
1247
1268
  const { id, toolId, input } = args;
1248
- if (toolId === 'test_rate_limit') {
1249
- return wrapWorkerToolResult(
1250
- toolId,
1251
- executeSyntheticTestRateLimit(input),
1252
- syntheticToolMetadata(toolId),
1253
- );
1254
- }
1255
- if (toolId === 'test_batch_rate_limit') {
1256
- return wrapWorkerToolResult(
1257
- toolId,
1258
- await executeSyntheticTestRateLimitBatch(req, input),
1259
- syntheticToolMetadata(toolId),
1260
- );
1261
- }
1262
1269
  const path = `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
1263
1270
  const maxAttempts = 3;
1264
1271
  let lastError: Error | null = null;
1265
1272
 
1266
1273
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1267
- if (toolId === 'test_transient_500' || toolId === 'test_transient_429') {
1268
- const syntheticResult = executeSyntheticTransientRetry(
1269
- toolId,
1270
- input,
1271
- attempt,
1272
- );
1273
- if (syntheticResult.ok) {
1274
- return wrapWorkerToolResult(
1275
- toolId,
1276
- syntheticResult.result,
1277
- syntheticToolMetadata(toolId),
1278
- );
1279
- }
1280
- lastError = new Error(
1281
- `tool ${toolId} ${syntheticResult.status} attempt ${attempt}/${maxAttempts}: ${syntheticResult.message}`,
1282
- );
1283
- if (attempt >= maxAttempts) {
1284
- throw lastError;
1285
- }
1286
- // Charge the retry budget per attempt, matching the cjs runner.
1287
- onRetryAttempt?.();
1288
- await new Promise((resolve) => setTimeout(resolve, 1_000));
1289
- continue;
1290
- }
1291
-
1292
1274
  const res = await fetchRuntimeApi(req.baseUrl, path, {
1293
1275
  method: 'POST',
1294
1276
  headers: {
@@ -1466,7 +1448,7 @@ function parseStringArray(value: unknown): string[] {
1466
1448
  .filter(Boolean);
1467
1449
  }
1468
1450
 
1469
- function syntheticToolMetadata(toolId: string): ToolResultMetadataInput {
1451
+ function toolMetadataFallback(toolId: string): ToolResultMetadataInput {
1470
1452
  if (toolId === 'test_rate_limit') {
1471
1453
  return {
1472
1454
  toolId,
@@ -1511,193 +1493,6 @@ function wrapWorkerToolResult(
1511
1493
  });
1512
1494
  }
1513
1495
 
1514
- async function executeSyntheticTestRateLimitBatch(
1515
- req: RunRequest,
1516
- input: Record<string, unknown>,
1517
- ): Promise<Record<string, unknown>> {
1518
- const delayMs =
1519
- typeof input.simulated_delay_ms === 'number' &&
1520
- Number.isInteger(input.simulated_delay_ms) &&
1521
- input.simulated_delay_ms > 0
1522
- ? input.simulated_delay_ms
1523
- : 0;
1524
- if (delayMs > 0) {
1525
- await new Promise((resolve) => setTimeout(resolve, delayMs));
1526
- }
1527
- const rawItems = Array.isArray(input.items) ? input.items : [];
1528
- const items = rawItems
1529
- .filter((item): item is Record<string, unknown> =>
1530
- Boolean(item && typeof item === 'object' && !Array.isArray(item)),
1531
- )
1532
- .map((item, index) => {
1533
- const itemKey =
1534
- typeof item.itemKey === 'string' && item.itemKey.trim()
1535
- ? item.itemKey.trim()
1536
- : `item-${index}`;
1537
- const payload =
1538
- item.payload &&
1539
- typeof item.payload === 'object' &&
1540
- !Array.isArray(item.payload)
1541
- ? (item.payload as Record<string, unknown>)
1542
- : {};
1543
- return { itemKey, payload };
1544
- });
1545
- const batchRequest: ToolBatchRequest = {
1546
- runId: req.runId,
1547
- orgId: req.orgId,
1548
- toolId: 'test_rate_limit',
1549
- operation: 'test_batch_rate_limit',
1550
- provider: 'test',
1551
- items,
1552
- waterfallId:
1553
- typeof input.waterfall_id === 'string' ? input.waterfall_id : null,
1554
- stageId: typeof input.stage === 'string' ? input.stage : null,
1555
- fieldName: typeof input.field_name === 'string' ? input.field_name : null,
1556
- mapName: typeof input.map_name === 'string' ? input.map_name : null,
1557
- chunkIndex:
1558
- typeof input.chunk_index === 'number' ? input.chunk_index : null,
1559
- userProvidedRateLimitKey:
1560
- typeof input.rate_limit_key === 'string' ? input.rate_limit_key : null,
1561
- providerBatchSize: 200,
1562
- };
1563
- const executor = createToolBatchExecutor({
1564
- async executeProviderBatch({ items: providerItems }) {
1565
- return providerItems.map((item) => ({
1566
- itemKey: item.itemKey,
1567
- result: executeSyntheticTestRateLimit(item.payload),
1568
- }));
1569
- },
1570
- });
1571
- const result = await executor.executeToolBatch(batchRequest);
1572
- return {
1573
- status: 'completed',
1574
- key: String(input.key ?? 'batch'),
1575
- provider: 'test',
1576
- batch: true,
1577
- batch_size: result.itemCount,
1578
- provider_batch_count: result.batchCount,
1579
- items: result.results.map((item) => ({
1580
- itemKey: item.itemKey,
1581
- result: item.result,
1582
- })),
1583
- };
1584
- }
1585
-
1586
- type SyntheticTransientRetryResult =
1587
- | { ok: true; result: Record<string, unknown> }
1588
- | { ok: false; status: number; message: string };
1589
-
1590
- function executeSyntheticTransientRetry(
1591
- toolId: string,
1592
- input: Record<string, unknown>,
1593
- attempt: number,
1594
- ): SyntheticTransientRetryResult {
1595
- const failuresBeforeSuccess =
1596
- typeof input.failures_before_success === 'number' &&
1597
- Number.isInteger(input.failures_before_success) &&
1598
- input.failures_before_success >= 0
1599
- ? input.failures_before_success
1600
- : 1;
1601
- if (attempt <= failuresBeforeSuccess) {
1602
- const status = toolId === 'test_transient_429' ? 429 : 502;
1603
- return {
1604
- ok: false,
1605
- status,
1606
- message: `Synthetic transient ${status} for attempt ${attempt}`,
1607
- };
1608
- }
1609
- return {
1610
- ok: true,
1611
- result: {
1612
- status: 'completed',
1613
- provider: 'test',
1614
- key: String(input.key ?? 'transient'),
1615
- attempts: attempt,
1616
- recovered: attempt > 1,
1617
- },
1618
- };
1619
- }
1620
-
1621
- function executeSyntheticTestRateLimit(
1622
- input: Record<string, unknown>,
1623
- ): Record<string, unknown> {
1624
- if (
1625
- typeof input.key === 'string' &&
1626
- input.key.startsWith('public-error-message-regression')
1627
- ) {
1628
- throw new ToolHttpError(
1629
- [
1630
- 'tool test_rate_limit 422 attempt 1/1:',
1631
- 'Synthetic public test error with a redacted token=[REDACTED].',
1632
- 'code=TEST_PUBLIC_ERROR.',
1633
- 'failure_description=The fake test provider intentionally raised a typed public error so V2 runner output preserves actionable details.',
1634
- 'operator_hint=Use this no-bill test provider fixture when verifying play runner error rendering.',
1635
- ].join(' '),
1636
- null,
1637
- );
1638
- }
1639
- const rowNumber =
1640
- typeof input.row_number === 'number' && Number.isInteger(input.row_number)
1641
- ? input.row_number
1642
- : null;
1643
- const leadId = typeof input.lead_id === 'string' ? input.lead_id : null;
1644
- const matchedDomain =
1645
- typeof input.matched_domain === 'string' && input.matched_domain.trim()
1646
- ? input.matched_domain.trim()
1647
- : 'example.com';
1648
- const matchedPrefix =
1649
- typeof input.matched_prefix === 'string' && input.matched_prefix.trim()
1650
- ? input.matched_prefix.trim()
1651
- : (leadId ??
1652
- (rowNumber !== null
1653
- ? `row${String(rowNumber).padStart(3, '0')}`
1654
- : 'match'));
1655
- const matched = syntheticMatchWindow(input, rowNumber);
1656
- const matchedEmail = matched ? `${matchedPrefix}@${matchedDomain}` : null;
1657
- const securityGateway =
1658
- input.emit_security_gateway === true
1659
- ? { email_status: 'valid', mx_security_gateway: true }
1660
- : {};
1661
- return {
1662
- status: 'completed',
1663
- key: String(input.key || ''),
1664
- provider: 'test',
1665
- lead_id: leadId,
1666
- row_number: rowNumber,
1667
- matched_result: matchedEmail,
1668
- email: matchedEmail,
1669
- value: matchedEmail,
1670
- batch: false,
1671
- ...securityGateway,
1672
- };
1673
- }
1674
-
1675
- function syntheticMatchWindow(
1676
- input: Record<string, unknown>,
1677
- rowNumber: number | null,
1678
- ): boolean {
1679
- const min =
1680
- typeof input.match_rows_min === 'number' ? input.match_rows_min : null;
1681
- const max =
1682
- typeof input.match_rows_max === 'number' ? input.match_rows_max : null;
1683
- if (rowNumber === null) return min === null && max === null;
1684
- if (min !== null && rowNumber < min) return false;
1685
- if (max !== null && rowNumber > max) return false;
1686
- const moduloBase =
1687
- typeof input.match_modulo_base === 'number' && input.match_modulo_base > 0
1688
- ? input.match_modulo_base
1689
- : null;
1690
- if (moduloBase !== null) {
1691
- const equals = Array.isArray(input.match_modulo_equals)
1692
- ? input.match_modulo_equals
1693
- .filter((entry): entry is number => typeof entry === 'number')
1694
- .map((entry) => entry % moduloBase)
1695
- : [];
1696
- return equals.length > 0 && equals.includes(rowNumber % moduloBase);
1697
- }
1698
- return true;
1699
- }
1700
-
1701
1496
  function isRecordLike(value: unknown): value is Record<string, unknown> {
1702
1497
  return value != null && typeof value === 'object' && !Array.isArray(value);
1703
1498
  }
@@ -1791,7 +1586,7 @@ type WorkerToolBatchRequest = {
1791
1586
  reject: (error: unknown) => void;
1792
1587
  };
1793
1588
 
1794
- const WORKER_TOOL_BATCH_GRACE_MS = 15;
1589
+ const WORKER_TOOL_BATCH_GRACE_MS = 250;
1795
1590
  // Fallback batch-chunk parallelism when a tool declares no provider rate hints.
1796
1591
  // Matches the prior hardcoded `Math.min(4, ...)` so undeclared providers keep
1797
1592
  // their previous batching behavior; declared providers tighten via the
@@ -1812,6 +1607,7 @@ class WorkerToolBatchScheduler {
1812
1607
  private readonly governor: PlayExecutionGovernor,
1813
1608
  private readonly resolvePacing: WorkerPacingResolver,
1814
1609
  private readonly abortSignal?: AbortSignal,
1610
+ private readonly onRequestsSettled?: (count: number) => void,
1815
1611
  ) {}
1816
1612
 
1817
1613
  /**
@@ -1933,6 +1729,7 @@ class WorkerToolBatchScheduler {
1933
1729
  } catch (error) {
1934
1730
  request.reject(error);
1935
1731
  } finally {
1732
+ this.onRequestsSettled?.(1);
1936
1733
  slot.release();
1937
1734
  }
1938
1735
  }),
@@ -1959,6 +1756,7 @@ class WorkerToolBatchScheduler {
1959
1756
  abortSignal: this.abortSignal,
1960
1757
  reportBackpressure: (retryAfterMs) =>
1961
1758
  this.reportBackpressure(toolId, retryAfterMs),
1759
+ onRequestsSettled: this.onRequestsSettled,
1962
1760
  });
1963
1761
  recordRunnerPerfTrace({
1964
1762
  req: this.req,
@@ -1992,12 +1790,25 @@ async function executeBatchedWorkerToolGroup(input: {
1992
1790
  suggestedParallelism: number;
1993
1791
  abortSignal?: AbortSignal;
1994
1792
  reportBackpressure: (retryAfterMs: number) => void;
1793
+ onRequestsSettled?: (count: number) => void;
1995
1794
  }): Promise<void> {
1996
1795
  const compiledBatches = compileRequestsWithStrategy({
1997
1796
  requests: input.requests,
1998
1797
  strategy: input.strategy,
1999
1798
  getPayload: (request) => request.input,
2000
1799
  });
1800
+ recordRunnerPerfTrace({
1801
+ req: input.req,
1802
+ phase: 'runner.tool.batch.compile',
1803
+ ms: 0,
1804
+ extra: {
1805
+ sourceOperation: input.strategy.sourceOperation,
1806
+ batchOperation: input.strategy.batchOperation,
1807
+ requests: input.requests.length,
1808
+ batches: compiledBatches.length,
1809
+ batchSizes: compiledBatches.map((batch) => batch.memberRequests.length),
1810
+ },
1811
+ });
2001
1812
 
2002
1813
  await executeChunkedRequests({
2003
1814
  requests: compiledBatches,
@@ -2052,11 +1863,18 @@ async function executeBatchedWorkerToolGroup(input: {
2052
1863
  wrapWorkerToolResult(
2053
1864
  request.toolId,
2054
1865
  splitResults[index] ?? null,
2055
- syntheticToolMetadata(request.toolId),
1866
+ toolMetadataFallback(request.toolId),
2056
1867
  ),
2057
1868
  );
2058
1869
  }
2059
1870
  }
1871
+ const settledMembers = chunkResults.reduce(
1872
+ (total, entry) => total + entry.request.memberRequests.length,
1873
+ 0,
1874
+ );
1875
+ if (settledMembers > 0) {
1876
+ input.onRequestsSettled?.(settledMembers);
1877
+ }
2060
1878
  },
2061
1879
  }).catch((error) => {
2062
1880
  for (const request of input.requests) {
@@ -3172,10 +2990,10 @@ async function persistCompletedMapRows(input: {
3172
2990
  tableNamespace: input.tableNamespace,
3173
2991
  sheetContract: augmentSheetContractWithDatasetFields({
3174
2992
  contract: requireSheetContract(input.req, input.tableNamespace),
3175
- rows: input.rows,
2993
+ rows: input.rows.map((row) => publicCsvStorageRow(row)),
3176
2994
  outputFields,
3177
2995
  }),
3178
- rows: input.rows,
2996
+ rows: input.rows.map((row) => publicCsvStorageRow(row)),
3179
2997
  outputFields,
3180
2998
  runId: input.req.runId,
3181
2999
  userEmail: input.req.userEmail,
@@ -3206,10 +3024,10 @@ async function prepareMapRows(input: {
3206
3024
  tableNamespace: input.tableNamespace,
3207
3025
  sheetContract: augmentSheetContractWithDatasetFields({
3208
3026
  contract: requireSheetContract(input.req, input.tableNamespace),
3209
- rows: input.rows,
3027
+ rows: input.rows.map((row) => publicCsvStorageRow(row)),
3210
3028
  outputFields: input.outputFields,
3211
3029
  }),
3212
- rows: input.rows.map((row) => ({ ...row })),
3030
+ rows: input.rows.map((row) => publicCsvStorageRow(row)),
3213
3031
  runId: input.req.runId,
3214
3032
  userEmail: input.req.userEmail,
3215
3033
  cellPolicies: input.cellPolicies,
@@ -3334,18 +3152,65 @@ function childPipelineNeedsWorkflowScheduler(
3334
3152
  * fail OPEN — grant immediately — matching customer-rate-limiter semantics so a
3335
3153
  * miswired binding degrades pacing without stalling the run.
3336
3154
  */
3337
- function createCoordinatorRatePort(): CoordinatorRatePort {
3155
+ function createCoordinatorRatePort(req: RunRequest): CoordinatorRatePort {
3338
3156
  return {
3339
3157
  async rateAcquire(input) {
3340
3158
  const binding = cachedCoordinatorBinding;
3341
3159
  if (!binding?.rateAcquire) {
3342
- return { granted: input.requested, waitMs: 0 };
3160
+ const coordinatorUrl = req.coordinatorUrl?.trim();
3161
+ if (!coordinatorUrl) {
3162
+ throw new Error('Coordinator rate acquire is unavailable.');
3163
+ }
3164
+ const res = await fetch(`${coordinatorUrl.replace(/\/$/, '')}/rate-acquire`, {
3165
+ method: 'POST',
3166
+ headers: {
3167
+ 'x-deepline-request-id': makeRequestId(),
3168
+ ...coordinatorRequestHeaders({
3169
+ runId: req.runId,
3170
+ contentType: 'application/json',
3171
+ internalToken: req.coordinatorInternalToken,
3172
+ }),
3173
+ },
3174
+ body: JSON.stringify(input),
3175
+ });
3176
+ if (!res.ok) {
3177
+ const text = await res.text().catch(() => '');
3178
+ throw new Error(
3179
+ `Coordinator rate acquire failed (${res.status}): ${text}`,
3180
+ );
3181
+ }
3182
+ return (await res.json()) as { granted: number; waitMs: number };
3343
3183
  }
3344
3184
  return await binding.rateAcquire(input);
3345
3185
  },
3346
3186
  async ratePenalize(input) {
3347
3187
  const binding = cachedCoordinatorBinding;
3348
- if (!binding?.ratePenalize) return;
3188
+ if (!binding?.ratePenalize) {
3189
+ const coordinatorUrl = req.coordinatorUrl?.trim();
3190
+ if (!coordinatorUrl) return;
3191
+ const res = await fetch(
3192
+ `${coordinatorUrl.replace(/\/$/, '')}/rate-penalize`,
3193
+ {
3194
+ method: 'POST',
3195
+ headers: {
3196
+ 'x-deepline-request-id': makeRequestId(),
3197
+ ...coordinatorRequestHeaders({
3198
+ runId: req.runId,
3199
+ contentType: 'application/json',
3200
+ internalToken: req.coordinatorInternalToken,
3201
+ }),
3202
+ },
3203
+ body: JSON.stringify(input),
3204
+ },
3205
+ );
3206
+ if (!res.ok) {
3207
+ const text = await res.text().catch(() => '');
3208
+ throw new Error(
3209
+ `Coordinator rate penalize failed (${res.status}): ${text}`,
3210
+ );
3211
+ }
3212
+ return;
3213
+ }
3349
3214
  await binding.ratePenalize(input);
3350
3215
  },
3351
3216
  };
@@ -3477,7 +3342,7 @@ function createGovernorForRun(req: RunRequest): {
3477
3342
  orgId: req.orgId,
3478
3343
  rootRunId: req.playCallGovernance?.rootRunId ?? req.runId,
3479
3344
  },
3480
- rateState: new CoordinatorRateStateBackend(createCoordinatorRatePort()),
3345
+ rateState: new CoordinatorRateStateBackend(createCoordinatorRatePort(req)),
3481
3346
  resolvePacing,
3482
3347
  resume: resumeGovernanceFromRequest(req),
3483
3348
  });
@@ -3660,6 +3525,7 @@ function createMinimalWorkerCtx(
3660
3525
  ...progress,
3661
3526
  updatedAt: progress.updatedAt ?? nowMs(),
3662
3527
  },
3528
+ forceFlush: true,
3663
3529
  });
3664
3530
  };
3665
3531
  const formatMapProgressMessage = (completed: number, total?: number) =>
@@ -3789,6 +3655,18 @@ function createMinimalWorkerCtx(
3789
3655
  completedRows: prepared.completedRows.length,
3790
3656
  },
3791
3657
  });
3658
+ updateMapProgress({
3659
+ completed: prepared.completedRows.length,
3660
+ total: chunkRows.length,
3661
+ startedAt: mapStartedAt,
3662
+ message:
3663
+ prepared.pendingRows.length > 0
3664
+ ? `${prepared.pendingRows.length.toLocaleString()} rows queued`
3665
+ : formatMapProgressMessage(
3666
+ prepared.completedRows.length,
3667
+ chunkRows.length,
3668
+ ),
3669
+ });
3792
3670
  const pendingKeys = new Set<string>();
3793
3671
  const pendingRowsByKey = new Map<string, Record<string, unknown>>();
3794
3672
  const completedKeys = new Set<string>();
@@ -3836,6 +3714,34 @@ function createMinimalWorkerCtx(
3836
3714
  0,
3837
3715
  prepared.skipped - missingPreparedRows.length,
3838
3716
  );
3717
+ let settledToolRequests = 0;
3718
+ let lastToolProgressAt = 0;
3719
+ const reportSettledToolRequests = (count: number) => {
3720
+ if (count <= 0) return;
3721
+ settledToolRequests += count;
3722
+ const now = nowMs();
3723
+ const estimatedCompleted = Math.min(
3724
+ chunkRows.length,
3725
+ prepared.completedRows.length + settledToolRequests,
3726
+ );
3727
+ const isTerminalEstimate = estimatedCompleted >= chunkRows.length;
3728
+ if (
3729
+ !isTerminalEstimate &&
3730
+ now - lastToolProgressAt < RUN_LEDGER_FLUSH_INTERVAL_MS
3731
+ ) {
3732
+ return;
3733
+ }
3734
+ lastToolProgressAt = now;
3735
+ updateMapProgress({
3736
+ completed: estimatedCompleted,
3737
+ total: chunkRows.length,
3738
+ startedAt: mapStartedAt,
3739
+ message: formatMapProgressMessage(
3740
+ estimatedCompleted,
3741
+ chunkRows.length,
3742
+ ),
3743
+ });
3744
+ };
3839
3745
  // Row concurrency comes from the Governor: an explicit map concurrency is
3840
3746
  // clamped to the policy row-max, otherwise the policy default. Each row
3841
3747
  // body additionally acquires a global row slot (the Governor's rowMax
@@ -3863,6 +3769,7 @@ function createMinimalWorkerCtx(
3863
3769
  governor,
3864
3770
  resolveToolPacing,
3865
3771
  abortSignal,
3772
+ reportSettledToolRequests,
3866
3773
  );
3867
3774
  const generatedOutputFields = new Set<string>();
3868
3775
  let idx = 0;
@@ -5398,6 +5305,100 @@ async function executeRunRequest(
5398
5305
 
5399
5306
  const stepProgressSnapshot = () => ({ ...stepProgressByNodeId });
5400
5307
 
5308
+ const publishCoordinatorProgressEvent = async (
5309
+ occurredAt: number,
5310
+ ): Promise<void> => {
5311
+ const coordinatorUrl = req.coordinatorUrl?.trim();
5312
+ if (!coordinatorUrl) {
5313
+ recordRunnerPerfTrace({
5314
+ req,
5315
+ phase: 'runner.coordinator_progress_publish',
5316
+ ms: 0,
5317
+ extra: { status: 'skipped_no_url' },
5318
+ });
5319
+ return;
5320
+ }
5321
+ const publishStartedAt = nowMs();
5322
+ const liveNodeProgress = stepProgressSnapshot();
5323
+ const activeEntry =
5324
+ Object.entries(liveNodeProgress).find(
5325
+ ([, progress]) => typeof progress.completedAt !== 'number',
5326
+ ) ?? Object.entries(liveNodeProgress).at(-1);
5327
+ const activeNodeId = activeEntry?.[0] ?? null;
5328
+ const activeProgress = activeEntry?.[1] ?? null;
5329
+ const activeArtifactTableNamespace =
5330
+ typeof activeProgress?.artifactTableNamespace === 'string'
5331
+ ? activeProgress.artifactTableNamespace
5332
+ : null;
5333
+ const activeCompleted =
5334
+ typeof activeProgress?.completed === 'number'
5335
+ ? activeProgress.completed
5336
+ : null;
5337
+ const activeTotal =
5338
+ typeof activeProgress?.total === 'number' ? activeProgress.total : null;
5339
+ const activeMessage =
5340
+ typeof activeProgress?.message === 'string'
5341
+ ? activeProgress.message
5342
+ : null;
5343
+ const response = await fetch(
5344
+ `${coordinatorUrl.replace(/\/$/, '')}/dedup/${encodeURIComponent(
5345
+ req.runId,
5346
+ )}/event-add`,
5347
+ {
5348
+ method: 'POST',
5349
+ headers: {
5350
+ 'x-deepline-request-id': makeRequestId(),
5351
+ ...coordinatorRequestHeaders({
5352
+ runId: req.runId,
5353
+ contentType: 'application/json',
5354
+ internalToken: req.coordinatorInternalToken,
5355
+ }),
5356
+ },
5357
+ body: JSON.stringify({
5358
+ runId: req.runId,
5359
+ type: 'progress',
5360
+ status: 'running',
5361
+ ts: occurredAt,
5362
+ logs: runLogBuffer,
5363
+ activeNodeId,
5364
+ activeArtifactTableNamespace,
5365
+ updatedAt: occurredAt,
5366
+ liveNodeProgress,
5367
+ }),
5368
+ },
5369
+ );
5370
+ if (!response.ok) {
5371
+ recordRunnerPerfTrace({
5372
+ req,
5373
+ phase: 'runner.coordinator_progress_publish',
5374
+ ms: nowMs() - publishStartedAt,
5375
+ extra: {
5376
+ status: 'failed',
5377
+ httpStatus: response.status,
5378
+ activeNodeId,
5379
+ activeArtifactTableNamespace,
5380
+ activeCompleted,
5381
+ activeTotal,
5382
+ activeMessage,
5383
+ },
5384
+ });
5385
+ throw new Error(`coordinator progress event failed ${response.status}`);
5386
+ }
5387
+ recordRunnerPerfTrace({
5388
+ req,
5389
+ phase: 'runner.coordinator_progress_publish',
5390
+ ms: nowMs() - publishStartedAt,
5391
+ extra: {
5392
+ status: 'ok',
5393
+ activeNodeId,
5394
+ activeArtifactTableNamespace,
5395
+ activeCompleted,
5396
+ activeTotal,
5397
+ activeMessage,
5398
+ },
5399
+ });
5400
+ };
5401
+
5401
5402
  const appendStepLifecycleEvent = (event: PlayStepLifecycleEvent) => {
5402
5403
  updateStepProgress({
5403
5404
  nodeId: event.nodeId,
@@ -5465,6 +5466,12 @@ async function executeRunRequest(
5465
5466
  progress.artifactTableNamespace === null
5466
5467
  ? { artifactTableNamespace: progress.artifactTableNamespace }
5467
5468
  : {}),
5469
+ ...(typeof progress.startedAt === 'number'
5470
+ ? { startedAt: progress.startedAt }
5471
+ : {}),
5472
+ ...(typeof progress.completedAt === 'number'
5473
+ ? { completedAt: progress.completedAt }
5474
+ : {}),
5468
5475
  updatedAt:
5469
5476
  typeof progress.updatedAt === 'number'
5470
5477
  ? progress.updatedAt
@@ -5513,6 +5520,7 @@ async function executeRunRequest(
5513
5520
  pendingLedgerEvents = [...events, ...pendingLedgerEvents];
5514
5521
  throw new Error('runtime run-ledger append failed');
5515
5522
  }
5523
+ await publishCoordinatorProgressEvent(now).catch(() => undefined);
5516
5524
  })
5517
5525
  .catch(() => undefined);
5518
5526
  };
@@ -5556,7 +5564,7 @@ async function executeRunRequest(
5556
5564
  const workerCallbacks: WorkerCtxCallbacks = {
5557
5565
  onNodeProgress: (input) => {
5558
5566
  updateStepProgress(input);
5559
- flushLedgerEvents(false);
5567
+ flushLedgerEvents(Boolean(input.forceFlush));
5560
5568
  },
5561
5569
  onMapStarted: (nodeId, at) => stepLifecycle?.onMapStarted(nodeId, at),
5562
5570
  onMapCompleted: (nodeId, at) => stepLifecycle?.onMapCompleted(nodeId, at),