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.
- package/dist/bundling-sources/sdk/src/client.ts +13 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +18 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +10 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +145 -29
- package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +138 -14
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +57 -3
- package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +3 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/workspace-bundle.ts +651 -0
- package/dist/cli/index.js +48 -2
- package/dist/cli/index.mjs +48 -2
- package/dist/{compiler-manifest-4bYusxBr.d.mts → compiler-manifest-Dpd2hEet.d.mts} +28 -0
- package/dist/{compiler-manifest-4bYusxBr.d.ts → compiler-manifest-Dpd2hEet.d.ts} +28 -0
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +7 -1
- package/dist/index.mjs +7 -1
- package/dist/install-integrity.json +3 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +24 -3
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- 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.
|
|
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',
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
4721
|
-
|
|
4722
|
-
$
|
|
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
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
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
|
|
5687
|
-
*
|
|
5688
|
-
*
|
|
5689
|
-
*
|
|
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 =
|
|
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
|
-
|
|
263
|
+
binding: PlayDocflowBinding,
|
|
204
264
|
extentOf: DocflowExtentResolver,
|
|
205
265
|
): DocflowLineExtent | null {
|
|
206
|
-
const extent =
|
|
266
|
+
const extent = extentForBinding(substep, binding, extentOf);
|
|
207
267
|
if (!extent) return null;
|
|
208
|
-
return line >= extent.startLine && line <= extent.endLine
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
419
|
-
|
|
420
|
-
|
|
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',
|