deepline 0.3.372 → 0.3.374

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 (116) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +1 -53
  3. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +15 -23
  4. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +65 -3
  5. package/dist/bundling-sources/shared_libs/plays/email-status.ts +95 -0
  6. package/dist/bundling-sources/shared_libs/plays/extractor-targets.ts +14 -0
  7. package/dist/bundling-sources/shared_libs/plays/field-catalog.ts +253 -0
  8. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  9. package/dist/cli/auth-IFXXXZP6.js +33 -0
  10. package/dist/cli/{auth-ODSACXLF.mjs → auth-YGCDA4WG.mjs} +4 -4
  11. package/dist/cli/{billing-OPIOZLAN.mjs → billing-3ZLR3CJU.mjs} +4 -4
  12. package/dist/cli/{billing-4ERXY2DZ.js → billing-ZXRKPD5D.js} +73 -73
  13. package/dist/cli/{chunk-XSPAI6MF.mjs → chunk-2DAWTVNN.mjs} +2 -2
  14. package/dist/cli/{chunk-E5UD3JDQ.js → chunk-3VN4FTE3.js} +289 -5
  15. package/dist/cli/{chunk-VVFH745L.js → chunk-3XKAOV74.js} +6 -6
  16. package/dist/cli/{chunk-N5TPMNTT.js → chunk-53PZA4EP.js} +8 -8
  17. package/dist/cli/{chunk-BFLPCGOR.mjs → chunk-62IBVKWB.mjs} +1 -1
  18. package/dist/cli/{chunk-2RC5JQ3C.mjs → chunk-A62HBMHY.mjs} +1 -1
  19. package/dist/cli/{chunk-3GK3SH3G.mjs → chunk-E6BZWURC.mjs} +200 -13
  20. package/dist/cli/{chunk-KC2YOJTU.mjs → chunk-FT6O2ZVJ.mjs} +319 -115
  21. package/dist/cli/{chunk-5HYD3GMS.mjs → chunk-GT6KGIWV.mjs} +3 -3
  22. package/dist/cli/{chunk-RUYZAREB.js → chunk-HC5L6KGE.js} +9 -9
  23. package/dist/cli/{chunk-JAERLCMH.mjs → chunk-IZVGMVNX.mjs} +1 -1
  24. package/dist/cli/{chunk-ML2ZY7YD.js → chunk-JK547IDB.js} +4 -4
  25. package/dist/cli/{chunk-D6O3NJHX.js → chunk-MOBOAZMA.js} +60 -60
  26. package/dist/cli/{chunk-DONL233W.mjs → chunk-MQ5UW5AZ.mjs} +4 -4
  27. package/dist/cli/{chunk-MIMV5LTU.mjs → chunk-NNYQ24O5.mjs} +1 -1
  28. package/dist/cli/{chunk-DM5QUZJR.js → chunk-NOSIMGZV.js} +1 -1
  29. package/dist/cli/{chunk-AQFA7HMN.js → chunk-O7KJYX7R.js} +15 -15
  30. package/dist/cli/{chunk-U3W7E5WT.mjs → chunk-OS7GXARW.mjs} +27 -15
  31. package/dist/cli/{chunk-BVUGPOU3.mjs → chunk-OYTPVM2R.mjs} +2 -2
  32. package/dist/cli/{chunk-EFJL5FCU.js → chunk-PCYCNZLR.js} +8 -8
  33. package/dist/cli/{chunk-6MDAGRZP.js → chunk-RT5ICJ5V.js} +2 -2
  34. package/dist/cli/{chunk-GZQKUFM5.js → chunk-RXR7A2MX.js} +26 -26
  35. package/dist/cli/{chunk-4LIUZ4QR.mjs → chunk-SBB27CIJ.mjs} +2 -2
  36. package/dist/cli/{chunk-RJ7JJMQO.js → chunk-T35LCTC7.js} +266 -254
  37. package/dist/cli/{chunk-IUWPC5K6.js → chunk-TZTGK5JQ.js} +50 -50
  38. package/dist/cli/{chunk-FGHRRJ3D.mjs → chunk-VZSVYNZH.mjs} +1 -1
  39. package/dist/cli/{chunk-WJWO74MM.js → chunk-X5V2QTDR.js} +390 -186
  40. package/dist/cli/{chunk-JB6ERQUV.mjs → chunk-Y6VOGHF2.mjs} +284 -0
  41. package/dist/cli/{chunk-3CTB7V5F.mjs → chunk-YFFH6NDU.mjs} +2 -2
  42. package/dist/cli/{chunk-2JRRXXTA.js → chunk-YPVTQRUM.js} +201 -14
  43. package/dist/cli/client-O6IEMURJ.js +20 -0
  44. package/dist/cli/{client-KF2J754U.mjs → client-TV254HC4.mjs} +3 -3
  45. package/dist/cli/{csv-UL3MCD5D.mjs → csv-VEHHRUG4.mjs} +3 -3
  46. package/dist/cli/{csv-UJ3M7NFP.js → csv-WSRTANXI.js} +12 -12
  47. package/dist/cli/{db-SIOWA63T.mjs → db-BH25DX2G.mjs} +4 -4
  48. package/dist/cli/{db-444XYOTV.js → db-JXZNMFCE.js} +29 -29
  49. package/dist/cli/{describe-RRBESS3S.js → describe-N2OBGVJM.js} +61 -32
  50. package/dist/cli/{describe-GI5GHAKO.mjs → describe-TMBECHTX.mjs} +46 -17
  51. package/dist/cli/{executions-ERXZUOL5.js → executions-TX5CQF66.js} +6 -6
  52. package/dist/cli/{executions-4YDP6BXI.mjs → executions-Y4HQHYSO.mjs} +4 -4
  53. package/dist/cli/{feedback-3M7D7DZQ.js → feedback-ASEJ5A7I.js} +7 -7
  54. package/dist/cli/{feedback-CPWQL5LU.mjs → feedback-W7SRXYSP.mjs} +3 -3
  55. package/dist/cli/{git-XN6C3TJZ.mjs → git-2W7CZZRC.mjs} +4 -4
  56. package/dist/cli/git-SSSOIQ2L.js +23 -0
  57. package/dist/cli/index.js +81 -81
  58. package/dist/cli/index.mjs +32 -32
  59. package/dist/cli/{legacy-noops-5XDGOFWH.js → legacy-noops-NJYAAGBY.js} +7 -7
  60. package/dist/cli/{legacy-noops-KXQ6TFFL.mjs → legacy-noops-TXMPY3P7.mjs} +4 -4
  61. package/dist/cli/{monitors-33IU5JR6.mjs → monitors-G4L65QGQ.mjs} +4 -4
  62. package/dist/cli/{monitors-KH22PDOC.js → monitors-YLKTQ57D.js} +85 -85
  63. package/dist/cli/{org-MHYMWUEP.mjs → org-BCHOWKIA.mjs} +4 -4
  64. package/dist/cli/{org-X4NSLQG5.js → org-ENXO2A33.js} +64 -64
  65. package/dist/cli/{play-LCZKHKW2.mjs → play-JBY7HUYN.mjs} +8 -8
  66. package/dist/cli/play-SSJ3PO4D.js +155 -0
  67. package/dist/cli/{providers-J6LWCF5R.js → providers-GWFX3E7C.js} +7 -7
  68. package/dist/cli/{providers-L5FJTSEA.mjs → providers-JV2DZG43.mjs} +4 -4
  69. package/dist/cli/{quickstart-NE3BTWGL.mjs → quickstart-AIXURXZQ.mjs} +3 -3
  70. package/dist/cli/{quickstart-OOPFA4MX.js → quickstart-CASPK7F4.js} +5 -5
  71. package/dist/cli/{search-DBXOEOIE.mjs → search-DYQB7LHK.mjs} +5 -5
  72. package/dist/cli/search-N7K6A2YV.js +26 -0
  73. package/dist/cli/{secrets-WPMU7UBD.js → secrets-AZQG2E4U.js} +13 -13
  74. package/dist/cli/{secrets-U2MLWZH2.mjs → secrets-RQZPIPO5.mjs} +4 -4
  75. package/dist/cli/{sessions-WYNC4AUE.mjs → sessions-3FJJDMBB.mjs} +4 -4
  76. package/dist/cli/{sessions-YYZ3UJDS.js → sessions-ZY6RBYD4.js} +5 -5
  77. package/dist/cli/{settings-WQDFX3X6.js → settings-GXKWNR5V.js} +74 -74
  78. package/dist/cli/{settings-FQZSHFC6.mjs → settings-UNZDQGSE.mjs} +7 -7
  79. package/dist/cli/{setup-JKYJT35S.mjs → setup-PIRXJ2KJ.mjs} +6 -6
  80. package/dist/cli/{setup-DKYVNDTM.js → setup-UURZD4YO.js} +41 -41
  81. package/dist/cli/skills-4OUZLKTW.js +28 -0
  82. package/dist/cli/{skills-VHVCJJJY.mjs → skills-B4AQEIAF.mjs} +4 -4
  83. package/dist/cli/tools-MIZS5UN6.js +40 -0
  84. package/dist/cli/{tools-E23PU7KM.mjs → tools-UMGQKUSW.mjs} +8 -8
  85. package/dist/cli/update-3CO7LA4M.js +28 -0
  86. package/dist/cli/{update-4A4VU6CW.mjs → update-SFUTMEKL.mjs} +6 -6
  87. package/dist/cli/{workflow-WB2USWAA.js → workflow-CDAJCMT7.js} +15 -15
  88. package/dist/cli/{workflow-2ZR2ECTG.mjs → workflow-ON6XVW36.mjs} +5 -5
  89. package/dist/{client-Cw0e2Uec.d.mts → client-Cn-txrXv.d.mts} +1 -1
  90. package/dist/{client-CwdiawoG.d.ts → client-fPoIN-3V.d.ts} +1 -1
  91. package/dist/{compiler-manifest-DgdW0q0b.d.mts → compiler-manifest-CY2v8ab0.d.mts} +2 -0
  92. package/dist/{compiler-manifest-DgdW0q0b.d.ts → compiler-manifest-CY2v8ab0.d.ts} +2 -0
  93. package/dist/{index-C3nQQTI-.d.mts → index-B9fBZH1g.d.mts} +1 -1
  94. package/dist/{index-CEYQAfS0.d.ts → index-BZ6uzZJl.d.ts} +1 -1
  95. package/dist/index.d.mts +5 -5
  96. package/dist/index.d.ts +5 -5
  97. package/dist/index.js +79 -249
  98. package/dist/index.mjs +10 -180
  99. package/dist/install-integrity.json +85 -84
  100. package/dist/plays/bundle-play-file.d.mts +3 -3
  101. package/dist/plays/bundle-play-file.d.ts +3 -3
  102. package/dist/plays/run-request.d.mts +2 -2
  103. package/dist/plays/run-request.d.ts +2 -2
  104. package/dist/release.d.mts +1 -1
  105. package/dist/release.d.ts +1 -1
  106. package/dist/release.js +2 -2
  107. package/dist/release.mjs +1 -1
  108. package/package.json +1 -1
  109. package/dist/cli/auth-4SXMV3CD.js +0 -33
  110. package/dist/cli/client-UUNLXOHF.js +0 -20
  111. package/dist/cli/git-ITSCNUVB.js +0 -23
  112. package/dist/cli/play-4QRPOBWW.js +0 -155
  113. package/dist/cli/search-UGQDBP76.js +0 -26
  114. package/dist/cli/skills-VBQMXDAX.js +0 -28
  115. package/dist/cli/tools-ACEJZMJD.js +0 -40
  116. package/dist/cli/update-V5KMYHVE.js +0 -28
