deepline 0.3.195 → 0.3.197

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 (29) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +13 -0
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/types.ts +18 -0
  4. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  5. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +10 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +145 -29
  8. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +138 -14
  9. package/dist/bundling-sources/shared_libs/plays/docflow.ts +57 -3
  10. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +3 -0
  11. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +21 -0
  12. package/dist/bundling-sources/shared_libs/plays/workspace-bundle.ts +651 -0
  13. package/dist/cli/index.js +48 -2
  14. package/dist/cli/index.mjs +48 -2
  15. package/dist/{compiler-manifest-4bYusxBr.d.mts → compiler-manifest-Dpd2hEet.d.mts} +28 -0
  16. package/dist/{compiler-manifest-4bYusxBr.d.ts → compiler-manifest-Dpd2hEet.d.ts} +28 -0
  17. package/dist/index.d.mts +27 -2
  18. package/dist/index.d.ts +27 -2
  19. package/dist/index.js +7 -1
  20. package/dist/index.mjs +7 -1
  21. package/dist/install-integrity.json +3 -2
  22. package/dist/plays/bundle-play-file.d.mts +2 -2
  23. package/dist/plays/bundle-play-file.d.ts +2 -2
  24. package/dist/plays/bundle-play-file.mjs +24 -3
  25. package/dist/release.d.mts +1 -1
  26. package/dist/release.d.ts +1 -1
  27. package/dist/release.js +1 -1
  28. package/dist/release.mjs +1 -1
  29. package/package.json +1 -1
