@osolmaz/pi-workflows 0.11.0 → 0.11.2
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/README.md +14 -2
- package/dist/builtins/autoimplement.workflow.d.ts +2 -0
- package/dist/builtins/autoimplement.workflow.js +186 -9
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +55 -7
- package/dist/controllers/sqlite.js +195 -48
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.js +246 -54
- package/dist/extension/index.js.map +1 -1
- package/dist/herdr/setup.d.ts +13 -1
- package/dist/herdr/setup.js +349 -36
- package/dist/herdr/setup.js.map +1 -1
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- package/dist/viewer/cli.d.ts +1 -0
- package/dist/viewer/cli.js +21 -10
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +449 -0
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
- package/docs/workflows.md +11 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +212 -9
- package/src/controllers/sqlite.ts +308 -54
- package/src/extension/index.ts +303 -58
- package/src/herdr/setup.ts +429 -39
- package/src/host/runner.ts +3 -0
- package/src/viewer/cli.ts +22 -10
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/tool-input.ts +5 -2
|
@@ -83,20 +83,36 @@ type WorkflowRow = {
|
|
|
83
83
|
error: string | null;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
export type WorkflowRunLaunchStatus =
|
|
87
|
+
| "queued"
|
|
88
|
+
| "starting"
|
|
89
|
+
| "running"
|
|
90
|
+
| "parked"
|
|
91
|
+
| "done"
|
|
92
|
+
| "failed"
|
|
93
|
+
| "cancelled";
|
|
94
|
+
|
|
86
95
|
type WorkflowRunQueueRow = {
|
|
87
96
|
run_id: string;
|
|
88
97
|
workflow_ref: string;
|
|
89
98
|
workflow_path: string;
|
|
99
|
+
workflow_source_json: string;
|
|
100
|
+
definition_digest: string;
|
|
90
101
|
input_json: string;
|
|
91
|
-
|
|
102
|
+
launch_options_json: string;
|
|
103
|
+
status: WorkflowRunLaunchStatus;
|
|
92
104
|
runner_id: string | null;
|
|
93
105
|
claim_token: string | null;
|
|
94
106
|
claim_expires_at: number | null;
|
|
95
107
|
affinity_runner_id: string | null;
|
|
96
108
|
origin_session_id: string | null;
|
|
97
109
|
parent_run_id: string | null;
|
|
110
|
+
error_code: string | null;
|
|
111
|
+
error_message: string | null;
|
|
98
112
|
created_at: string;
|
|
99
113
|
updated_at: string;
|
|
114
|
+
started_at: string | null;
|
|
115
|
+
finished_at: string | null;
|
|
100
116
|
};
|
|
101
117
|
|
|
102
118
|
/** A user-started workflow run tracked by the durable run queue. */
|
|
@@ -106,8 +122,11 @@ export type WorkflowRunQueueRecord = {
|
|
|
106
122
|
workflowName: string;
|
|
107
123
|
/** Canonical source reference used to reopen the run. */
|
|
108
124
|
workflowSourceRef: string;
|
|
125
|
+
workflowSource: unknown;
|
|
126
|
+
definitionDigest: string;
|
|
109
127
|
input: unknown;
|
|
110
|
-
|
|
128
|
+
launchOptions: unknown;
|
|
129
|
+
status: WorkflowRunLaunchStatus;
|
|
111
130
|
runnerId: string | null;
|
|
112
131
|
claimToken: string | null;
|
|
113
132
|
claimExpiresAt: string | null;
|
|
@@ -115,8 +134,12 @@ export type WorkflowRunQueueRecord = {
|
|
|
115
134
|
/** Pi session that owns delivery and interactive execution, or null for detached runs. */
|
|
116
135
|
originSessionId: string | null;
|
|
117
136
|
parentRunId: string | null;
|
|
137
|
+
errorCode: string | null;
|
|
138
|
+
errorMessage: string | null;
|
|
118
139
|
createdAt: string;
|
|
119
140
|
updatedAt: string;
|
|
141
|
+
startedAt: string | null;
|
|
142
|
+
finishedAt: string | null;
|
|
120
143
|
};
|
|
121
144
|
|
|
122
145
|
type WorkflowNotificationRow = {
|
|
@@ -126,7 +149,7 @@ type WorkflowNotificationRow = {
|
|
|
126
149
|
attempt_id: string;
|
|
127
150
|
notification_index: number;
|
|
128
151
|
target_session_id: string;
|
|
129
|
-
kind: "progress" | "final";
|
|
152
|
+
kind: "progress" | "final" | "launch_failure";
|
|
130
153
|
content: string;
|
|
131
154
|
created_at: string;
|
|
132
155
|
delivery_claim_token: string | null;
|
|
@@ -141,7 +164,7 @@ export type WorkflowNotificationRecord = {
|
|
|
141
164
|
attemptId: string;
|
|
142
165
|
notificationIndex: number;
|
|
143
166
|
targetSessionId: string;
|
|
144
|
-
kind: "progress" | "final";
|
|
167
|
+
kind: "progress" | "final" | "launch_failure";
|
|
145
168
|
content: string;
|
|
146
169
|
createdAt: string;
|
|
147
170
|
deliveryClaimExpiresAt: string | null;
|
|
@@ -799,21 +822,57 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
799
822
|
if (row === undefined) {
|
|
800
823
|
throw new Error("Controller store has no schema identifier");
|
|
801
824
|
}
|
|
802
|
-
|
|
825
|
+
} else {
|
|
826
|
+
const existingQueue = this.database
|
|
827
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_queue'")
|
|
828
|
+
.get();
|
|
829
|
+
if (existingQueue !== undefined) this.assertAlphaSchemaLayout();
|
|
830
|
+
this.transaction(() => {
|
|
831
|
+
this.database
|
|
832
|
+
.prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
|
|
833
|
+
.run(CONTROLLER_STORE_SCHEMA);
|
|
834
|
+
this.database.exec(SCHEMA_SQL);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
this.assertAlphaSchemaLayout();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private assertAlphaSchemaLayout(): void {
|
|
841
|
+
const requiredQueueColumns = new Set([
|
|
842
|
+
"run_id",
|
|
843
|
+
"workflow_ref",
|
|
844
|
+
"workflow_path",
|
|
845
|
+
"workflow_source_json",
|
|
846
|
+
"definition_digest",
|
|
847
|
+
"input_json",
|
|
848
|
+
"launch_options_json",
|
|
849
|
+
"status",
|
|
850
|
+
"runner_id",
|
|
851
|
+
"claim_token",
|
|
852
|
+
"claim_expires_at",
|
|
853
|
+
"affinity_runner_id",
|
|
854
|
+
"origin_session_id",
|
|
855
|
+
"parent_run_id",
|
|
856
|
+
"error_code",
|
|
857
|
+
"error_message",
|
|
858
|
+
"created_at",
|
|
859
|
+
"updated_at",
|
|
860
|
+
"started_at",
|
|
861
|
+
"finished_at",
|
|
862
|
+
]);
|
|
863
|
+
const actual = new Set(
|
|
864
|
+
(
|
|
865
|
+
this.database.pragma("table_info(workflow_run_queue)") as {
|
|
866
|
+
name: string;
|
|
867
|
+
}[]
|
|
868
|
+
).map((column) => column.name),
|
|
869
|
+
);
|
|
870
|
+
const missing = [...requiredQueueColumns].filter((column) => !actual.has(column));
|
|
871
|
+
if (missing.length > 0) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
"Controller store uses an incompatible alpha layout. Preserve needed run bundles, then reset the project controller store.",
|
|
874
|
+
);
|
|
803
875
|
}
|
|
804
|
-
this.transaction(() => {
|
|
805
|
-
this.database
|
|
806
|
-
.prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
|
|
807
|
-
.run(CONTROLLER_STORE_SCHEMA);
|
|
808
|
-
this.database.exec(SCHEMA_SQL);
|
|
809
|
-
const queueColumns = this.database.pragma("table_info(workflow_run_queue)") as {
|
|
810
|
-
name: string;
|
|
811
|
-
}[];
|
|
812
|
-
if (!queueColumns.some((column) => column.name === "origin_session_id")) {
|
|
813
|
-
this.database.exec("ALTER TABLE workflow_run_queue ADD COLUMN origin_session_id TEXT");
|
|
814
|
-
}
|
|
815
|
-
this.database.exec("DROP TABLE IF EXISTS session_watermarks");
|
|
816
|
-
});
|
|
817
876
|
}
|
|
818
877
|
|
|
819
878
|
private transaction<T>(task: () => T): T {
|
|
@@ -901,15 +960,68 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
901
960
|
return workflow;
|
|
902
961
|
}
|
|
903
962
|
|
|
904
|
-
/**
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
963
|
+
/** Reserve a user-started run before the initiating agent turn settles. */
|
|
964
|
+
reserveWorkflowRun(options: {
|
|
965
|
+
runId: string;
|
|
966
|
+
workflowName: string;
|
|
967
|
+
workflowSourceRef: string;
|
|
968
|
+
workflowSource: unknown;
|
|
969
|
+
definitionDigest: string;
|
|
970
|
+
input: unknown;
|
|
971
|
+
launchOptions?: unknown;
|
|
972
|
+
runnerId: string;
|
|
973
|
+
originSessionId: string;
|
|
974
|
+
parentRunId?: string;
|
|
975
|
+
now?: string;
|
|
976
|
+
}): WorkflowRunQueueRecord {
|
|
977
|
+
validateRunId(options.runId);
|
|
978
|
+
validateKey(options.workflowName, "workflow name");
|
|
979
|
+
validateKey(options.workflowSourceRef, "workflow source ref");
|
|
980
|
+
validateKey(options.definitionDigest, "workflow definition digest");
|
|
981
|
+
validateKey(options.runnerId, "runner id");
|
|
982
|
+
validateKey(options.originSessionId, "origin session id");
|
|
983
|
+
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
984
|
+
const sourceJson = canonicalJson(options.workflowSource, "workflow run source");
|
|
985
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
986
|
+
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
987
|
+
validateJsonSize(sourceJson, "Workflow run source", MAX_EVENT_BYTES);
|
|
988
|
+
validateJsonSize(launchJson, "Workflow launch options", MAX_EVENT_BYTES);
|
|
989
|
+
const now = validTimestamp(options.now);
|
|
990
|
+
this.database
|
|
991
|
+
.prepare(
|
|
992
|
+
`INSERT INTO workflow_run_queue (
|
|
993
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
994
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
995
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
996
|
+
created_at, updated_at, started_at, finished_at
|
|
997
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', NULL, NULL, NULL, ?, ?, ?, NULL, NULL, ?, ?, NULL, NULL)`,
|
|
998
|
+
)
|
|
999
|
+
.run(
|
|
1000
|
+
options.runId,
|
|
1001
|
+
options.workflowName,
|
|
1002
|
+
options.workflowSourceRef,
|
|
1003
|
+
sourceJson,
|
|
1004
|
+
options.definitionDigest,
|
|
1005
|
+
inputJson,
|
|
1006
|
+
launchJson,
|
|
1007
|
+
options.runnerId,
|
|
1008
|
+
options.originSessionId,
|
|
1009
|
+
options.parentRunId ?? null,
|
|
1010
|
+
now,
|
|
1011
|
+
now,
|
|
1012
|
+
);
|
|
1013
|
+
return this.requireWorkflowRun(options.runId);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** Insert a run that will start immediately outside a live model turn. */
|
|
908
1017
|
enqueueWorkflowRun(options: {
|
|
909
1018
|
runId: string;
|
|
910
1019
|
workflowName: string;
|
|
911
1020
|
workflowSourceRef: string;
|
|
1021
|
+
workflowSource?: unknown;
|
|
1022
|
+
definitionDigest?: string;
|
|
912
1023
|
input: unknown;
|
|
1024
|
+
launchOptions?: unknown;
|
|
913
1025
|
runnerId: string;
|
|
914
1026
|
claimToken: string;
|
|
915
1027
|
leaseMs: number;
|
|
@@ -928,22 +1040,31 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
928
1040
|
validateKey(options.originSessionId, "origin session id");
|
|
929
1041
|
}
|
|
930
1042
|
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
1043
|
+
const sourceJson = canonicalJson(
|
|
1044
|
+
options.workflowSource ?? { ref: options.workflowSourceRef },
|
|
1045
|
+
"workflow run source",
|
|
1046
|
+
);
|
|
1047
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
931
1048
|
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
932
1049
|
const now = validTimestamp(options.now);
|
|
933
1050
|
const expiresAt = epoch(now) + options.leaseMs;
|
|
934
1051
|
this.database
|
|
935
1052
|
.prepare(
|
|
936
1053
|
`INSERT INTO workflow_run_queue (
|
|
937
|
-
run_id, workflow_ref, workflow_path,
|
|
938
|
-
runner_id, claim_token, claim_expires_at,
|
|
939
|
-
origin_session_id, parent_run_id,
|
|
940
|
-
|
|
1054
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
1055
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
1056
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
1057
|
+
created_at, updated_at, started_at, finished_at
|
|
1058
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, NULL)`,
|
|
941
1059
|
)
|
|
942
1060
|
.run(
|
|
943
1061
|
options.runId,
|
|
944
1062
|
options.workflowName,
|
|
945
1063
|
options.workflowSourceRef,
|
|
1064
|
+
sourceJson,
|
|
1065
|
+
options.definitionDigest ?? "unavailable",
|
|
946
1066
|
inputJson,
|
|
1067
|
+
launchJson,
|
|
947
1068
|
options.runnerId,
|
|
948
1069
|
options.claimToken,
|
|
949
1070
|
expiresAt,
|
|
@@ -952,6 +1073,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
952
1073
|
options.parentRunId ?? null,
|
|
953
1074
|
now,
|
|
954
1075
|
now,
|
|
1076
|
+
now,
|
|
955
1077
|
);
|
|
956
1078
|
return this.requireWorkflowRun(options.runId);
|
|
957
1079
|
}
|
|
@@ -978,6 +1100,58 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
978
1100
|
return rows.map(workflowRunFromRow);
|
|
979
1101
|
}
|
|
980
1102
|
|
|
1103
|
+
findSessionReservation(sessionId: string): WorkflowRunQueueRecord | undefined {
|
|
1104
|
+
validateKey(sessionId, "session id");
|
|
1105
|
+
const row = this.database
|
|
1106
|
+
.prepare(
|
|
1107
|
+
`SELECT * FROM workflow_run_queue
|
|
1108
|
+
WHERE origin_session_id = ? AND status IN ('queued', 'starting', 'running')
|
|
1109
|
+
ORDER BY created_at LIMIT 1`,
|
|
1110
|
+
)
|
|
1111
|
+
.get(sessionId) as WorkflowRunQueueRow | undefined;
|
|
1112
|
+
return row === undefined ? undefined : workflowRunFromRow(row);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
claimWorkflowRun(options: {
|
|
1116
|
+
runId: string;
|
|
1117
|
+
runnerId: string;
|
|
1118
|
+
claimToken: string;
|
|
1119
|
+
leaseMs: number;
|
|
1120
|
+
now?: string;
|
|
1121
|
+
}): WorkflowRunQueueRecord | undefined {
|
|
1122
|
+
validateRunId(options.runId);
|
|
1123
|
+
validateKey(options.runnerId, "runner id");
|
|
1124
|
+
validateKey(options.claimToken, "claim token");
|
|
1125
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1126
|
+
const now = validTimestamp(options.now);
|
|
1127
|
+
const nowMs = epoch(now);
|
|
1128
|
+
const expiresAt = nowMs + options.leaseMs;
|
|
1129
|
+
const result = this.database
|
|
1130
|
+
.prepare(
|
|
1131
|
+
`UPDATE workflow_run_queue
|
|
1132
|
+
SET status = 'starting', runner_id = ?, claim_token = ?, claim_expires_at = ?,
|
|
1133
|
+
started_at = COALESCE(started_at, ?), updated_at = ?
|
|
1134
|
+
WHERE run_id = ? AND (
|
|
1135
|
+
status IN ('queued', 'parked')
|
|
1136
|
+
OR (status = 'starting' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1137
|
+
)`,
|
|
1138
|
+
)
|
|
1139
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, options.runId, nowMs);
|
|
1140
|
+
return result.changes === 1 ? this.requireWorkflowRun(options.runId) : undefined;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
markWorkflowRunRunning(options: { runId: string; claimToken: string; now?: string }): boolean {
|
|
1144
|
+
validateRunId(options.runId);
|
|
1145
|
+
const now = validTimestamp(options.now);
|
|
1146
|
+
const result = this.database
|
|
1147
|
+
.prepare(
|
|
1148
|
+
`UPDATE workflow_run_queue SET status = 'running', updated_at = ?
|
|
1149
|
+
WHERE run_id = ? AND claim_token = ? AND status = 'starting'`,
|
|
1150
|
+
)
|
|
1151
|
+
.run(now, options.runId, options.claimToken);
|
|
1152
|
+
return result.changes === 1;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
981
1155
|
/**
|
|
982
1156
|
* Claim the oldest claimable run, preferring runs with affinity to this
|
|
983
1157
|
* runner. Parked rows are claimable immediately; claimed rows become
|
|
@@ -1008,11 +1182,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1008
1182
|
excluded.length === 0 ? "" : `AND run_id NOT IN (${excluded.map(() => "?").join(", ")})`;
|
|
1009
1183
|
const sessionFilter =
|
|
1010
1184
|
options.sessionId === undefined
|
|
1011
|
-
? ""
|
|
1185
|
+
? "AND status != 'queued' AND (status != 'starting' OR origin_session_id IS NULL)"
|
|
1012
1186
|
: "AND (origin_session_id IS NULL OR origin_session_id = ?)";
|
|
1013
1187
|
const claimable = `(
|
|
1014
|
-
status
|
|
1015
|
-
OR (status
|
|
1188
|
+
status IN ('queued', 'parked')
|
|
1189
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1016
1190
|
)`;
|
|
1017
1191
|
return this.transaction(() => {
|
|
1018
1192
|
const candidate = this.database
|
|
@@ -1036,14 +1210,14 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1036
1210
|
const result = this.database
|
|
1037
1211
|
.prepare(
|
|
1038
1212
|
`UPDATE workflow_run_queue
|
|
1039
|
-
SET status = '
|
|
1040
|
-
claim_expires_at = ?, updated_at = ?
|
|
1213
|
+
SET status = 'starting', runner_id = ?, claim_token = ?,
|
|
1214
|
+
claim_expires_at = ?, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
1041
1215
|
WHERE run_id = ? AND (
|
|
1042
|
-
status
|
|
1043
|
-
OR (status
|
|
1216
|
+
status IN ('queued', 'parked')
|
|
1217
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1044
1218
|
)`,
|
|
1045
1219
|
)
|
|
1046
|
-
.run(options.runnerId, options.claimToken, expiresAt, now, candidate.run_id, nowMs);
|
|
1220
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, candidate.run_id, nowMs);
|
|
1047
1221
|
return result.changes === 1 ? this.requireWorkflowRun(candidate.run_id) : undefined;
|
|
1048
1222
|
});
|
|
1049
1223
|
}
|
|
@@ -1062,7 +1236,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1062
1236
|
const result = this.database
|
|
1063
1237
|
.prepare(
|
|
1064
1238
|
`UPDATE workflow_run_queue SET claim_expires_at = ?
|
|
1065
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1239
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
1066
1240
|
AND claim_expires_at > ?`,
|
|
1067
1241
|
)
|
|
1068
1242
|
.run(expiresAt, options.runId, options.claimToken, nowMs);
|
|
@@ -1076,7 +1250,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1076
1250
|
const row = this.database
|
|
1077
1251
|
.prepare(
|
|
1078
1252
|
`SELECT 1 AS live FROM workflow_run_queue
|
|
1079
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1253
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
1080
1254
|
AND claim_expires_at > ?`,
|
|
1081
1255
|
)
|
|
1082
1256
|
.get(options.runId, options.claimToken, nowMs);
|
|
@@ -1092,12 +1266,68 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1092
1266
|
`UPDATE workflow_run_queue
|
|
1093
1267
|
SET status = 'parked', runner_id = NULL, claim_token = NULL,
|
|
1094
1268
|
claim_expires_at = NULL, updated_at = ?
|
|
1095
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1269
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`,
|
|
1096
1270
|
)
|
|
1097
1271
|
.run(now, options.runId, options.claimToken);
|
|
1098
1272
|
return result.changes === 1;
|
|
1099
1273
|
}
|
|
1100
1274
|
|
|
1275
|
+
failWorkflowRun(options: {
|
|
1276
|
+
runId: string;
|
|
1277
|
+
claimToken?: string;
|
|
1278
|
+
errorCode: string;
|
|
1279
|
+
errorMessage: string;
|
|
1280
|
+
now?: string;
|
|
1281
|
+
}): boolean {
|
|
1282
|
+
validateRunId(options.runId);
|
|
1283
|
+
validateKey(options.errorCode, "workflow launch error code");
|
|
1284
|
+
const message = options.errorMessage.trim();
|
|
1285
|
+
if (message.length === 0 || Buffer.byteLength(message) > 2_048) {
|
|
1286
|
+
throw new Error("Workflow launch error message must be between 1 and 2048 bytes");
|
|
1287
|
+
}
|
|
1288
|
+
const now = validTimestamp(options.now);
|
|
1289
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
1290
|
+
const result = this.database
|
|
1291
|
+
.prepare(
|
|
1292
|
+
`UPDATE workflow_run_queue
|
|
1293
|
+
SET status = 'failed', runner_id = NULL, claim_token = NULL,
|
|
1294
|
+
claim_expires_at = NULL, parent_run_id = NULL, input_json = 'null',
|
|
1295
|
+
launch_options_json = '{}', error_code = ?, error_message = ?,
|
|
1296
|
+
finished_at = ?, updated_at = ?
|
|
1297
|
+
WHERE run_id = ? AND status IN ('queued', 'starting', 'running') ${claimFilter}`,
|
|
1298
|
+
)
|
|
1299
|
+
.run(
|
|
1300
|
+
options.errorCode,
|
|
1301
|
+
message,
|
|
1302
|
+
now,
|
|
1303
|
+
now,
|
|
1304
|
+
options.runId,
|
|
1305
|
+
...(options.claimToken === undefined ? [] : [options.claimToken]),
|
|
1306
|
+
);
|
|
1307
|
+
return result.changes === 1;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
cancelWorkflowRun(options: { runId: string; claimToken?: string; now?: string }): boolean {
|
|
1311
|
+
validateRunId(options.runId);
|
|
1312
|
+
const now = validTimestamp(options.now);
|
|
1313
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
1314
|
+
const result = this.database
|
|
1315
|
+
.prepare(
|
|
1316
|
+
`UPDATE workflow_run_queue
|
|
1317
|
+
SET status = 'cancelled', runner_id = NULL, claim_token = NULL,
|
|
1318
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
1319
|
+
finished_at = ?, updated_at = ?
|
|
1320
|
+
WHERE run_id = ? AND status IN ('queued', 'starting') ${claimFilter}`,
|
|
1321
|
+
)
|
|
1322
|
+
.run(
|
|
1323
|
+
now,
|
|
1324
|
+
now,
|
|
1325
|
+
options.runId,
|
|
1326
|
+
...(options.claimToken === undefined ? [] : [options.claimToken]),
|
|
1327
|
+
);
|
|
1328
|
+
return result.changes === 1;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1101
1331
|
/**
|
|
1102
1332
|
* Delete a claimed row. Used when a continuation fails before its bundle
|
|
1103
1333
|
* exists, so the parent's one-continuation slot is not consumed by a run
|
|
@@ -1119,10 +1349,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1119
1349
|
.prepare(
|
|
1120
1350
|
`UPDATE workflow_run_queue
|
|
1121
1351
|
SET status = 'done', runner_id = NULL, claim_token = NULL,
|
|
1122
|
-
claim_expires_at = NULL,
|
|
1123
|
-
|
|
1352
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
1353
|
+
finished_at = ?, updated_at = ?
|
|
1354
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`,
|
|
1124
1355
|
)
|
|
1125
|
-
.run(now, options.runId, options.claimToken);
|
|
1356
|
+
.run(now, now, options.runId, options.claimToken);
|
|
1126
1357
|
return result.changes === 1;
|
|
1127
1358
|
}
|
|
1128
1359
|
|
|
@@ -1149,26 +1380,28 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1149
1380
|
const row = this.database
|
|
1150
1381
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
1151
1382
|
.get(options.runId) as WorkflowRunQueueRow | undefined;
|
|
1152
|
-
if (row === undefined ||
|
|
1383
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1153
1386
|
if (row.workflow_path === options.workflowSourceRef && row.status === "parked") {
|
|
1154
1387
|
return "unchanged";
|
|
1155
1388
|
}
|
|
1156
1389
|
const claimable =
|
|
1157
1390
|
row.status === "parked" ||
|
|
1158
|
-
(row.status === "
|
|
1391
|
+
(row.status === "starting" &&
|
|
1159
1392
|
row.claim_token === options.claimToken &&
|
|
1160
1393
|
row.claim_expires_at !== null &&
|
|
1161
1394
|
row.claim_expires_at > nowMs) ||
|
|
1162
|
-
(row.status === "
|
|
1395
|
+
(row.status === "starting" &&
|
|
1163
1396
|
row.claim_expires_at !== null &&
|
|
1164
1397
|
row.claim_expires_at <= nowMs);
|
|
1165
1398
|
if (!claimable) return false;
|
|
1166
1399
|
const result = this.database
|
|
1167
1400
|
.prepare(
|
|
1168
1401
|
`UPDATE workflow_run_queue
|
|
1169
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
1402
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
1170
1403
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
1171
|
-
WHERE run_id = ? AND status
|
|
1404
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`,
|
|
1172
1405
|
)
|
|
1173
1406
|
.run(
|
|
1174
1407
|
options.workflowName,
|
|
@@ -1208,26 +1441,28 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1208
1441
|
const row = this.database
|
|
1209
1442
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
1210
1443
|
.get(options.runId) as WorkflowRunQueueRow | undefined;
|
|
1211
|
-
if (row === undefined ||
|
|
1444
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1212
1447
|
const sourceMatches =
|
|
1213
1448
|
row.workflow_path === options.oldWorkflowPath ||
|
|
1214
1449
|
row.workflow_path === options.workflowSourceRef;
|
|
1215
1450
|
const claimable =
|
|
1216
1451
|
row.status === "parked" ||
|
|
1217
|
-
(row.status === "
|
|
1452
|
+
(row.status === "starting" &&
|
|
1218
1453
|
row.claim_token === options.claimToken &&
|
|
1219
1454
|
row.claim_expires_at !== null &&
|
|
1220
1455
|
row.claim_expires_at > nowMs) ||
|
|
1221
|
-
(row.status === "
|
|
1456
|
+
(row.status === "starting" &&
|
|
1222
1457
|
row.claim_expires_at !== null &&
|
|
1223
1458
|
row.claim_expires_at <= nowMs);
|
|
1224
1459
|
if (!sourceMatches || !claimable) return false;
|
|
1225
1460
|
const result = this.database
|
|
1226
1461
|
.prepare(
|
|
1227
1462
|
`UPDATE workflow_run_queue
|
|
1228
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
1463
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
1229
1464
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
1230
|
-
WHERE run_id = ? AND status
|
|
1465
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`,
|
|
1231
1466
|
)
|
|
1232
1467
|
.run(
|
|
1233
1468
|
options.workflowName,
|
|
@@ -1297,7 +1532,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1297
1532
|
attemptId: string;
|
|
1298
1533
|
notificationIndex: number;
|
|
1299
1534
|
targetSessionId: string;
|
|
1300
|
-
kind: "progress" | "final";
|
|
1535
|
+
kind: "progress" | "final" | "launch_failure";
|
|
1301
1536
|
content: string;
|
|
1302
1537
|
notificationId?: string;
|
|
1303
1538
|
now?: string;
|
|
@@ -1483,19 +1718,31 @@ const SCHEMA_SQL = `
|
|
|
1483
1718
|
run_id TEXT PRIMARY KEY,
|
|
1484
1719
|
workflow_ref TEXT NOT NULL,
|
|
1485
1720
|
workflow_path TEXT NOT NULL,
|
|
1721
|
+
workflow_source_json TEXT NOT NULL,
|
|
1722
|
+
definition_digest TEXT NOT NULL,
|
|
1486
1723
|
input_json TEXT NOT NULL,
|
|
1487
|
-
|
|
1724
|
+
launch_options_json TEXT NOT NULL,
|
|
1725
|
+
status TEXT NOT NULL CHECK (
|
|
1726
|
+
status IN ('queued', 'starting', 'running', 'parked', 'done', 'failed', 'cancelled')
|
|
1727
|
+
),
|
|
1488
1728
|
runner_id TEXT,
|
|
1489
1729
|
claim_token TEXT,
|
|
1490
1730
|
claim_expires_at INTEGER,
|
|
1491
1731
|
affinity_runner_id TEXT,
|
|
1492
1732
|
origin_session_id TEXT,
|
|
1493
1733
|
parent_run_id TEXT,
|
|
1734
|
+
error_code TEXT,
|
|
1735
|
+
error_message TEXT,
|
|
1494
1736
|
created_at TEXT NOT NULL,
|
|
1495
|
-
updated_at TEXT NOT NULL
|
|
1737
|
+
updated_at TEXT NOT NULL,
|
|
1738
|
+
started_at TEXT,
|
|
1739
|
+
finished_at TEXT
|
|
1496
1740
|
);
|
|
1497
1741
|
CREATE INDEX IF NOT EXISTS workflow_run_queue_claimable
|
|
1498
1742
|
ON workflow_run_queue(status, claim_expires_at);
|
|
1743
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_session_reservation
|
|
1744
|
+
ON workflow_run_queue(origin_session_id)
|
|
1745
|
+
WHERE origin_session_id IS NOT NULL AND status IN ('queued', 'starting', 'running');
|
|
1499
1746
|
-- A checkpointed parent admits exactly one continuation run, across
|
|
1500
1747
|
-- sessions and processes.
|
|
1501
1748
|
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_parent
|
|
@@ -1519,7 +1766,7 @@ const SCHEMA_SQL = `
|
|
|
1519
1766
|
attempt_id TEXT NOT NULL,
|
|
1520
1767
|
notification_index INTEGER NOT NULL CHECK (notification_index > 0),
|
|
1521
1768
|
target_session_id TEXT NOT NULL,
|
|
1522
|
-
kind TEXT NOT NULL CHECK (kind IN ('progress', 'final')),
|
|
1769
|
+
kind TEXT NOT NULL CHECK (kind IN ('progress', 'final', 'launch_failure')),
|
|
1523
1770
|
content TEXT NOT NULL,
|
|
1524
1771
|
created_at TEXT NOT NULL,
|
|
1525
1772
|
delivery_claim_token TEXT,
|
|
@@ -1643,7 +1890,10 @@ function workflowRunFromRow(row: WorkflowRunQueueRow): WorkflowRunQueueRecord {
|
|
|
1643
1890
|
runId: row.run_id,
|
|
1644
1891
|
workflowName: row.workflow_ref,
|
|
1645
1892
|
workflowSourceRef: row.workflow_path,
|
|
1893
|
+
workflowSource: parseStoredJson(row.workflow_source_json, "workflow run source"),
|
|
1894
|
+
definitionDigest: row.definition_digest,
|
|
1646
1895
|
input: parseStoredJson(row.input_json, "workflow run input"),
|
|
1896
|
+
launchOptions: parseStoredJson(row.launch_options_json, "workflow launch options"),
|
|
1647
1897
|
status: row.status,
|
|
1648
1898
|
runnerId: row.runner_id,
|
|
1649
1899
|
claimToken: row.claim_token,
|
|
@@ -1651,8 +1901,12 @@ function workflowRunFromRow(row: WorkflowRunQueueRow): WorkflowRunQueueRecord {
|
|
|
1651
1901
|
affinityRunnerId: row.affinity_runner_id,
|
|
1652
1902
|
originSessionId: row.origin_session_id,
|
|
1653
1903
|
parentRunId: row.parent_run_id,
|
|
1904
|
+
errorCode: row.error_code,
|
|
1905
|
+
errorMessage: row.error_message,
|
|
1654
1906
|
createdAt: row.created_at,
|
|
1655
1907
|
updatedAt: row.updated_at,
|
|
1908
|
+
startedAt: row.started_at,
|
|
1909
|
+
finishedAt: row.finished_at,
|
|
1656
1910
|
};
|
|
1657
1911
|
}
|
|
1658
1912
|
|