@@ -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.372',
203
+ version: '0.3.374',
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: {
@@ -28,7 +28,6 @@ import {
28
28
  RUNTIME_SANDBOX_MEMORY_ABORT_CODE,
29
29
  RUNTIME_SANDBOX_MEMORY_ABORT_MESSAGE,
30
30
  RUNTIME_SANDBOX_OOM_MESSAGE,
31
- RUNTIME_CONTROL_PLANE_UNAVAILABLE_MESSAGE,
32
31
  } from '../../run-failure';
33
32
  import {
34
33
  buildDaytonaPayloadMaterializerSource,
@@ -295,41 +294,6 @@ function watchdogMemoryAbortDiagnostic() {
295
294
  return null;
296
295
  }
297
296
 
298
- function watchdogRunnerHeartbeatUnavailableDiagnostic() {
299
- try {
300
- if (!terminationDiagnosticPath) return null;
301
- const diagnostic = JSON.parse(
302
- fs.readFileSync(terminationDiagnosticPath, 'utf8'),
303
- );
304
- if (
305
- !diagnostic ||
306
- diagnostic.schemaVersion !== 1 ||
307
- diagnostic.reason !== 'runner_heartbeat_unreachable'
308
- ) {
309
- return null;
310
- }
311
- return {
312
- heartbeatAttempt: Number.isSafeInteger(diagnostic.heartbeatAttempt)
313
- ? diagnostic.heartbeatAttempt
314
- : null,
315
- heartbeatAgeMs: Number.isFinite(diagnostic.heartbeatAgeMs)
316
- ? Math.max(0, diagnostic.heartbeatAgeMs)
317
- : null,
318
- leaseMs: Number.isFinite(diagnostic.leaseMs)
319
- ? Math.max(0, diagnostic.leaseMs)
320
- : null,
321
- lastErrorCode:
322
- typeof diagnostic.lastErrorCode === 'string' &&
323
- /^(?:E[A-Z0-9_]+|ERR_[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+|TimeoutError|redirect_[0-9]{3}|http_[0-9]{3}|protocol_error|unknown)$/.test(
324
- diagnostic.lastErrorCode,
325
- )
326
- ? diagnostic.lastErrorCode
327
- : 'unknown',
328
- };
329
- } catch {}
330
- return null;
331
- }
332
-
333
297
  function memoryAbortCause(diagnostic) {
334
298
  const parts = [
335
299
  ${JSON.stringify(`${RUNTIME_SANDBOX_MEMORY_ABORT_CODE}: ${RUNTIME_SANDBOX_MEMORY_ABORT_MESSAGE}`)},
@@ -411,7 +375,6 @@ async function main() {
411
375
  // SIGKILL does not collapse into the wrapper's generic exit code 1.
412
376
  const sandboxKilled = exitCode === 137 || watchdogObservedChildSigkill();
413
377
  const memoryAbort = watchdogMemoryAbortDiagnostic();
414
- const heartbeatUnavailable = watchdogRunnerHeartbeatUnavailableDiagnostic();
415
378
  const sandboxOom =
416
379
  !memoryAbort &&
417
380
  sandboxKilled &&
@@ -422,10 +385,6 @@ async function main() {
422
385
  ? memoryAbortCause(memoryAbort)
423
386
  : sandboxOom
424
387
  ? 'RUNTIME_SANDBOX_OOM: ' + ${JSON.stringify(RUNTIME_SANDBOX_OOM_MESSAGE)}
425
- : heartbeatUnavailable
426
- ? 'RUNTIME_CONTROL_PLANE_UNAVAILABLE: ' +
427
- ${JSON.stringify(RUNTIME_CONTROL_PLANE_UNAVAILABLE_MESSAGE)} +
428
- ' heartbeat=' + JSON.stringify(heartbeatUnavailable)
429
388
  : sandboxKilled
430
389
  ? 'RUNTIME_SANDBOX_KILLED: ' + ${JSON.stringify(RUNTIME_SANDBOX_KILLED_MESSAGE)}
431
390
  :
@@ -453,15 +412,6 @@ async function main() {
453
412
  cause: synthesizedError,
454
413
  }] }
455
414
  : {}),
456
- ...(heartbeatUnavailable && !runtimeLimitExceeded && !memoryAbort && !sandboxOom
457
- ? { errors: [{
458
- code: 'RUNTIME_CONTROL_PLANE_UNAVAILABLE',
459
- phase: 'infrastructure',
460
- message: ${JSON.stringify(RUNTIME_CONTROL_PLANE_UNAVAILABLE_MESSAGE)},
461
- retryable: false,
462
- cause: synthesizedError,
463
- }] }
464
- : {}),
465
415
  ...(sandboxOom
466
416
  ? { errors: [{
467
417
  code: 'RUNTIME_SANDBOX_OOM',
@@ -480,9 +430,7 @@ async function main() {
480
430
  cause: synthesizedError,
481
431
  }] }
482
432
  : {}),
483
- logs: heartbeatUnavailable
484
- ? ['[runtime.failure_diagnostic] ' + JSON.stringify({ event: 'runner_heartbeat_unavailable', ...heartbeatUnavailable })]
485
- : [],
433
+ logs: [],
486
434
  stats: {},
487
435
  steps: [],
488
436
  checkpointDisposition: '${RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT}',
@@ -196,8 +196,6 @@ let heartbeatConfigRead = false;
196
196
  let heartbeatConfig = null;
197
197
  let heartbeatLastSuccessAtMs = 0;
198
198
  let heartbeatAttempt = 0;
199
- let heartbeatLastErrorCode = null;
200
- let heartbeatStopBeforeLeaseMs = 0;
201
199
  let stopRequested = false;
202
200
 
203
201
  function readRunnerHeartbeatConfig() {
@@ -246,7 +244,7 @@ function stopRunnerHeartbeat() {
246
244
 
247
245
  function runnerHeartbeatErrorCode(error) {
248
246
  const code = error && typeof error.code === 'string' ? error.code : '';
249
- if (/^(?:E[A-Z0-9_]+|ERR_[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+)$/.test(code)) return code;
247
+ if (/^(?:E[A-Z0-9_]+|ERR_[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+|http_[0-9]{3}|redirect_[0-9]{3}|protocol_error)$/.test(code)) return code;
250
248
  const name = error && typeof error.name === 'string' ? error.name : '';
251
249
  if (name === 'AbortError' || name === 'TimeoutError') return name;
252
250
  return 'unknown';
@@ -263,10 +261,6 @@ function startRunnerHeartbeatSupervisor() {
263
261
  const handoffAt = Number(handoff && handoff.lastHeartbeatAtMs);
264
262
  heartbeatLastSuccessAtMs = Number.isFinite(handoffAt) ? handoffAt : Date.now();
265
263
  const cadenceMs = Math.max(250, Math.floor(config.leaseMs / ${RUNTIME_CLAIM_HEARTBEAT_DIVISOR}));
266
- heartbeatStopBeforeLeaseMs = Math.max(
267
- 0,
268
- config.leaseMs - cadenceMs - ${RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS},
269
- );
270
264
  process.stderr.write(
271
265
  '[play-runner.heartbeat] watchdog_owner_started ' +
272
266
  JSON.stringify({ runId: config.runId, cadenceMs, handoffHeartbeatAgeMs: Math.max(0, Date.now() - heartbeatLastSuccessAtMs) }) +
@@ -325,35 +319,33 @@ async function runnerHeartbeatTick() {
325
319
  throw Object.assign(new Error('gateway_protocol_error'), { code: 'protocol_error' });
326
320
  }
327
321
  heartbeatLastSuccessAtMs = Date.now();
328
- heartbeatLastErrorCode = null;
329
322
  } catch (error) {
330
323
  if (heartbeatStopped || settled) return;
331
- heartbeatLastErrorCode = controller.signal.aborted
324
+ const errorCode = controller.signal.aborted
332
325
  ? 'TimeoutError'
333
326
  : runnerHeartbeatErrorCode(error);
334
327
  const heartbeatAgeMs = Math.max(0, Date.now() - heartbeatLastSuccessAtMs);
335
328
  process.stderr.write(
336
329
  '[play-runner.heartbeat] watchdog_owner_retrying ' +
337
- JSON.stringify({ runId: config.runId, attempt: heartbeatAttempt, durationMs: Math.max(0, Date.now() - startedAt), heartbeatAgeMs, errorCode: heartbeatLastErrorCode }) +
330
+ JSON.stringify({ runId: config.runId, attempt: heartbeatAttempt, durationMs: Math.max(0, Date.now() - startedAt), heartbeatAgeMs, errorCode }) +
338
331
  '\\n',
339
332
  );
340
- // Stop before the scheduler's lease expires. A failed request can consume
341
- // its full timeout, and the next cadence would otherwise let the scheduler
342
- // win the terminal race and report an ambiguous runner loss.
343
- if (heartbeatAgeMs >= heartbeatStopBeforeLeaseMs) {
333
+ // The gateway answered definitively: this token is invalid (401), out of
334
+ // scope (403), or its attempt is no longer the run's current one (404).
335
+ // Nothing else stops a superseded attempt's paid calls, so stop now.
336
+ const rejection = /^http_(401|403|404)$/.exec(errorCode);
337
+ if (rejection) {
344
338
  stopRunnerHeartbeat();
345
- const diagnostic = {
346
- heartbeatAttempt,
347
- heartbeatAgeMs,
348
- leaseMs: config.leaseMs,
349
- stopBeforeLeaseMs: heartbeatStopBeforeLeaseMs,
350
- lastErrorCode: heartbeatLastErrorCode,
351
- };
339
+ const diagnostic = { heartbeatAttempt, status: Number(rejection[1]) };
352
340
  process.stderr.write(
353
- 'RUNTIME_CONTROL_PLANE_UNAVAILABLE: ' + JSON.stringify(diagnostic) + '\\n',
341
+ '[play-runner.heartbeat] watchdog_owner_rejected ' + JSON.stringify(diagnostic) + '\\n',
354
342
  );
355
- stopChild('runner_heartbeat_unreachable', diagnostic);
343
+ stopChild('runner_heartbeat_rejected', diagnostic);
356
344
  }
345
+ // Otherwise keep renewing. An unreachable or failing control plane is not
346
+ // evidence that this run is over: the scheduler owns the liveness verdict
347
+ // and confirms it only after it can accept heartbeats again. The runtime
348
+ // limit, the terminal channel above, and sandbox cleanup bound this process.
357
349
  } finally {
358
350
  clearTimeout(timeout);
359
351
  if (heartbeatController === controller) heartbeatController = null;
@@ -45,6 +45,7 @@ import {
45
45
  } from '../app-runtime-api';
46
46
  import { RUNTIME_CAPACITY_POLICY } from '../runtime-capacity-policy';
47
47
  import { RUNTIME_RELIABILITY_POLICY } from '../runtime-reliability-policy';
48
+ import { RUNTIME_CLAIM_HEARTBEAT_DIVISOR } from '../runtime-constants';
48
49
  import type { RuntimeTrafficPolicy } from '../runtime-traffic-policy';
49
50
  import { runtimeLimitExceededCause } from '../run-failure';
50
51
  import {
@@ -736,7 +737,7 @@ export class AbsurdExecutionOperationTimeoutError extends Error {
736
737
  * park/resume seam without a real engine. */
737
738
  export type AbsurdSuspensionContext = Pick<
738
739
  TaskContext,
739
- 'sleepFor' | 'sleepUntil' | 'awaitEvent' | 'heartbeat'
740
+ 'sleepFor' | 'sleepUntil' | 'awaitEvent' | 'heartbeat' | 'step'
740
741
  >;
741
742
 
742
743
  /** Injected DB + app-runtime seams so the handler is testable against real SQL
@@ -1284,6 +1285,46 @@ function signalData(
1284
1285
  return null;
1285
1286
  }
1286
1287
 
1288
+ const LIVENESS_CONFIRMATION_SLICES_PER_CADENCE = 4;
1289
+
1290
+ /**
1291
+ * A stale heartbeat proves a lost runner only if the runner could have renewed
1292
+ * it. During a scheduler-store stall no runner can, and the worker's own
1293
+ * scheduling writes (`schedule_run` NOTIFYs) stall with it. So the worker
1294
+ * counts only short sleep slices that woke on time, and decides after two
1295
+ * heartbeat cadences of such observed availability. A late slice counts for
1296
+ * nothing. Slice times and lateness are checkpointed, so replay is
1297
+ * deterministic. Returns true when the runner stayed silent or the hard
1298
+ * ceiling arrived, false as soon as it renewed or pushed its terminal.
1299
+ */
1300
+ async function confirmDetachedRunnerSilence(input: {
1301
+ ctx: AbsurdSuspensionContext;
1302
+ stepPrefix: string;
1303
+ cadenceMs: number;
1304
+ ceilingDeadlineMs: number;
1305
+ runnerResponded: () => Promise<boolean>;
1306
+ }): Promise<boolean> {
1307
+ const sliceMs = Math.floor(
1308
+ input.cadenceMs / LIVENESS_CONFIRMATION_SLICES_PER_CADENCE,
1309
+ );
1310
+ let observedMs = 0;
1311
+ for (let slice = 1; observedMs < 2 * input.cadenceMs; slice += 1) {
1312
+ const stepName = `${input.stepPrefix}:${slice}`;
1313
+ const wakeAtMs = await input.ctx.step(`${stepName}:at`, async () =>
1314
+ Math.min(input.ceilingDeadlineMs, Date.now() + sliceMs),
1315
+ );
1316
+ await input.ctx.sleepUntil(stepName, new Date(wakeAtMs));
1317
+ if (await input.runnerResponded()) return false;
1318
+ if (wakeAtMs >= input.ceilingDeadlineMs) return true;
1319
+ const lateMs = await input.ctx.step(
1320
+ `${stepName}:late`,
1321
+ async () => Date.now() - wakeAtMs,
1322
+ );
1323
+ if (lateMs <= sliceMs) observedMs += sliceMs;
1324
+ }
1325
+ return true;
1326
+ }
1327
+
1287
1328
  /**
1288
1329
  * Map a runner suspension onto an absurd durable primitive and return the
1289
1330
  * checkpoint with the resolved boundary output. THROWS `SuspendTask` when the
@@ -1382,9 +1423,30 @@ export async function parkAndResolveSuspension(input: {
1382
1423
  (Number.isFinite(renewedAtMs) ? renewedAtMs : suspension.startedAtMs) +
1383
1424
  heartbeatTimeoutMs,
1384
1425
  );
1385
- if (renewedDeadlineMs <= Date.now() || renewedDeadlineMs === deadlineMs) {
1386
- break;
1426
+ if (renewedDeadlineMs > Date.now()) {
1427
+ if (renewedDeadlineMs === deadlineMs) break;
1428
+ continue;
1387
1429
  }
1430
+ if (ceilingDeadlineMs <= Date.now()) break;
1431
+ const readRunnerTerminal = input.readRunnerTerminal;
1432
+ const readRunnerLiveness = input.readRunnerLiveness;
1433
+ const silent = await confirmDetachedRunnerSilence({
1434
+ ctx,
1435
+ stepPrefix: `${suspension.boundaryId}:liveness-confirm:${Number.isFinite(renewedAtMs) ? renewedAtMs : 'none'}`,
1436
+ cadenceMs: Math.floor(
1437
+ heartbeatTimeoutMs / RUNTIME_CLAIM_HEARTBEAT_DIVISOR,
1438
+ ),
1439
+ ceilingDeadlineMs,
1440
+ runnerResponded: async () => {
1441
+ if (await readRunnerTerminal(suspension.runnerAttempt)) return true;
1442
+ const current = await readRunnerLiveness(suspension.runnerAttempt);
1443
+ const currentAtMs = current?.heartbeatAt
1444
+ ? Date.parse(current.heartbeatAt)
1445
+ : Number.NaN;
1446
+ return Number.isFinite(currentAtMs) && !(currentAtMs <= renewedAtMs);
1447
+ },
1448
+ });
1449
+ if (silent) break;
1388
1450
  }
1389
1451
  // Truth is the persisted terminal (read by the caller's wake path); the
1390
1452
  // absurd event carries no payload we trust.
@@ -53,6 +53,101 @@ export type EmailStatus = {
53
53
  };
54
54
  };
55
55
 
56
+ const nullable = (type: string) => ({ type: [type, 'null'] });
57
+ const oneOf = (values: readonly string[]) => ({ type: 'string', enum: values });
58
+
59
+ /** `EmailStatus` as a JSON schema, for describing it where TypeScript types are not at hand. */
60
+ export const EMAIL_STATUS_JSON_SCHEMA = {
61
+ type: 'object',
62
+ required: [
63
+ 'verdict',
64
+ 'status',
65
+ 'verified',
66
+ 'confidence',
67
+ 'reasons',
68
+ 'signals',
69
+ 'provider',
70
+ ],
71
+ properties: {
72
+ verdict: oneOf([
73
+ 'send',
74
+ 'send_with_caution',
75
+ 'verify_next',
76
+ 'hold',
77
+ 'drop',
78
+ ] satisfies EmailStatusVerdict[]),
79
+ status: oneOf([
80
+ 'valid',
81
+ 'invalid',
82
+ 'catch_all',
83
+ 'valid_catch_all',
84
+ 'unknown',
85
+ 'do_not_mail',
86
+ 'spamtrap',
87
+ 'abuse',
88
+ 'disposable',
89
+ ] satisfies EmailStatusValue[]),
90
+ verified: { type: 'boolean' },
91
+ confidence: nullable('number'),
92
+ reasons: { type: 'array', items: { type: 'string' } },
93
+ signals: {
94
+ type: 'object',
95
+ required: [
96
+ 'catch_all',
97
+ 'deliverability',
98
+ 'mx_class',
99
+ 'mx_provider',
100
+ 'mx_record',
101
+ 'fraud_score',
102
+ 'disposable',
103
+ 'role_based',
104
+ 'free_email',
105
+ 'abuse',
106
+ 'spamtrap',
107
+ 'suspect',
108
+ 'valid',
109
+ ] satisfies Array<keyof EmailStatus['signals']>,
110
+ properties: {
111
+ catch_all: nullable('boolean'),
112
+ deliverability: oneOf([
113
+ 'high',
114
+ 'medium',
115
+ 'low',
116
+ 'unknown',
117
+ ] satisfies EmailDeliverability[]),
118
+ mx_class: oneOf([
119
+ 'consumer_mailbox',
120
+ 'workspace_mailbox',
121
+ 'security_gateway',
122
+ 'on_prem',
123
+ 'unknown',
124
+ ] satisfies EmailMxClass[]),
125
+ mx_provider: nullable('string'),
126
+ mx_record: nullable('string'),
127
+ fraud_score: nullable('number'),
128
+ disposable: nullable('boolean'),
129
+ role_based: nullable('boolean'),
130
+ free_email: nullable('boolean'),
131
+ abuse: nullable('boolean'),
132
+ spamtrap: nullable('boolean'),
133
+ suspect: nullable('boolean'),
134
+ valid: nullable('boolean'),
135
+ },
136
+ },
137
+ provider: {
138
+ type: 'object',
139
+ required: ['name', 'raw_status', 'raw_score'] satisfies Array<
140
+ keyof EmailStatus['provider']
141
+ >,
142
+ properties: {
143
+ name: { type: 'string' },
144
+ raw_status: { type: ['string', 'boolean', 'number', 'null'] },
145
+ raw_score: nullable('number'),
146
+ },
147
+ },
148
+ },
149
+ } as const;
150
+
56
151
  export type EmailStatusMapEntry = {
57
152
  status: EmailStatusValue;
58
153
  verdict?: EmailStatusVerdict;
@@ -18,6 +18,20 @@ export type JobChangeGetterValue = {
18
18
  new_title: string | null;
19
19
  };
20
20
 
21
+ /** `JobChangeGetterValue` as a JSON schema. */
22
+ export const JOB_CHANGE_JSON_SCHEMA = {
23
+ type: 'object',
24
+ required: ['status', 'date', 'new_company', 'new_title'] satisfies Array<
25
+ keyof JobChangeGetterValue
26
+ >,
27
+ properties: {
28
+ status: { type: 'string', enum: JOB_CHANGE_STATUS_VALUES },
29
+ date: { type: ['string', 'null'] },
30
+ new_company: { type: ['string', 'null'] },
31
+ new_title: { type: ['string', 'null'] },
32
+ },
33
+ } as const;
34
+
21
35
  export const PHONE_STATUS_VALUES = ['valid', 'invalid', 'unknown'] as const;
22
36
 
23
37
  export type PhoneStatus = (typeof PHONE_STATUS_VALUES)[number];
@@ -0,0 +1,253 @@
1
+ import type { DeeplineExtractorTarget } from './extractor-targets';
2
+
3
+ /**
4
+ * The catalog of things a tool or play can take or return about a person or
5
+ * a company, named by what they are instead of by what a provider calls them.
6
+ *
7
+ * An id is a dotted path that always ends in a leaf:
8
+ *
9
+ * person.email.value any email for the person
10
+ * person.email.work.value their work email
11
+ * person.email.work.validated whether that work email was validated
12
+ *
13
+ * It reads `<entity>.<group>[.<kind>].<leaf>`. A group is one thing about the
14
+ * entity (an email). A leaf is one fact about that thing (its value, whether
15
+ * it was validated). A kind narrows which one is meant (work, personal). No
16
+ * id is both a field and a parent of fields, so `person.email` alone is never
17
+ * a field; the plain value is `person.email.value`.
18
+ *
19
+ * A kind is a narrowing, so a field with a kind also answers for the same
20
+ * field without it: a tool that returns `person.email.work.value` satisfies a
21
+ * request for `person.email.value`. A tool that cannot tell which kind it has
22
+ * declares the id without one.
23
+ *
24
+ * This is an addition. Existing short names (`email`, `name`) keep working;
25
+ * `EXTRACTOR_TARGET_FIELDS` records what each can mean.
26
+ */
27
+ type Leaf = { type: string; description: string };
28
+
29
+ type Group = {
30
+ description: string;
31
+ leaves: Record<string, Leaf>;
32
+ /** Narrowings of this group. Each has every leaf the group has. */
33
+ kinds?: Record<string, string>;
34
+ };
35
+
36
+ const value = (description: string): Leaf => ({ type: 'string', description });
37
+
38
+ const SOURCE: Leaf = {
39
+ type: 'string',
40
+ description: 'The provider or step that found it.',
41
+ };
42
+
43
+ export const FIELD_CATALOG = {
44
+ person: {
45
+ name: {
46
+ description: "The person's name.",
47
+ leaves: {
48
+ value: value('Full name.'),
49
+ first: value('First (given) name.'),
50
+ last: value('Last (family) name.'),
51
+ },
52
+ },
53
+ email: {
54
+ description: 'An email address for the person.',
55
+ leaves: {
56
+ value: value('The address.'),
57
+ validated: {
58
+ type: 'boolean',
59
+ description: 'Whether the address passed validation.',
60
+ },
61
+ status: {
62
+ type: 'EmailStatus',
63
+ description: 'The validation verdict and its signals.',
64
+ },
65
+ source: SOURCE,
66
+ },
67
+ kinds: {
68
+ work: 'At their employer.',
69
+ personal: 'Their own, not tied to an employer.',
70
+ },
71
+ },
72
+ phone: {
73
+ description: 'A phone number for the person.',
74
+ leaves: {
75
+ value: value('The number.'),
76
+ validated: {
77
+ type: 'boolean',
78
+ description: 'Whether the number passed validation.',
79
+ },
80
+ status: {
81
+ type: '"valid" | "invalid" | "unknown"',
82
+ description: 'The validation verdict.',
83
+ },
84
+ line_type: value('Mobile, landline, VoIP and so on.'),
85
+ carrier: value('The carrier serving the number.'),
86
+ source: SOURCE,
87
+ },
88
+ kinds: {
89
+ mobile: 'Their mobile.',
90
+ work: 'A direct line at their employer.',
91
+ },
92
+ },
93
+ linkedin: {
94
+ description: "The person's LinkedIn profile.",
95
+ leaves: { url: value('The profile URL.') },
96
+ },
97
+ job: {
98
+ description: "The person's current role.",
99
+ leaves: {
100
+ title: value('Job title.'),
101
+ change: {
102
+ type: 'JobChange',
103
+ description: 'Whether they changed jobs, and to what.',
104
+ },
105
+ change_status: {
106
+ type: 'JobChangeStatus',
107
+ description: 'The job-change verdict alone.',
108
+ },
109
+ },
110
+ },
111
+ record: {
112
+ description: "A provider's own record of the person.",
113
+ leaves: { id: value("The provider's id for the person.") },
114
+ },
115
+ },
116
+ company: {
117
+ name: {
118
+ description: "The company's name.",
119
+ leaves: { value: value('The name.') },
120
+ kinds: { legal: 'As registered with a government.' },
121
+ },
122
+ domain: {
123
+ description: "The company's web domain.",
124
+ leaves: { value: value('The bare domain, such as example.com.') },
125
+ },
126
+ website: {
127
+ description: "The company's website.",
128
+ leaves: { url: value('The site URL.') },
129
+ },
130
+ linkedin: {
131
+ description: "The company's LinkedIn page.",
132
+ leaves: { url: value('The page URL.') },
133
+ },
134
+ phone: {
135
+ description: 'A phone number for the company.',
136
+ leaves: { value: value('The number.') },
137
+ },
138
+ industry: {
139
+ description: 'The industry the company is in.',
140
+ leaves: { value: value('The industry.') },
141
+ },
142
+ record: {
143
+ description: "A provider's own record of the company.",
144
+ leaves: { id: value("The provider's id for the company.") },
145
+ },
146
+ },
147
+ } as const satisfies Record<string, Record<string, Group>>;
148
+
149
+ export type FieldEntity = keyof typeof FIELD_CATALOG;
150
+
151
+ export type CatalogField = Leaf & {
152
+ id: string;
153
+ entity: FieldEntity;
154
+ group: string;
155
+ kind?: string;
156
+ leaf: string;
157
+ };
158
+
159
+ /** Every field in the catalog, with and without each kind. */
160
+ export function catalogFields(): CatalogField[] {
161
+ return Object.entries(FIELD_CATALOG).flatMap(([entity, groups]) =>
162
+ Object.entries<Group>(groups).flatMap(([group, definition]) =>
163
+ [undefined, ...Object.keys(definition.kinds ?? {})].flatMap((kind) =>
164
+ Object.entries(definition.leaves).map(([leaf, described]) => ({
165
+ ...described,
166
+ id: [entity, group, kind, leaf].filter(Boolean).join('.'),
167
+ entity: entity as FieldEntity,
168
+ group,
169
+ ...(kind ? { kind } : {}),
170
+ leaf,
171
+ })),
172
+ ),
173
+ ),
174
+ );
175
+ }
176
+
177
+ const FIELDS_BY_ID = new Map(catalogFields().map((field) => [field.id, field]));
178
+
179
+ export function catalogField(id: string): CatalogField | undefined {
180
+ return FIELDS_BY_ID.get(id);
181
+ }
182
+
183
+ /** The same field without its kind, or null when it has none to drop. */
184
+ export function generalField(id: string): string | null {
185
+ const field = FIELDS_BY_ID.get(id);
186
+ return field?.kind ? [field.entity, field.group, field.leaf].join('.') : null;
187
+ }
188
+
189
+ /** Whether a tool returning `offered` gives a caller what `wanted` asks for. */
190
+ export function fieldSatisfies(offered: string, wanted: string): boolean {
191
+ return offered === wanted || generalField(offered) === wanted;
192
+ }
193
+
194
+ /**
195
+ * What each existing short output name can mean, as the general field: a tool
196
+ * may also declare any narrowing of one (`email` as `person.email.work.value`).
197
+ * One entry means the name means that everywhere unless a tool narrows it.
198
+ * Several mean it depends on the tool, which has to say. None means the name
199
+ * is used for things outside the catalog and stays a per-tool name.
200
+ */
201
+ export const EXTRACTOR_TARGET_FIELDS: Record<
202
+ DeeplineExtractorTarget,
203
+ readonly string[]
204
+ > = {
205
+ email: ['person.email.value'],
206
+ personal_email: ['person.email.personal.value'],
207
+ email_status: ['person.email.status'],
208
+ phone: ['person.phone.value', 'company.phone.value'],
209
+ phone_status: ['person.phone.status'],
210
+ first_name: ['person.name.first'],
211
+ last_name: ['person.name.last'],
212
+ full_name: ['person.name.value'],
213
+ // Also used for a list name, a channel title and a search suggestion.
214
+ name: ['person.name.value', 'company.name.value'],
215
+ linkedin: ['person.linkedin.url', 'company.linkedin.url'],
216
+ linkedin_url: ['person.linkedin.url'],
217
+ title: ['person.job.title'],
218
+ job_change: ['person.job.change'],
219
+ job_change_status: ['person.job.change_status'],
220
+ company: ['company.name.value'],
221
+ company_name: ['company.name.value'],
222
+ organization_name: ['company.name.value'],
223
+ company_domain: ['company.domain.value'],
224
+ domain: ['company.domain.value'],
225
+ company_website: ['company.website.url'],
226
+ // Also used for a podcast URL.
227
+ website: ['company.website.url'],
228
+ company_linkedin_url: ['company.linkedin.url'],
229
+ industry: ['company.industry.value'],
230
+ id: ['person.record.id', 'company.record.id'],
231
+ // Used for unrelated provider statuses; none is a catalog field.
232
+ status: [],
233
+ };
234
+
235
+ /**
236
+ * The catalog field a short name stands for on one tool: what the tool
237
+ * declares, or else the name's one meaning. Undefined when the name is
238
+ * ambiguous and the tool has not said, or when what it said is not something
239
+ * the name can mean.
240
+ */
241
+ export function fieldOfName(
242
+ name: string,
243
+ declared?: string,
244
+ ): string | undefined {
245
+ const meanings: readonly string[] =
246
+ EXTRACTOR_TARGET_FIELDS[name as DeeplineExtractorTarget] ?? [];
247
+ if (declared === undefined) {
248
+ return meanings.length === 1 ? meanings[0] : undefined;
249
+ }
250
+ return meanings.some((meaning) => fieldSatisfies(declared, meaning))
251
+ ? declared
252
+ : undefined;
253
+ }
@@ -55,6 +55,8 @@ export type ToolResultListMetadata = {
55
55
 
56
56
  export type ToolResultExtractorDescriptor = {
57
57
  paths: readonly string[];
58
+ /** The field catalog id this output is on this tool, when it declares one. */
59
+ means?: string;
58
60
  transforms?: readonly string[];
59
61
  enum?: readonly string[];
60
62
  overrides?: readonly ToolResultExtractorOverride[];