@@ -793,6 +793,13 @@ export type PlaysListOptions = {
793
793
  */
794
794
  grepMode?: 'all' | 'any' | 'phrase';
795
795
  categories?: string | string[];
796
+ /**
797
+ * Keep only plays carrying one of these tags — the job a play does, not the
798
+ * tools it calls. Deepline's closed vocabulary today: `smb`,
799
+ * `find-prospects`, `qualify`, `contact-info`, `signals`. Several tags widen
800
+ * the result rather than narrowing it, because a play carries exactly one.
801
+ */
802
+ tags?: string | string[];
796
803
  includeToolCategories?: boolean;
797
804
  includeArchived?: boolean;
798
805
  includeCostEstimates?: boolean;
@@ -5420,6 +5427,12 @@ export class DeeplineClient {
5420
5427
  if (options?.categories || options?.includeToolCategories) {
5421
5428
  params.set('include_tool_categories', '1');
5422
5429
  }
5430
+ if (options?.tags) {
5431
+ params.set(
5432
+ 'tags',
5433
+ Array.isArray(options.tags) ? options.tags.join(',') : options.tags,
5434
+ );
5435
+ }
5423
5436
  if (options?.includeArchived) params.set('include_archived', '1');
5424
5437
  if (options?.includeCostEstimates) {
5425
5438
  params.set('include_cost_estimates', '1');
@@ -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.195',
203
+ version: '0.3.197',
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: {
@@ -1378,6 +1378,14 @@ export interface PlayListItem {
1378
1378
  description?: string | null;
1379
1379
  pinned?: boolean;
1380
1380
  toolCategories?: string[];
1381
+ /**
1382
+ * The job the play does, from Deepline's closed prebuilt tag vocabulary
1383
+ * (`smb`, `find-prospects`, `qualify`, `contact-info`, `signals`) — a
1384
+ * different question from `toolCategories`, which reports the tools it
1385
+ * calls. Empty for org-owned plays; absent from a server too old to report
1386
+ * it.
1387
+ */
1388
+ tags?: string[];
1381
1389
  origin?: 'prebuilt' | 'owned';
1382
1390
  ownerType?: 'deepline' | 'org';
1383
1391
  ownerSlug?: string;
@@ -1469,6 +1477,16 @@ export interface PlayDescription {
1469
1477
  description?: string | null;
1470
1478
  pinned?: boolean;
1471
1479
  toolCategories?: string[];
1480
+ /**
1481
+ * What job this play does, as a closed vocabulary — `smb`,
1482
+ * `find-prospects`, `qualify`, `contact-info`, `signals`.
1483
+ *
1484
+ * Distinct from `toolCategories`, which is derived from the tool ids a play
1485
+ * calls and so describes its MACHINERY. A play that calls an email tool is
1486
+ * not thereby an email play; every SMB play calls one and none of them is.
1487
+ * Empty for org-owned plays, which carry no registry tag.
1488
+ */
1489
+ tags?: string[];
1472
1490
  origin?: 'prebuilt' | 'owned';
1473
1491
  ownerType?: 'deepline' | 'org';
1474
1492
  canEdit?: boolean;
@@ -885,6 +885,24 @@
885
885
  "failureAlert": "convex_action_wrapper",
886
886
  "domainReportRequired": true
887
887
  },
888
+ {
889
+ "id": "convex.cleanup-expired-api-key-claims",
890
+ "name": "Cleanup expired API key claims",
891
+ "scheduler": "convex_cron",
892
+ "cadence": "7 4 * * *",
893
+ "timezone": "UTC",
894
+ "source": "packages/convex/crons.ts",
895
+ "locator": "cleanup expired API key claims",
896
+ "owner": "auth",
897
+ "criticality": "maintenance",
898
+ "purpose": "Delete expired CLI claim tokens from apiKeyClaims, which had no reaper while a settings page view minted one durable claim row per unclaimed active key.",
899
+ "successDefinition": "The sweep advances its monotone createdAt watermark and deletes every expired claim it examines; zero expired claims is a valid no-op.",
900
+ "failureDefinition": "The sweep fails or stops advancing its watermark, leaving expired claim credentials retained indefinitely.",
901
+ "startGraceMs": 120000,
902
+ "runTimeoutMs": 1800000,
903
+ "failureAlert": "convex_action_wrapper",
904
+ "domainReportRequired": true
905
+ },
888
906
  {
889
907
  "id": "convex.cleanup-expired-analytics-ip2company-cache",
890
908
  "name": "Cleanup expired Analytics IP2Company cache entries",
@@ -11,6 +11,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
11
11
  'convex.fold-usage-rollups',
12
12
  'convex.record-neon-managed-project-usage',
13
13
  'convex.cleanup-expired-mcp-oauth-codes',
14
+ 'convex.cleanup-expired-api-key-claims',
14
15
  'convex.cleanup-expired-analytics-ip2company-cache',
15
16
  'convex.cleanup-expired-domain-health-cache',
16
17
  'convex.salesforce-oauth-refresh-maintenance',
@@ -72,7 +72,6 @@ import {
72
72
  } from '../runtime-api';
73
73
  import { PLAY_RUNTIME_ATTEMPT_EXECUTOR_TOKEN_PATH } from '../runtime-api-paths';
74
74
  import { vercelProtectionBypassHeaders } from '../vercel-protection';
75
- import { installPgRuntimePoolDriver } from '../runtime-pg-driver-pg';
76
75
  import {
77
76
  isManagedSandboxRuntimeBackend,
78
77
  normalizePlayRuntimeBackend,
@@ -302,7 +301,16 @@ class AttemptExecutorTokenCapacityError extends RuntimeSchedulerCapacityError {
302
301
  this.name = 'AttemptExecutorTokenCapacityError';
303
302
  }
304
303
  }
305
- installPgRuntimePoolDriver();
304
+
305
+ // No `installPgRuntimePoolDriver()` here: the runtime Postgres driver belongs to
306
+ // the process that owns this module's boot — `apps/absurd-runtime-worker` and
307
+ // `apps/absurd-release-scaler` install it at their entrypoints. `apps/deepline-api`
308
+ // imports the lane-count helpers below from a request path
309
+ // (`src/lib/plays/runtime-lanes.ts`), and the Next.js dev server re-evaluates that
310
+ // module graph on every recompile while `registerRuntimePoolFactory`'s registry
311
+ // lives on `globalThis` under a `Symbol.for` key and survives. A module-scope
312
+ // install therefore handed the registry a fresh factory object after each
313
+ // recompile and tripped its one-driver-per-process assertion.
306
314
 
307
315
  /**
308
316
  * A slot supervises one remote Daytona sandbox; it is not a local compute
@@ -62,6 +62,7 @@ import {
62
62
  type RuntimeReceiptValueReference,
63
63
  } from '../runtime-receipt-value';
64
64
  import { PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS } from '../lease-policy';
65
+ import { RUNTIME_CAPACITY_POLICY } from '../runtime-capacity-policy';
65
66
  import { RUNTIME_RELIABILITY_POLICY } from '../runtime-reliability-policy';
66
67
  import { COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID } from '../durable-receipt-execution';
67
68
  import {
@@ -159,6 +160,7 @@ import { requireRuntimeAuthorityDescriptor } from '../execution-capabilities';
159
160
  import { stringifyPostgresJson } from '../postgres-json';
160
161
  import {
161
162
  buildPlayLaunchIdentity,
163
+ PLAY_LAUNCH_ENVELOPE_FIELD_ROLES,
162
164
  withPlayLaunchIdentityDigest,
163
165
  } from '../launch-identity';
164
166
  import { PlayRunLaunchConflictError } from '../play-runs/admission/contracts';
@@ -173,7 +175,10 @@ import {
173
175
  recordSchedulerLaunchRead,
174
176
  type SchedulerLaunchReadCaller,
175
177
  } from './scheduler-launch-read-observability';
176
- import { emitPlayTelemetry } from '../runtime-telemetry';
178
+ import {
179
+ emitPlayRunLaunchConflictTelemetry,
180
+ emitPlayTelemetry,
181
+ } from '../runtime-telemetry';
177
182
  import { buildAbsurdPlayRunTask } from './absurd-play-run-task';
178
183
  import {
179
184
  buildAbsurdCronFireTask,
@@ -4509,6 +4514,38 @@ export async function submitPostgresAbsurdPlayRun(
4509
4514
  };
4510
4515
  }
4511
4516
 
4517
+ function isJsonObject(value: unknown): value is Record<string, unknown> {
4518
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4519
+ }
4520
+
4521
+ /**
4522
+ * Reasons this build cannot prove a stored launch has the incoming named
4523
+ * intent. Empty only when the stored envelope is readable, contains no field
4524
+ * this build does not know (a newer writer may have added identity), and
4525
+ * every named intent field matches.
4526
+ */
4527
+ function launchIntentRematchBlockers(
4528
+ existingLaunchJson: unknown,
4529
+ incomingLaunch: PlaySchedulerSubmitInput,
4530
+ ): string[] {
4531
+ if (!isJsonObject(existingLaunchJson)) return ['launch'];
4532
+ const unknownFields = Object.keys(existingLaunchJson).filter(
4533
+ (field) =>
4534
+ !Object.prototype.hasOwnProperty.call(
4535
+ PLAY_LAUNCH_ENVELOPE_FIELD_ROLES,
4536
+ field,
4537
+ ),
4538
+ );
4539
+ if (unknownFields.length > 0) return unknownFields.sort();
4540
+ return launchIdentityConflictFields(existingLaunchJson, incomingLaunch);
4541
+ }
4542
+
4543
+ function launchIdentityDigestOf(launchJson: unknown): string | null {
4544
+ if (!isJsonObject(launchJson)) return null;
4545
+ const digest = launchJson.launchIdentityDigest;
4546
+ return typeof digest === 'string' && digest.trim() ? digest.trim() : null;
4547
+ }
4548
+
4512
4549
  function launchIdentityConflictFields(
4513
4550
  existingLaunchJson: unknown,
4514
4551
  incomingLaunch: PlaySchedulerSubmitInput,
@@ -4559,12 +4596,18 @@ async function submitPostgresSchedulerRunTransaction(
4559
4596
  options: unknown;
4560
4597
  recoverExistingTask?: boolean;
4561
4598
  },
4599
+ /**
4600
+ * Set only on the single re-admission after the current build proved the
4601
+ * existing Run has the same named launch intent (see the conflict branch).
4602
+ */
4603
+ existingLaunchIntentMatches = false,
4562
4604
  ): Promise<{
4563
4605
  absurdTaskId: string | null;
4564
4606
  runCreated: boolean;
4565
4607
  taskCreated: boolean;
4566
4608
  taskRecoveryRequired: boolean;
4567
4609
  }> {
4610
+ const requestedInput = input;
4568
4611
  // Persist the ABI with every new launch. Null is reserved for genuinely
4569
4612
  // historic rows already in storage, where readers interpret it as legacy.
4570
4613
  input = {
@@ -4591,6 +4634,9 @@ async function submitPostgresSchedulerRunTransaction(
4591
4634
  // fields are removed so they can never change the durable launch identity.
4592
4635
  input = withPlayLaunchIdentityDigest(durableInput);
4593
4636
  const existingOnlySql = requestedExistingOnly === true ? 'true' : 'false';
4637
+ const existingLaunchIntentMatchesSql = existingLaunchIntentMatches
4638
+ ? 'true'
4639
+ : 'false';
4594
4640
  const releaseId = input.absurdReleaseId?.trim() || null;
4595
4641
  const legacyPreviewRelease =
4596
4642
  releaseId === 'dev' || releaseId?.startsWith('dev-') === true;
@@ -4717,9 +4763,12 @@ async function submitPostgresSchedulerRunTransaction(
4717
4763
  FOR UPDATE
4718
4764
  ), existing_run AS (
4719
4765
  SELECT
4720
- ${quoteIdent(schemaName(options))}.runtime_launch_admission_matches(
4721
- coalesce(runs.admission_json, runs.launch_json),
4722
- $8::jsonb
4766
+ (
4767
+ ${existingLaunchIntentMatchesSql}
4768
+ OR ${quoteIdent(schemaName(options))}.runtime_launch_admission_matches(
4769
+ coalesce(runs.admission_json, runs.launch_json),
4770
+ $8::jsonb
4771
+ )
4723
4772
  ) AS matches_launch,
4724
4773
  coalesce(runs.admission_json, runs.launch_json) AS launch_json,
4725
4774
  runs.absurd_task_id,
@@ -4985,13 +5034,42 @@ async function submitPostgresSchedulerRunTransaction(
4985
5034
  );
4986
5035
  }
4987
5036
  if (row?.accepted === false && row.existing_run && !row.matching_existing) {
4988
- throw new PlayRunLaunchConflictError(input.runId, {
4989
- conflictReason: 'launch_identity_mismatch',
4990
- conflictingFields: launchIdentityConflictFields(
4991
- row.existing_launch_json,
4992
- input,
4993
- ),
5037
+ const rematchBlockers = launchIntentRematchBlockers(
5038
+ row.existing_launch_json,
5039
+ input,
5040
+ );
5041
+ if (existingLaunchIntentMatches || rematchBlockers.length > 0) {
5042
+ throw new PlayRunLaunchConflictError(input.runId, {
5043
+ conflictReason: 'launch_identity_mismatch',
5044
+ conflictingFields: rematchBlockers,
5045
+ });
5046
+ }
5047
+ // The stored envelope was written by another build (no digest, or a
5048
+ // digest this build cannot reproduce), but every named intent field
5049
+ // matches. Re-admit once as a match so the Run's normal matched path,
5050
+ // including task recovery, runs instead of failing an at-least-once retry.
5051
+ emitPlayTelemetry({
5052
+ level: 'warn',
5053
+ event: 'play.run.admission_intent_rematch',
5054
+ tag: '[play.run.admission_intent_rematch]',
5055
+ fields: {
5056
+ runId: input.runId,
5057
+ orgId: input.orgId,
5058
+ playName: input.playName,
5059
+ triggerSource: input.triggerSource ?? null,
5060
+ existingLaunchIdentityDigest: launchIdentityDigestOf(
5061
+ row.existing_launch_json,
5062
+ ),
5063
+ launchIdentityDigest: input.launchIdentityDigest ?? null,
5064
+ },
4994
5065
  });
5066
+ return submitPostgresSchedulerRunTransaction(
5067
+ client,
5068
+ requestedInput,
5069
+ options,
5070
+ absurdTask,
5071
+ true,
5072
+ );
4995
5073
  }
4996
5074
  if (row?.accepted === false && !row?.existing_run && requestedExistingOnly) {
4997
5075
  throw new Error(
@@ -5683,10 +5761,11 @@ export type FirePostgresSchedulerCronScheduleInput = {
5683
5761
  * cursor and is a harmless `superseded` no-op.
5684
5762
  *
5685
5763
  * A re-fire of the same occurrence whose deterministic Run already exists is
5686
- * an idempotent re-fire, not a no-op. The cursor advances when the existing
5687
- * launch identity matches, even if this statement did not create the Run.
5688
- * A genuine identity mismatch is returned as `stalled` so it cannot silently
5689
- * freeze the schedule behind an indistinguishable replay.
5764
+ * an idempotent re-fire, not a no-op. The cursor advances over any existing
5765
+ * Run, even when a mixed-version or competing launch makes the named identity
5766
+ * comparator disagree. The persisted Run is authoritative for this
5767
+ * deterministic occurrence; a conflict is emitted for owner-visible
5768
+ * diagnosis, never converted into a permanent blocked cursor.
5690
5769
  */
5691
5770
  export async function firePostgresSchedulerCronSchedule(
5692
5771
  client: PostgresSchedulerQueryClient,
@@ -5698,6 +5777,7 @@ export async function firePostgresSchedulerCronSchedule(
5698
5777
  runId: string;
5699
5778
  successorDueAtMs: number;
5700
5779
  reused: boolean;
5780
+ conflictingFields?: string[];
5701
5781
  }
5702
5782
  | { status: 'superseded' }
5703
5783
  | { status: 'stalled'; runId: string; reason: string }
@@ -5772,6 +5852,7 @@ export async function firePostgresSchedulerCronSchedule(
5772
5852
  schedule_current: boolean;
5773
5853
  existing_run: boolean;
5774
5854
  matching_existing: boolean;
5855
+ existing_launch_json: unknown | null;
5775
5856
  release_accepting: boolean;
5776
5857
  }>(
5777
5858
  `
@@ -5797,6 +5878,7 @@ export async function firePostgresSchedulerCronSchedule(
5797
5878
  FOR UPDATE
5798
5879
  ), existing_run AS MATERIALIZED (
5799
5880
  SELECT runs.run_id,
5881
+ coalesce(runs.admission_json, runs.launch_json) AS launch_json,
5800
5882
  ${quoteIdent(schemaName(options))}.runtime_launch_admission_matches(
5801
5883
  coalesce(runs.admission_json, runs.launch_json),
5802
5884
  $6::jsonb
@@ -5864,7 +5946,6 @@ export async function firePostgresSchedulerCronSchedule(
5864
5946
  OR (
5865
5947
  occurrence.schedule_current
5866
5948
  AND occurrence.existing_run
5867
- AND occurrence.matching_existing
5868
5949
  )
5869
5950
  ), successor_task_request AS (
5870
5951
  SELECT $11::text AS queue_name,
@@ -5953,6 +6034,7 @@ export async function firePostgresSchedulerCronSchedule(
5953
6034
  (SELECT schedule_current FROM occurrence) AS schedule_current,
5954
6035
  (SELECT existing_run FROM occurrence) AS existing_run,
5955
6036
  (SELECT matching_existing FROM occurrence) AS matching_existing,
6037
+ (SELECT launch_json FROM existing_run) AS existing_launch_json,
5956
6038
  coalesce(
5957
6039
  (SELECT accepting
5958
6040
  FROM ${tableName('runtime_release_admission', options)}
@@ -5986,24 +6068,53 @@ export async function firePostgresSchedulerCronSchedule(
5986
6068
  );
5987
6069
  }
5988
6070
  if (fired.advanced === true) {
6071
+ const conflictingFields =
6072
+ fired.existing_run === true && fired.matching_existing !== true
6073
+ ? launchIntentRematchBlockers(fired.existing_launch_json, launch)
6074
+ : [];
6075
+ if (conflictingFields.length === 0 && fired.existing_run === true) {
6076
+ emitPlayTelemetry({
6077
+ level: 'warn',
6078
+ event: 'play.run.admission_intent_rematch',
6079
+ tag: '[play.run.admission_intent_rematch]',
6080
+ fields: {
6081
+ runId: launch.runId,
6082
+ orgId: launch.orgId,
6083
+ playName: launch.playName,
6084
+ triggerSource: 'cron',
6085
+ scheduleId: schedule.scheduleId,
6086
+ existingLaunchIdentityDigest: launchIdentityDigestOf(
6087
+ fired.existing_launch_json,
6088
+ ),
6089
+ launchIdentityDigest: launch.launchIdentityDigest ?? null,
6090
+ },
6091
+ });
6092
+ } else if (conflictingFields.length > 0) {
6093
+ const conflict = new Error(
6094
+ `Cron occurrence "${launch.runId}" already exists with a different launch identity: ${conflictingFields.join(', ')}`,
6095
+ );
6096
+ emitPlayRunLaunchConflictTelemetry({
6097
+ orgId: launch.orgId,
6098
+ runId: launch.runId,
6099
+ playName: launch.playName,
6100
+ triggerSource: 'cron',
6101
+ workflowFamilyKey: launch.workflowFamilyKey,
6102
+ revisionId: launch.revisionId,
6103
+ artifactHash: launch.artifactHash,
6104
+ graphHash: launch.graphHash,
6105
+ inputSha256: launch.inputSha256,
6106
+ launchIdentityDigest: launch.launchIdentityDigest,
6107
+ conflictReason: 'launch_identity_mismatch',
6108
+ conflictingFields,
6109
+ error: conflict,
6110
+ });
6111
+ }
5989
6112
  return {
5990
6113
  status: 'admitted',
5991
6114
  runId: launch.runId,
5992
6115
  successorDueAtMs,
5993
6116
  reused: fired.run_created !== true,
5994
- };
5995
- }
5996
- if (
5997
- fired.schedule_current === true &&
5998
- fired.existing_run === true &&
5999
- fired.matching_existing !== true
6000
- ) {
6001
- return {
6002
- status: 'stalled',
6003
- runId: launch.runId,
6004
- reason:
6005
- `Cron schedule "${schedule.scheduleId}" generation ${schedule.generation} cannot advance past ` +
6006
- `occurrence ${schedule.dueAtMs}: Run "${launch.runId}" already exists with a different launch identity.`,
6117
+ ...(conflictingFields.length > 0 ? { conflictingFields } : {}),
6007
6118
  };
6008
6119
  }
6009
6120
  return { status: 'superseded' };
@@ -16711,7 +16822,12 @@ export async function listPostgresSchedulerOutboxMaterializationPlans(
16711
16822
  */
16712
16823
  export type RunLedgerOutboxTokenAuthority = 'local_mint' | 'gateway';
16713
16824
 
16714
- const POSTGRES_SCHEDULER_CANCELLATION_OUTBOX_LEASE_SECONDS = 15;
16825
+ const POSTGRES_SCHEDULER_CANCELLATION_OUTBOX_LEASE_SECONDS =
16826
+ // The direct projection can run for the full app-runtime request deadline.
16827
+ // Keep another 30 seconds for dispatch setup and acknowledgement so a
16828
+ // background projector cannot reclaim an append that is still in flight.
16829
+ Math.ceil(RUNTIME_CAPACITY_POLICY.receiptGateway.requestTimeoutMs / 1_000) +
16830
+ 30;
16715
16831
 
16716
16832
  /**
16717
16833
  * Stable lease identity shared by the atomic scheduler cancellation and the
@@ -91,6 +91,21 @@ export function flattenSubsteps(
91
91
  return flat;
92
92
  }
93
93
 
94
+ /**
95
+ * The names a substep's `field` answers to.
96
+ *
97
+ * A tool substep's field is a PATH, not a name: a step inside a `steps()`
98
+ * program is `steps.<step name>`, and a column's producer is
99
+ * `<column>.<step name>`. An `out:` contract names the step or column itself,
100
+ * so matching the whole path would never hit — `out:"contactout_personal"`
101
+ * against `steps.contactout_personal`. Both ends of the path are offered: the
102
+ * full path, and the last segment, which is the step/column the author named.
103
+ */
104
+ function substepFieldSymbols(field: string): Set<string> {
105
+ const segments = field.split('.');
106
+ return new Set([field, segments[segments.length - 1]!]);
107
+ }
108
+
94
109
  /** How a binding reached its substep — see the module header for the rules. */
95
110
  export type DocflowJoinVia = 'symbol' | 'extent';
96
111
 
@@ -115,6 +130,10 @@ export type DocflowLineExtent = { startLine: number; endLine: number };
115
130
  export function substepSourceExtent(
116
131
  substep: PlayStaticSubstep,
117
132
  ): DocflowLineExtent | null {
133
+ // Resolved through a call: the substep and everything under it live in the
134
+ // callee, so the only interval an annotation here can contain is the call.
135
+ const viaCall = callSiteExtent(substep);
136
+ if (viaCall) return viaCall;
118
137
  const ranges: PlayStaticSourceRange[] = [];
119
138
  const collect = (candidate: PlayStaticSubstep) => {
120
139
  if (candidate.sourceRange) ranges.push(candidate.sourceRange);
@@ -194,30 +213,73 @@ export type DocflowExtentResolver = (
194
213
  export function substepOwnSourceExtent(
195
214
  substep: PlayStaticSubstep,
196
215
  ): DocflowLineExtent | null {
216
+ const viaCall = callSiteExtent(substep);
217
+ if (viaCall) return viaCall;
197
218
  const range = substep.sourceRange;
198
219
  return range ? { startLine: range.startLine, endLine: range.endLine } : null;
199
220
  }
200
221
 
222
+ /**
223
+ * The extent of the call a substep was reached through, when the extractor
224
+ * followed TypeScript symbol resolution into another function to find it.
225
+ *
226
+ * This is what lets a box annotated on `const x = await helper(ctx, …)` own the
227
+ * provider calls inside `helper` — including when `helper` is imported from
228
+ * another module, where the callee's own range is not even comparable to a line
229
+ * number in this file.
230
+ */
231
+ function callSiteExtent(
232
+ substep: PlayStaticSubstep,
233
+ ): DocflowLineExtent | null {
234
+ const range = substep.callSiteRange;
235
+ return range ? { startLine: range.startLine, endLine: range.endLine } : null;
236
+ }
237
+
238
+ /**
239
+ * The extent a binding's line may be compared against.
240
+ *
241
+ * An annotation written in the ENTRY file keeps the existing behavior exactly:
242
+ * whatever the resolver prefers, no path check. An annotation written in an
243
+ * imported module is only comparable to a range from that same module — two
244
+ * files both have a line 42 — so it picks the substep's own range or its call
245
+ * site, whichever was recorded in the annotation's file, and matches nothing
246
+ * when neither was.
247
+ */
248
+ function extentForBinding(
249
+ substep: PlayStaticSubstep,
250
+ binding: PlayDocflowBinding,
251
+ extentOf: DocflowExtentResolver,
252
+ ): DocflowLineExtent | null {
253
+ if (!binding.sourcePath) return extentOf(substep);
254
+ for (const range of [substep.sourceRange, substep.callSiteRange]) {
255
+ if (range?.sourcePath !== binding.sourcePath) continue;
256
+ return { startLine: range.startLine, endLine: range.endLine };
257
+ }
258
+ return null;
259
+ }
260
+
201
261
  function extentContainsLine(
202
262
  substep: PlayStaticSubstep,
203
- line: number,
263
+ binding: PlayDocflowBinding,
204
264
  extentOf: DocflowExtentResolver,
205
265
  ): DocflowLineExtent | null {
206
- const extent = extentOf(substep);
266
+ const extent = extentForBinding(substep, binding, extentOf);
207
267
  if (!extent) return null;
208
- return line >= extent.startLine && line <= extent.endLine ? extent : null;
268
+ return binding.line >= extent.startLine && binding.line <= extent.endLine
269
+ ? extent
270
+ : null;
209
271
  }
210
272
 
211
273
  /** Smallest containing extent wins, so a nested construct beats its parent. */
212
274
  function smallestContaining<TSubstep extends PlayStaticSubstep>(
213
275
  candidates: readonly TSubstep[],
214
- line: number,
276
+ binding: PlayDocflowBinding,
215
277
  extentOf: DocflowExtentResolver = substepSourceExtent,
216
278
  ): TSubstep | null {
217
279
  let best: TSubstep | null = null;
218
280
  let bestSpan = Number.POSITIVE_INFINITY;
219
281
  for (const candidate of candidates) {
220
- const extent = extentContainsLine(candidate, line, extentOf);
282
+ const extent = extentContainsLine(candidate, binding, extentOf);
221
283
  if (!extent) continue;
222
284
  const span = extent.endLine - extent.startLine;
223
285
  if (span < bestSpan) {
@@ -269,12 +331,12 @@ function joinBySymbolThenExtent<TSubstep extends PlayStaticSubstep>(
269
331
  // Two constructs compute the same column name. The one whose body the
270
332
  // annotation actually sits in decides; otherwise this is drift, and a
271
333
  // guess would attribute a member's rows to the wrong table.
272
- const narrowed = smallestContaining(matches, binding.line, extentOf);
334
+ const narrowed = smallestContaining(matches, binding, extentOf);
273
335
  if (narrowed) return { substep: narrowed, via: 'symbol' };
274
336
  return { substep: null, reason: 'ambiguous' };
275
337
  }
276
338
  }
277
- const positional = smallestContaining(candidates, binding.line, extentOf);
339
+ const positional = smallestContaining(candidates, binding, extentOf);
278
340
  if (positional) return { substep: positional, via: 'extent' };
279
341
  return { substep: null, reason: 'no-owner' };
280
342
  }
@@ -326,13 +388,53 @@ export function resolveDocflowBoundTool(
326
388
  const tools = substeps.filter(
327
389
  (substep): substep is DocflowToolSubstep => substep.type === 'tool',
328
390
  );
329
- return joinBySymbolThenExtent(
330
- binding,
331
- tools,
332
- (substep) => new Set([substep.field]),
391
+ return joinBySymbolThenExtent(binding, tools, (substep) =>
392
+ substepFieldSymbols(substep.field),
333
393
  );
334
394
  }
335
395
 
396
+ /**
397
+ * EVERY tool a binding names, not just the one it unambiguously resolves to.
398
+ *
399
+ * {@link resolveDocflowBoundTool} answers "which single provider is this box",
400
+ * and correctly declines when a step calls two — a finder plus a validator, the
401
+ * shape every waterfall leg has. That decline is right for a headline tool and
402
+ * wrong for the Config surface, which exists to list a step's provider calls
403
+ * and their params. So this returns the whole set, in source order.
404
+ *
405
+ * Symbol match only: a step's tools are named by its `out:` contract. Extent is
406
+ * deliberately not a fallback here, because "every tool whose lines happen to
407
+ * sit inside this construct" would sweep up a sibling leg's calls.
408
+ */
409
+ export function docflowBoundToolSubsteps(
410
+ binding: PlayDocflowBinding,
411
+ substeps: readonly PlayStaticSubstep[],
412
+ ): DocflowToolSubstep[] {
413
+ const roots = docflowOutputRoots(binding);
414
+ if (roots.length === 0) return [];
415
+ const matched: DocflowToolSubstep[] = [];
416
+ const seen = new Set<string>();
417
+ // Callers hand over whatever shape they hold — the config model passes the
418
+ // top-level stages, whose tools sit several levels down inside step suites
419
+ // and control flow. Flatten here so the answer never depends on that.
420
+ for (const substep of flattenSubsteps(substeps)) {
421
+ if (substep.type !== 'tool') continue;
422
+ const symbols = substepFieldSymbols(substep.field);
423
+ if (!roots.some((root) => symbols.has(root))) continue;
424
+ if (binding.sourcePath) {
425
+ const paths = [substep.sourceRange, substep.callSiteRange].map(
426
+ (range) => range?.sourcePath,
427
+ );
428
+ if (!paths.includes(binding.sourcePath)) continue;
429
+ }
430
+ const identity = `${substep.toolId}::${substep.paramsSource ?? ''}`;
431
+ if (seen.has(identity)) continue;
432
+ seen.add(identity);
433
+ matched.push(substep);
434
+ }
435
+ return matched;
436
+ }
437
+
336
438
  /**
337
439
  * The `ctx.runPlay` or `ctx.runPlayAsync` call a `type:"play"` docflow node
338
440
  * names. Same join as every other binding — symbol first, extent as the fallback
@@ -386,6 +488,13 @@ function legNameFromField(suiteField: string, legField: string): string {
386
488
  * field, and carrying only decidable facts about the text — never a claim about
387
489
  * whether this run reached it.
388
490
  */
491
+ /** One provider call inside a leg, with the source of its input. */
492
+ export type DocflowLegCall = {
493
+ toolId: string;
494
+ /** The authored expression passed as `input`, verbatim. Absent if unparsed. */
495
+ paramsSource?: string;
496
+ };
497
+
389
498
  export type DocflowStepSuiteLeg = {
390
499
  /** The leg's durable step name — the string the author passed `.step(…)`. */
391
500
  name: string;
@@ -395,6 +504,14 @@ export type DocflowStepSuiteLeg = {
395
504
  toolId: string | null;
396
505
  /** Every provider call the leg makes, in order. */
397
506
  toolIds: string[];
507
+ /**
508
+ * The same calls, each with the source text of the input it is given.
509
+ *
510
+ * `toolIds` answers "which provider"; a reader looking at one attempt of a
511
+ * nine-attempt cascade also needs "with what" — the pane used to name the
512
+ * provider twice and never once say what it was called with.
513
+ */
514
+ calls: DocflowLegCall[];
398
515
  /** The leg is `runIf`-guarded, so whether it runs depends on earlier legs. */
399
516
  conditional: boolean;
400
517
  /** Every substep of the leg is statically off (`runIf: () => false`). */
@@ -415,12 +532,18 @@ export function stepSuiteLegs(
415
532
  for (const child of suite.steps ?? []) {
416
533
  const field = (child as { field?: string }).field ?? '';
417
534
  if (!field) continue;
418
- const toolIds = flattenSubsteps([child])
419
- .filter((step): step is DocflowToolSubstep => step.type === 'tool')
420
- .map((step) => step.toolId);
535
+ const toolSteps = flattenSubsteps([child]).filter(
536
+ (step): step is DocflowToolSubstep => step.type === 'tool',
537
+ );
538
+ const toolIds = toolSteps.map((step) => step.toolId);
539
+ const calls: DocflowLegCall[] = toolSteps.map((step) => ({
540
+ toolId: step.toolId,
541
+ ...(step.paramsSource ? { paramsSource: step.paramsSource } : {}),
542
+ }));
421
543
  const existing = byField.get(field);
422
544
  if (existing) {
423
545
  existing.toolIds.push(...toolIds);
546
+ existing.calls.push(...calls);
424
547
  existing.toolId ??= toolIds[0] ?? null;
425
548
  existing.conditional ||= child.conditional === true;
426
549
  existing.disabled &&= child.disabled === true;
@@ -432,6 +555,7 @@ export function stepSuiteLegs(
432
555
  field,
433
556
  toolId: toolIds[0] ?? null,
434
557
  toolIds: [...toolIds],
558
+ calls: [...calls],
435
559
  conditional: child.conditional === true,
436
560
  disabled: child.disabled === true,
437
561
  nested: child.type === 'step_suite',