deepline 0.3.258 → 0.3.259
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/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +21 -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 +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -0
- 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 +12 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
normalizePlayRunLedgerTerminalSource,
|
|
8
8
|
} from './run-terminal-source';
|
|
9
9
|
import { MAX_LEDGER_LOG_LINES_PER_EVENT } from './ledger-safe-payload';
|
|
10
|
+
import type { RunStopCause } from './run-execution-state';
|
|
10
11
|
import {
|
|
11
12
|
normalizePlayDocflowNodeIoState,
|
|
12
13
|
type PlayDocflowNodeIoState,
|
|
@@ -94,6 +95,14 @@ export type PlayRunLedgerStepSnapshot = {
|
|
|
94
95
|
kind?: string;
|
|
95
96
|
status: PlayRunLedgerStepStatus;
|
|
96
97
|
error?: string | null;
|
|
98
|
+
/**
|
|
99
|
+
* Set when the run's own terminal event settled this step while it was in
|
|
100
|
+
* flight: the step did not fail, the run stopped under it. Its status reads
|
|
101
|
+
* `failed` for compatibility; readers derive `stopped` through
|
|
102
|
+
* `deriveNodeExecutionState` (run-execution-state.ts). Absent on snapshots
|
|
103
|
+
* written before the field and on every step that settled itself.
|
|
104
|
+
*/
|
|
105
|
+
stoppedBy?: RunStopCause;
|
|
97
106
|
artifactTableNamespace?: string | null;
|
|
98
107
|
startedAt?: number | null;
|
|
99
108
|
completedAt?: number | null;
|
|
@@ -863,6 +872,11 @@ export function normalizePlayRunLedgerSnapshot(
|
|
|
863
872
|
kind: optionalString(rawStep.kind),
|
|
864
873
|
status,
|
|
865
874
|
error: optionalNullableString(rawStep.error),
|
|
875
|
+
...(status === 'failed' &&
|
|
876
|
+
(rawStep.stoppedBy === 'run_failed' ||
|
|
877
|
+
rawStep.stoppedBy === 'run_cancelled')
|
|
878
|
+
? { stoppedBy: rawStep.stoppedBy }
|
|
879
|
+
: {}),
|
|
866
880
|
artifactTableNamespace: optionalNullableString(
|
|
867
881
|
rawStep.artifactTableNamespace,
|
|
868
882
|
),
|
|
@@ -1034,6 +1048,24 @@ function maxProgressNumber(
|
|
|
1034
1048
|
return Math.max(current, next);
|
|
1035
1049
|
}
|
|
1036
1050
|
|
|
1051
|
+
/**
|
|
1052
|
+
* A row is counted once: it either succeeded or failed. `failed` is merged by
|
|
1053
|
+
* max, so a row that failed on one attempt and then succeeded on a later one
|
|
1054
|
+
* (a resumed run re-executes it) would otherwise stay counted in both, and a
|
|
1055
|
+
* 2-row map settled "2 completed, 2 failed of 2". Once `completed` and
|
|
1056
|
+
* `total` are known, failed rows are at most the rows that did not succeed.
|
|
1057
|
+
*/
|
|
1058
|
+
function settledFailedRows(input: {
|
|
1059
|
+
failed: number;
|
|
1060
|
+
completed: number | undefined;
|
|
1061
|
+
total: number | undefined;
|
|
1062
|
+
}): number {
|
|
1063
|
+
if (input.completed === undefined || input.total === undefined) {
|
|
1064
|
+
return input.failed;
|
|
1065
|
+
}
|
|
1066
|
+
return Math.min(input.failed, Math.max(0, input.total - input.completed));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1037
1069
|
function mergeMonotonicStepProgress(input: {
|
|
1038
1070
|
current?: PlayRunLedgerStepProgress | null;
|
|
1039
1071
|
next: PlayRunLedgerStepProgress;
|
|
@@ -1056,7 +1088,13 @@ function mergeMonotonicStepProgress(input: {
|
|
|
1056
1088
|
? { total: maxProgressNumber(current.total, next.total) }
|
|
1057
1089
|
: {}),
|
|
1058
1090
|
...(maxProgressNumber(current.failed, next.failed) !== undefined
|
|
1059
|
-
? {
|
|
1091
|
+
? {
|
|
1092
|
+
failed: settledFailedRows({
|
|
1093
|
+
failed: maxProgressNumber(current.failed, next.failed)!,
|
|
1094
|
+
completed: maxProgressNumber(current.completed, next.completed),
|
|
1095
|
+
total: maxProgressNumber(current.total, next.total),
|
|
1096
|
+
}),
|
|
1097
|
+
}
|
|
1060
1098
|
: {}),
|
|
1061
1099
|
...(maxProgressNumber(current.startedRows, next.startedRows) !== undefined
|
|
1062
1100
|
? {
|
|
@@ -1089,15 +1127,57 @@ function clearInFlightProgressOnTerminal(
|
|
|
1089
1127
|
progress: PlayRunLedgerStepProgress | null | undefined,
|
|
1090
1128
|
): PlayRunLedgerStepProgress | null | undefined {
|
|
1091
1129
|
if (!progress) return progress;
|
|
1092
|
-
|
|
1130
|
+
const message =
|
|
1131
|
+
typeof progress.message === 'string'
|
|
1132
|
+
? settledProgressMessage(progress.message, progress)
|
|
1133
|
+
: progress.message;
|
|
1134
|
+
if (
|
|
1135
|
+
progress.activeRows === undefined &&
|
|
1136
|
+
progress.waitingRows === undefined &&
|
|
1137
|
+
message === progress.message
|
|
1138
|
+
) {
|
|
1093
1139
|
return progress;
|
|
1094
1140
|
}
|
|
1095
1141
|
const cleared = { ...progress };
|
|
1096
1142
|
delete cleared.activeRows;
|
|
1097
1143
|
delete cleared.waitingRows;
|
|
1144
|
+
if (message === undefined) delete cleared.message;
|
|
1145
|
+
else cleared.message = message;
|
|
1098
1146
|
return cleared;
|
|
1099
1147
|
}
|
|
1100
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* A live progress line's in-flight clause ("20 calls in flight · ") is a
|
|
1151
|
+
* live-only counter in words, like `activeRows`: a settled step has nothing in
|
|
1152
|
+
* flight. A run that stopped kept "20 calls in flight · 1,241 / 50,000 rows
|
|
1153
|
+
* processed" as its final word. Drop the clause and keep the settled count; a
|
|
1154
|
+
* line that was only the clause says nothing once it is gone.
|
|
1155
|
+
*/
|
|
1156
|
+
const IN_FLIGHT_MESSAGE_CLAUSE = /^\s*[\d,]+ calls? in flight(?:\s*·\s*)?/i;
|
|
1157
|
+
/** The producer's plain count line, "1,241 / 50,000 rows processed". */
|
|
1158
|
+
const ROWS_PROCESSED_MESSAGE = /^[\d,]+ \/ [\d,]+ rows processed$/;
|
|
1159
|
+
|
|
1160
|
+
function settledProgressMessage(
|
|
1161
|
+
message: string,
|
|
1162
|
+
progress: PlayRunLedgerStepProgress,
|
|
1163
|
+
): string | undefined {
|
|
1164
|
+
const settled = message.replace(IN_FLIGHT_MESSAGE_CLAUSE, '').trim();
|
|
1165
|
+
if (settled.length === 0) return undefined;
|
|
1166
|
+
// The line is the last live tick's words; the settled counts can be newer
|
|
1167
|
+
// (a failed run's terminal row settles them from the runtime sheet). A plain
|
|
1168
|
+
// count line restates the counts it now sits beside, never older ones.
|
|
1169
|
+
const completed = progress.completedRows ?? progress.completed;
|
|
1170
|
+
if (
|
|
1171
|
+
ROWS_PROCESSED_MESSAGE.test(settled) &&
|
|
1172
|
+
typeof completed === 'number' &&
|
|
1173
|
+
typeof progress.total === 'number' &&
|
|
1174
|
+
progress.total > 0
|
|
1175
|
+
) {
|
|
1176
|
+
return `${completed.toLocaleString()} / ${progress.total.toLocaleString()} rows processed`;
|
|
1177
|
+
}
|
|
1178
|
+
return settled === message ? message : settled;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1101
1181
|
function isTerminalStepStatus(status: PlayRunLedgerStepStatus): boolean {
|
|
1102
1182
|
return status === 'completed' || status === 'failed' || status === 'skipped';
|
|
1103
1183
|
}
|
|
@@ -1402,9 +1482,12 @@ function retryablePlatformDeployFailureSnapshot(
|
|
|
1402
1482
|
// counts, just mark it settled so the UI stops rendering a live ticker.
|
|
1403
1483
|
function settleRunningStepsOnTerminal(
|
|
1404
1484
|
snapshot: PlayRunLedgerSnapshot,
|
|
1405
|
-
|
|
1485
|
+
settle:
|
|
1486
|
+
| { status: 'completed' }
|
|
1487
|
+
| { status: 'failed'; stoppedBy: RunStopCause },
|
|
1406
1488
|
occurredAt: number,
|
|
1407
1489
|
): PlayRunLedgerSnapshot {
|
|
1490
|
+
const { status } = settle;
|
|
1408
1491
|
let changed = false;
|
|
1409
1492
|
const stepsById = Object.fromEntries(
|
|
1410
1493
|
Object.entries(snapshot.stepsById).map(([stepId, step]) => {
|
|
@@ -1417,6 +1500,9 @@ function settleRunningStepsOnTerminal(
|
|
|
1417
1500
|
{
|
|
1418
1501
|
...step,
|
|
1419
1502
|
status,
|
|
1503
|
+
...(settle.status === 'failed'
|
|
1504
|
+
? { stoppedBy: settle.stoppedBy }
|
|
1505
|
+
: {}),
|
|
1420
1506
|
completedAt: step.completedAt ?? occurredAt,
|
|
1421
1507
|
updatedAt: occurredAt,
|
|
1422
1508
|
progress: step.progress
|
|
@@ -1621,7 +1707,11 @@ export function reducePlayRunLedgerEvent(
|
|
|
1621
1707
|
terminalWorkCounterSegment !== undefined),
|
|
1622
1708
|
}) ??
|
|
1623
1709
|
withTiming({
|
|
1624
|
-
...settleRunningStepsOnTerminal(
|
|
1710
|
+
...settleRunningStepsOnTerminal(
|
|
1711
|
+
base,
|
|
1712
|
+
{ status: 'completed' },
|
|
1713
|
+
occurredAt,
|
|
1714
|
+
),
|
|
1625
1715
|
status: 'completed',
|
|
1626
1716
|
waitKind: null,
|
|
1627
1717
|
waitUntil: null,
|
|
@@ -1664,7 +1754,11 @@ export function reducePlayRunLedgerEvent(
|
|
|
1664
1754
|
terminalWorkCounterSegment !== undefined),
|
|
1665
1755
|
}) ??
|
|
1666
1756
|
withTiming({
|
|
1667
|
-
...settleRunningStepsOnTerminal(
|
|
1757
|
+
...settleRunningStepsOnTerminal(
|
|
1758
|
+
base,
|
|
1759
|
+
{ status: 'failed', stoppedBy: 'run_failed' },
|
|
1760
|
+
occurredAt,
|
|
1761
|
+
),
|
|
1668
1762
|
status: 'failed',
|
|
1669
1763
|
waitKind: null,
|
|
1670
1764
|
waitUntil: null,
|
|
@@ -1697,7 +1791,11 @@ export function reducePlayRunLedgerEvent(
|
|
|
1697
1791
|
canPromoteTerminalResultReference(base.result, event.result),
|
|
1698
1792
|
}) ??
|
|
1699
1793
|
withTiming({
|
|
1700
|
-
...settleRunningStepsOnTerminal(
|
|
1794
|
+
...settleRunningStepsOnTerminal(
|
|
1795
|
+
base,
|
|
1796
|
+
{ status: 'failed', stoppedBy: 'run_cancelled' },
|
|
1797
|
+
occurredAt,
|
|
1798
|
+
),
|
|
1701
1799
|
status: 'cancelled',
|
|
1702
1800
|
waitKind: null,
|
|
1703
1801
|
waitUntil: null,
|
|
@@ -1915,6 +2013,8 @@ export function reducePlayRunLedgerEvent(
|
|
|
1915
2013
|
// Terminal step: clear live in-flight/waiting accounting.
|
|
1916
2014
|
progress: clearInFlightProgressOnTerminal(current?.progress) ?? null,
|
|
1917
2015
|
};
|
|
2016
|
+
// The step's own settle event replaces a stop the run recorded.
|
|
2017
|
+
if (!preserveFailedStatus) delete nextStep.stoppedBy;
|
|
1918
2018
|
const runningStepIds = base.orderedStepIds.filter((stepId) => {
|
|
1919
2019
|
if (stepId === event.stepId) return false;
|
|
1920
2020
|
return base.stepsById[stepId]?.status === 'running';
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPlayRunStatusSnapshot,
|
|
3
|
+
type LedgerBackedRunLike,
|
|
4
|
+
type PlayRunStreamNodeState,
|
|
5
|
+
} from './run-snapshot-stream';
|
|
6
|
+
import {
|
|
7
|
+
decodePlayRunPublicStatus,
|
|
8
|
+
isActivePlayRunLifecycleStatus,
|
|
9
|
+
isTerminalPlayRunLifecycleStatus,
|
|
10
|
+
type PlayRunPublicStatus,
|
|
11
|
+
} from './run-lifecycle-policy';
|
|
12
|
+
import type { RunStopCause } from './run-execution-state';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run Observation (ADR-0022, docs/plans/2026-09-30-live-run-observation.md §3.2).
|
|
16
|
+
*
|
|
17
|
+
* The one bounded document every dashboard surface reads a Play Run's live
|
|
18
|
+
* state from. It is a pure projection of the Convex Run Snapshot (ADR-0001);
|
|
19
|
+
* Runtime Postgres stays the lifecycle authority and is never read here.
|
|
20
|
+
*
|
|
21
|
+
* Bounded BY TYPE (AGENTS.md "Status and metadata documents"): every field is
|
|
22
|
+
* a scalar, a fixed-shape summary, or `nodes`, whose length is the static
|
|
23
|
+
* graph's step count (an artifact property, not a function of rows, calls,
|
|
24
|
+
* facts or logs). Row payloads, log lines, calls and billing facts live behind
|
|
25
|
+
* their own paginated endpoints. `run-observation-size-contract.test.ts` pins
|
|
26
|
+
* the byte bound for a 20,000-row / 200-node / 50,000-log run.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How the run started, as far as its admission recorded it: the automated
|
|
31
|
+
* trigger that fired it, a child or rerun of another run, or the client
|
|
32
|
+
* surface a person used (`ui`, `cli`, `api`). `unknown` when nothing says —
|
|
33
|
+
* never a guess. The run table reads the same derivation (`runLaunchTrigger`),
|
|
34
|
+
* so the canvas Start pill and the runs grid name the same provenance.
|
|
35
|
+
*/
|
|
36
|
+
export type RunObservationTrigger =
|
|
37
|
+
| 'ui'
|
|
38
|
+
| 'cli'
|
|
39
|
+
| 'api'
|
|
40
|
+
| 'rerun'
|
|
41
|
+
| 'schedule'
|
|
42
|
+
| 'webhook'
|
|
43
|
+
| 'sql'
|
|
44
|
+
| 'child'
|
|
45
|
+
| 'unknown';
|
|
46
|
+
|
|
47
|
+
/** Public lifecycle status, or `unknown` when the stored value is unreadable. */
|
|
48
|
+
export type RunObservationStatus = PlayRunPublicStatus | 'unknown';
|
|
49
|
+
|
|
50
|
+
export type RunObservationNodeStatus = PlayRunStreamNodeState['status'];
|
|
51
|
+
|
|
52
|
+
export type RunObservationNode = {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
status: RunObservationNodeStatus;
|
|
55
|
+
startedAt: number | null;
|
|
56
|
+
completedAt: number | null;
|
|
57
|
+
/** The step's declared row total, when the runtime reported one. */
|
|
58
|
+
total: number | null;
|
|
59
|
+
/** Rows the step has finished (the progress `completed` counter). */
|
|
60
|
+
settled: number;
|
|
61
|
+
failed: number;
|
|
62
|
+
/**
|
|
63
|
+
* The run stopped under this step (it did not fail itself): the projected
|
|
64
|
+
* Run Snapshot fact `deriveNodeExecutionState` reads. Absent otherwise.
|
|
65
|
+
*/
|
|
66
|
+
stoppedBy?: RunStopCause;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type RunObservation = {
|
|
70
|
+
runId: string;
|
|
71
|
+
playName: string;
|
|
72
|
+
trigger: RunObservationTrigger;
|
|
73
|
+
status: RunObservationStatus;
|
|
74
|
+
/**
|
|
75
|
+
* The seq of the last applied Run Event (Convex assigns it in the append),
|
|
76
|
+
* or `null` for a row that predates the ledger cursor and for an admission
|
|
77
|
+
* bridge answer. Freshness falls back to `updatedAt`; see
|
|
78
|
+
* {@link compareRunObservationFreshness}.
|
|
79
|
+
*/
|
|
80
|
+
ledgerSeq: number | null;
|
|
81
|
+
updatedAt: number;
|
|
82
|
+
/** `admission` = the pre-projection bridge: scalar, no node claims. */
|
|
83
|
+
source: 'snapshot' | 'admission';
|
|
84
|
+
createdAt: number;
|
|
85
|
+
startedAt: number | null;
|
|
86
|
+
finishedAt: number | null;
|
|
87
|
+
/** Present only while waiting. The UI derives "overdue" against serverTime. */
|
|
88
|
+
wait: { kind: string; deadlineAt: number | null } | null;
|
|
89
|
+
activity: { target: string; state: string; label: string } | null;
|
|
90
|
+
error: { code: string; message: string } | null;
|
|
91
|
+
/** Terminal row outcomes; `null` while the run is active (counts churn). */
|
|
92
|
+
rows: { total: number; ok: number; failed: number; pending: number } | null;
|
|
93
|
+
nodes: RunObservationNode[];
|
|
94
|
+
/** Cumulative ingested log lines. Lines stay on `/runs/:id/logs`. */
|
|
95
|
+
logs: { total: number };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** One row of the observe head: enough for the grid, rail and header count. */
|
|
99
|
+
export type RunObservationHeadEntry = {
|
|
100
|
+
runId: string;
|
|
101
|
+
status: RunObservationStatus;
|
|
102
|
+
trigger: RunObservationTrigger;
|
|
103
|
+
createdAt: number;
|
|
104
|
+
updatedAt: number;
|
|
105
|
+
finishedAt: number | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** The error message bound. Full text stays on `GET /runs/:id`. */
|
|
109
|
+
export const RUN_OBSERVATION_ERROR_MESSAGE_MAX_CHARS = 500;
|
|
110
|
+
/** The head is the newest runs of one Play. */
|
|
111
|
+
export const RUN_OBSERVATION_HEAD_LIMIT = 20;
|
|
112
|
+
/** Per-run observations one request may ask for. */
|
|
113
|
+
export const RUN_OBSERVATION_WATCH_LIMIT = 10;
|
|
114
|
+
|
|
115
|
+
/** The stored `playRuns` fields the projection reads. */
|
|
116
|
+
export type RunObservationSourceRun = LedgerBackedRunLike &
|
|
117
|
+
RunLaunchProvenance & {
|
|
118
|
+
name: string;
|
|
119
|
+
createdAt: number;
|
|
120
|
+
updatedAt: number;
|
|
121
|
+
ledgerNextSeq?: number | null;
|
|
122
|
+
error?: string | null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The stored fields a run's launch provenance is read from. */
|
|
126
|
+
export type RunLaunchProvenance = {
|
|
127
|
+
triggerSource?: string | null;
|
|
128
|
+
launchSurface?: string | null;
|
|
129
|
+
replayedFromRunId?: string | null;
|
|
130
|
+
parentRunId?: string | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The ONE derivation of how a run started. An automated trigger names
|
|
135
|
+
* itself; a child or a rerun says so; otherwise the client surface recorded
|
|
136
|
+
* at admission. `triggerSource: 'api'` alone is the default for every
|
|
137
|
+
* person-started run, so without a recorded surface it names nothing.
|
|
138
|
+
*/
|
|
139
|
+
export function runLaunchTrigger(
|
|
140
|
+
run: RunLaunchProvenance,
|
|
141
|
+
): RunObservationTrigger {
|
|
142
|
+
switch (run.triggerSource) {
|
|
143
|
+
case 'cron':
|
|
144
|
+
return 'schedule';
|
|
145
|
+
case 'webhook':
|
|
146
|
+
return 'webhook';
|
|
147
|
+
case 'sql_listener':
|
|
148
|
+
return 'sql';
|
|
149
|
+
case 'async_child':
|
|
150
|
+
return 'child';
|
|
151
|
+
}
|
|
152
|
+
if (run.parentRunId) return 'child';
|
|
153
|
+
if (run.replayedFromRunId) return 'rerun';
|
|
154
|
+
switch (run.launchSurface) {
|
|
155
|
+
case 'ui':
|
|
156
|
+
case 'cli':
|
|
157
|
+
case 'api':
|
|
158
|
+
return run.launchSurface;
|
|
159
|
+
default:
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function runObservationStatus(value: unknown): RunObservationStatus {
|
|
165
|
+
return decodePlayRunPublicStatus(value) ?? 'unknown';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function finite(value: unknown): number | null {
|
|
169
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nonNegative(value: unknown): number {
|
|
173
|
+
const number = finite(value);
|
|
174
|
+
return number !== null && number > 0 ? number : 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `ledgerNextSeq` is the NEXT seq; the last applied one is one less. */
|
|
178
|
+
export function ledgerSeqFromNextSeq(nextSeq: unknown): number | null {
|
|
179
|
+
const next = finite(nextSeq);
|
|
180
|
+
return next !== null && Number.isInteger(next) && next >= 2 ? next - 1 : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function boundRunObservationError(
|
|
184
|
+
error: string | null | undefined,
|
|
185
|
+
): RunObservation['error'] {
|
|
186
|
+
const text = typeof error === 'string' ? error.trim() : '';
|
|
187
|
+
if (!text) return null;
|
|
188
|
+
const code = /^([A-Za-z][A-Za-z0-9_]{0,79}):/.exec(text)?.[1] ?? 'run_error';
|
|
189
|
+
return {
|
|
190
|
+
code,
|
|
191
|
+
message:
|
|
192
|
+
text.length > RUN_OBSERVATION_ERROR_MESSAGE_MAX_CHARS
|
|
193
|
+
? text.slice(0, RUN_OBSERVATION_ERROR_MESSAGE_MAX_CHARS)
|
|
194
|
+
: text,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function projectNode(node: PlayRunStreamNodeState): RunObservationNode {
|
|
199
|
+
return {
|
|
200
|
+
nodeId: node.nodeId,
|
|
201
|
+
status: node.status,
|
|
202
|
+
startedAt: finite(node.startedAt),
|
|
203
|
+
completedAt: finite(node.completedAt),
|
|
204
|
+
total: finite(node.progress?.total),
|
|
205
|
+
settled: nonNegative(node.progress?.completed),
|
|
206
|
+
failed: nonNegative(node.progress?.failed),
|
|
207
|
+
...(node.stoppedBy ? { stoppedBy: node.stoppedBy } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Project a stored Convex Run Snapshot into its Run Observation. Pure: the
|
|
213
|
+
* same stored document always yields the same observation.
|
|
214
|
+
*/
|
|
215
|
+
export function projectRunObservation(
|
|
216
|
+
run: RunObservationSourceRun,
|
|
217
|
+
): RunObservation {
|
|
218
|
+
const snapshot = buildPlayRunStatusSnapshot({ run });
|
|
219
|
+
const status = runObservationStatus(
|
|
220
|
+
snapshot.status === 'unknown' ? run.status : snapshot.status,
|
|
221
|
+
);
|
|
222
|
+
const waitKind = run.waitKind ?? run.wait?.kind ?? null;
|
|
223
|
+
const rowOutcomes = snapshot.rowOutcomes ?? null;
|
|
224
|
+
const activity = snapshot.activity ?? null;
|
|
225
|
+
const terminal = isTerminalPlayRunLifecycleStatus(status);
|
|
226
|
+
return {
|
|
227
|
+
runId: run.workflowId,
|
|
228
|
+
playName: run.name,
|
|
229
|
+
trigger: runLaunchTrigger(run),
|
|
230
|
+
status,
|
|
231
|
+
ledgerSeq: ledgerSeqFromNextSeq(run.ledgerNextSeq),
|
|
232
|
+
updatedAt: Math.max(run.updatedAt, finite(snapshot.updatedAt) ?? 0),
|
|
233
|
+
source: 'snapshot',
|
|
234
|
+
createdAt: run.createdAt,
|
|
235
|
+
startedAt: finite(snapshot.startedAt) ?? finite(run.startedAt),
|
|
236
|
+
finishedAt: finite(snapshot.finishedAt) ?? finite(run.finishedAt),
|
|
237
|
+
wait:
|
|
238
|
+
status === 'waiting'
|
|
239
|
+
? {
|
|
240
|
+
kind: waitKind ?? 'unknown',
|
|
241
|
+
deadlineAt: finite(run.waitUntil),
|
|
242
|
+
}
|
|
243
|
+
: null,
|
|
244
|
+
activity: activity
|
|
245
|
+
? {
|
|
246
|
+
target: activity.observation.target.kind,
|
|
247
|
+
state: activity.observation.state.kind,
|
|
248
|
+
label: activity.summary,
|
|
249
|
+
}
|
|
250
|
+
: null,
|
|
251
|
+
error:
|
|
252
|
+
status === 'failed' || status === 'cancelled' || status === 'unknown'
|
|
253
|
+
? boundRunObservationError(run.error)
|
|
254
|
+
: null,
|
|
255
|
+
rows:
|
|
256
|
+
terminal && rowOutcomes
|
|
257
|
+
? {
|
|
258
|
+
total: rowOutcomes.totalRows,
|
|
259
|
+
ok: rowOutcomes.completedRows,
|
|
260
|
+
failed: rowOutcomes.failedRows,
|
|
261
|
+
pending: Math.max(
|
|
262
|
+
0,
|
|
263
|
+
rowOutcomes.totalRows -
|
|
264
|
+
rowOutcomes.completedRows -
|
|
265
|
+
rowOutcomes.failedRows -
|
|
266
|
+
(rowOutcomes.supersededRows ?? 0),
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
: null,
|
|
270
|
+
nodes: snapshot.nodeStates.map(projectNode),
|
|
271
|
+
logs: { total: nonNegative(snapshot.totalLogCount) },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The admission bridge's answer for a run Runtime Postgres admitted but whose
|
|
277
|
+
* `run.created` fact Convex has not projected yet. Scalar only: it makes no
|
|
278
|
+
* node, row or log claims, and any snapshot observation supersedes it.
|
|
279
|
+
*/
|
|
280
|
+
export function projectAdmissionRunObservation(run: {
|
|
281
|
+
workflowId: string;
|
|
282
|
+
name: string;
|
|
283
|
+
status: string;
|
|
284
|
+
createdAt: number | null;
|
|
285
|
+
startedAt: number | null;
|
|
286
|
+
updatedAt?: number | null;
|
|
287
|
+
waitKind?: string | null;
|
|
288
|
+
waitUntil?: number | null;
|
|
289
|
+
}): RunObservation {
|
|
290
|
+
// The bridge proves ADMISSION, not execution: the scheduler row is stamped
|
|
291
|
+
// `running` the moment the run is admitted (the start route's 202 says the
|
|
292
|
+
// same), before any work has been claimed. Until the snapshot projects the
|
|
293
|
+
// run's own facts, an active run reads `queued`; the snapshot then moves it
|
|
294
|
+
// forward, and the monotonic merge never lets it step back.
|
|
295
|
+
const reported = runObservationStatus(run.status);
|
|
296
|
+
const status: RunObservationStatus =
|
|
297
|
+
reported === 'running' ? 'queued' : reported;
|
|
298
|
+
const createdAt = finite(run.createdAt) ?? 0;
|
|
299
|
+
return {
|
|
300
|
+
runId: run.workflowId,
|
|
301
|
+
playName: run.name,
|
|
302
|
+
trigger: 'unknown',
|
|
303
|
+
status,
|
|
304
|
+
ledgerSeq: null,
|
|
305
|
+
updatedAt: finite(run.updatedAt) ?? createdAt,
|
|
306
|
+
source: 'admission',
|
|
307
|
+
createdAt,
|
|
308
|
+
startedAt: finite(run.startedAt),
|
|
309
|
+
finishedAt: null,
|
|
310
|
+
wait:
|
|
311
|
+
status === 'waiting'
|
|
312
|
+
? { kind: run.waitKind ?? 'unknown', deadlineAt: finite(run.waitUntil) }
|
|
313
|
+
: null,
|
|
314
|
+
activity: null,
|
|
315
|
+
error: null,
|
|
316
|
+
rows: null,
|
|
317
|
+
nodes: [],
|
|
318
|
+
logs: { total: 0 },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function isRunObservationActive(status: RunObservationStatus): boolean {
|
|
323
|
+
return isActivePlayRunLifecycleStatus(status);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function isRunObservationTerminal(
|
|
327
|
+
status: RunObservationStatus,
|
|
328
|
+
): boolean {
|
|
329
|
+
return isTerminalPlayRunLifecycleStatus(status);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Positive when `a` is fresher than `b`. Ordered by the ledger seq (a row
|
|
334
|
+
* without one sorts before any row with one), then by `updatedAt`, which
|
|
335
|
+
* orders same-seq snapshot rewrites and every pre-cursor row.
|
|
336
|
+
*/
|
|
337
|
+
export function compareRunObservationFreshness(
|
|
338
|
+
a: Pick<RunObservation, 'ledgerSeq' | 'updatedAt'>,
|
|
339
|
+
b: Pick<RunObservation, 'ledgerSeq' | 'updatedAt'>,
|
|
340
|
+
): number {
|
|
341
|
+
const seqA = a.ledgerSeq ?? -1;
|
|
342
|
+
const seqB = b.ledgerSeq ?? -1;
|
|
343
|
+
if (seqA !== seqB) return seqA - seqB;
|
|
344
|
+
return a.updatedAt - b.updatedAt;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The monotonic merge every reader applies (§3.4). Returns the observation to
|
|
349
|
+
* hold. Rules, in order:
|
|
350
|
+
* 1. A terminal status never regresses to a non-terminal one, and a
|
|
351
|
+
* terminal answer always replaces a non-terminal one.
|
|
352
|
+
* 2. A snapshot always replaces an admission answer, and an admission answer
|
|
353
|
+
* never replaces a snapshot.
|
|
354
|
+
* 3. Otherwise the fresher observation wins; a tie keeps the held one, so a
|
|
355
|
+
* repeated answer is referentially stable.
|
|
356
|
+
* 4. The status never steps back a lifecycle rank (queued → running/waiting
|
|
357
|
+
* → terminal): the winner's data with the held status, see
|
|
358
|
+
* `runObservationStatusRank`.
|
|
359
|
+
*/
|
|
360
|
+
export function mergeRunObservation(
|
|
361
|
+
held: RunObservation | null | undefined,
|
|
362
|
+
incoming: RunObservation,
|
|
363
|
+
): RunObservation {
|
|
364
|
+
if (!held) return incoming;
|
|
365
|
+
const heldTerminal = isRunObservationTerminal(held.status);
|
|
366
|
+
const incomingTerminal = isRunObservationTerminal(incoming.status);
|
|
367
|
+
if (heldTerminal && !incomingTerminal) return held;
|
|
368
|
+
// A terminal answer always ends a run the store still holds as active,
|
|
369
|
+
// whatever its seq, clock or source: an answer that ran AHEAD of it (a
|
|
370
|
+
// stale per-instance read, an admission row with a later clock) must not
|
|
371
|
+
// leave a finished run Queued/Running with Stop until a reload.
|
|
372
|
+
if (incomingTerminal && !heldTerminal) return incoming;
|
|
373
|
+
const winner =
|
|
374
|
+
held.source !== incoming.source
|
|
375
|
+
? incoming.source === 'snapshot'
|
|
376
|
+
? incoming
|
|
377
|
+
: held
|
|
378
|
+
: compareRunObservationFreshness(incoming, held) > 0
|
|
379
|
+
? incoming
|
|
380
|
+
: held;
|
|
381
|
+
return winner === held ? held : liftRunObservationStatus(held, winner);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Where a status sits in a run's lifecycle: `queued` → `running`/`waiting` →
|
|
386
|
+
* terminal. A run only ever moves forward through these ranks; `running` and
|
|
387
|
+
* `waiting` share one (a run may wait and resume any number of times).
|
|
388
|
+
* `unknown` ranks below everything, so it never displaces a known status.
|
|
389
|
+
*/
|
|
390
|
+
export function runObservationStatusRank(status: RunObservationStatus): number {
|
|
391
|
+
if (isRunObservationTerminal(status)) return 3;
|
|
392
|
+
if (status === 'running' || status === 'waiting') return 2;
|
|
393
|
+
if (status === 'queued') return 1;
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Rule 4 of the merge: a run's lifecycle never steps back. The fresher
|
|
399
|
+
* observation's data wins, but a status from an EARLIER rank than the one
|
|
400
|
+
* already held is a lagging source, not news — the Convex projection still
|
|
401
|
+
* saying `queued` after the runtime reported the run started, say. The held
|
|
402
|
+
* status stands until a later rank replaces it, so a surface never shows
|
|
403
|
+
* Running → Queued → Completed.
|
|
404
|
+
*/
|
|
405
|
+
function liftRunObservationStatus(
|
|
406
|
+
held: RunObservation,
|
|
407
|
+
winner: RunObservation,
|
|
408
|
+
): RunObservation {
|
|
409
|
+
if (
|
|
410
|
+
runObservationStatusRank(winner.status) >=
|
|
411
|
+
runObservationStatusRank(held.status)
|
|
412
|
+
) {
|
|
413
|
+
return winner;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
...winner,
|
|
417
|
+
status: held.status,
|
|
418
|
+
startedAt: winner.startedAt ?? held.startedAt,
|
|
419
|
+
wait: held.status === 'waiting' ? (winner.wait ?? held.wait) : null,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The same rules for a head row (no seq: `updatedAt` orders it). */
|
|
424
|
+
export function mergeRunObservationHeadEntry(
|
|
425
|
+
held: RunObservationHeadEntry | null | undefined,
|
|
426
|
+
incoming: RunObservationHeadEntry,
|
|
427
|
+
): RunObservationHeadEntry {
|
|
428
|
+
if (!held) return incoming;
|
|
429
|
+
const heldTerminal = isRunObservationTerminal(held.status);
|
|
430
|
+
const incomingTerminal = isRunObservationTerminal(incoming.status);
|
|
431
|
+
if (heldTerminal && !incomingTerminal) return held;
|
|
432
|
+
// Terminal always wins over an active row (see `mergeRunObservation`).
|
|
433
|
+
if (incomingTerminal && !heldTerminal) return incoming;
|
|
434
|
+
if (incoming.updatedAt <= held.updatedAt) return held;
|
|
435
|
+
// Never a step back in the lifecycle (see `liftRunObservationStatus`).
|
|
436
|
+
return runObservationStatusRank(incoming.status) <
|
|
437
|
+
runObservationStatusRank(held.status)
|
|
438
|
+
? { ...incoming, status: held.status }
|
|
439
|
+
: incoming;
|
|
440
|
+
}
|