@agimon-ai/po-workflow 0.0.0-alpha.79 → 0.0.0-alpha.80
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/{RecoverWorkflowService-lWxTE3o6.cjs → RecoverWorkflowService-C-iiu-ku.cjs} +432 -133
- package/dist/RecoverWorkflowService-C-iiu-ku.cjs.map +1 -0
- package/dist/{RecoverWorkflowService-DVRXEeCw.mjs → RecoverWorkflowService-D-Rr23lN.mjs} +432 -133
- package/dist/RecoverWorkflowService-D-Rr23lN.mjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/extensions/server.cjs +420 -38
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +421 -39
- package/dist/extensions/server.mjs.map +1 -1
- package/package.json +13 -12
- package/dist/RecoverWorkflowService-DVRXEeCw.mjs.map +0 -1
- package/dist/RecoverWorkflowService-lWxTE3o6.cjs.map +0 -1
|
@@ -608,8 +608,50 @@ const WorkflowLauncherMetadataSchema = zod.z.discriminatedUnion("type", [
|
|
|
608
608
|
TmuxWorkflowLauncherMetadataSchema,
|
|
609
609
|
NativeWorkflowLauncherMetadataSchema
|
|
610
610
|
]);
|
|
611
|
+
/** Only trusted in-process launchers set this metadata, never workflow environment. */
|
|
612
|
+
const WorkflowAdmissionMetadataSchema = zod.z.object({
|
|
613
|
+
attemptId: zod.z.string().min(1),
|
|
614
|
+
admissionId: zod.z.string().uuid(),
|
|
615
|
+
ownerSessionId: zod.z.string().min(1),
|
|
616
|
+
launcherSessionId: zod.z.string().min(1),
|
|
617
|
+
reason: zod.z.enum([
|
|
618
|
+
"launch",
|
|
619
|
+
"recover",
|
|
620
|
+
"startup",
|
|
621
|
+
"resume"
|
|
622
|
+
]),
|
|
623
|
+
previousRun: zod.z.object({
|
|
624
|
+
runId: zod.z.string(),
|
|
625
|
+
ownerSessionId: zod.z.string(),
|
|
626
|
+
runKey: zod.z.string(),
|
|
627
|
+
registryWorkspace: zod.z.string()
|
|
628
|
+
}).optional(),
|
|
629
|
+
environment: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
630
|
+
ready: zod.z.boolean().optional(),
|
|
631
|
+
binding: zod.z.object({
|
|
632
|
+
association: zod.z.object({
|
|
633
|
+
coordinator: zod.z.string().min(1),
|
|
634
|
+
key: zod.z.string().min(1)
|
|
635
|
+
}),
|
|
636
|
+
epoch: zod.z.object({
|
|
637
|
+
admissionId: zod.z.string().uuid(),
|
|
638
|
+
operationId: zod.z.string().uuid()
|
|
639
|
+
})
|
|
640
|
+
}).optional()
|
|
641
|
+
});
|
|
642
|
+
const WorkflowAdmissionIntentSchema = zod.z.object({
|
|
643
|
+
metadata: WorkflowAdmissionMetadataSchema,
|
|
644
|
+
requestHash: zod.z.string(),
|
|
645
|
+
rejected: zod.z.array(zod.z.string()).optional()
|
|
646
|
+
});
|
|
611
647
|
const WorkflowRunRecordSchema = zod.z.object({
|
|
612
648
|
activeRepairId: zod.z.string().regex(/^\d+$/).optional(),
|
|
649
|
+
admission: WorkflowAdmissionMetadataSchema.optional(),
|
|
650
|
+
archivedProgress: zod.z.array(WorkflowProgressEventSchema).optional(),
|
|
651
|
+
configuration: zod.z.object({
|
|
652
|
+
revision: zod.z.string(),
|
|
653
|
+
workflow: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
654
|
+
}).optional(),
|
|
613
655
|
/** Read-only legacy metadata. New runs only write runner. */
|
|
614
656
|
cliAgent: zod.z.string().optional(),
|
|
615
657
|
continueOnError: zod.z.boolean().optional(),
|
|
@@ -1016,6 +1058,193 @@ var WorkflowRegistryService = class {
|
|
|
1016
1058
|
resolveWorkspace(explicitWorkspace, workflowWorkspace) {
|
|
1017
1059
|
return this.slugifySegment(explicitWorkspace || workflowWorkspace || DEFAULT_WORKSPACE$1, DEFAULT_WORKSPACE$1);
|
|
1018
1060
|
}
|
|
1061
|
+
/** The journal is outside reusable and movable run directories. Exclusive publish is the CAS. */
|
|
1062
|
+
async reserveAdmission(intent) {
|
|
1063
|
+
intent = WorkflowAdmissionIntentSchema.parse(intent);
|
|
1064
|
+
const directory = (0, node_path.resolve)(this.homeDir, "admissions");
|
|
1065
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.createDir(directory, { recursive: true }, this.runtime.ioContext));
|
|
1066
|
+
const file = this.admissionPath(intent.metadata.launcherSessionId, intent.metadata.attemptId);
|
|
1067
|
+
try {
|
|
1068
|
+
await this.writeExclusiveJson(file, intent);
|
|
1069
|
+
return {
|
|
1070
|
+
intent,
|
|
1071
|
+
fresh: true
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (error.code !== "EEXIST") throw error;
|
|
1075
|
+
const existing = await this.readAdmission(intent.metadata.launcherSessionId, intent.metadata.attemptId);
|
|
1076
|
+
if (!existing || existing.requestHash !== intent.requestHash) throw new Error("Workflow attempt ID was reused with a different request.");
|
|
1077
|
+
return {
|
|
1078
|
+
intent: existing,
|
|
1079
|
+
fresh: false
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async readAdmission(launcher, attempt) {
|
|
1084
|
+
const file = this.admissionPath(launcher, attempt);
|
|
1085
|
+
try {
|
|
1086
|
+
return WorkflowAdmissionIntentSchema.parse(JSON.parse((0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.readTextFile(file, this.runtime.ioContext))));
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (["ENOENT", "not_found"].includes(String(error.code))) return void 0;
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async rejectAdmission(intent, errors) {
|
|
1093
|
+
const file = this.admissionPath(intent.metadata.launcherSessionId, intent.metadata.attemptId);
|
|
1094
|
+
await this.writeAtomic(file, {
|
|
1095
|
+
...intent,
|
|
1096
|
+
rejected: errors
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
admissionPath(launcher, attempt) {
|
|
1100
|
+
return (0, node_path.resolve)(this.homeDir, "admissions", `${(0, node_crypto.createHash)("sha256").update(JSON.stringify([launcher, attempt])).digest("hex")}.json`);
|
|
1101
|
+
}
|
|
1102
|
+
runRef(record) {
|
|
1103
|
+
const ownerSessionId = record.admission?.ownerSessionId ?? record.env?.PO_SESSION_ID;
|
|
1104
|
+
return record.runId && ownerSessionId ? {
|
|
1105
|
+
runId: record.runId,
|
|
1106
|
+
runKey: record.runKey,
|
|
1107
|
+
ownerSessionId,
|
|
1108
|
+
registryWorkspace: record.workspace
|
|
1109
|
+
} : void 0;
|
|
1110
|
+
}
|
|
1111
|
+
/** Exact-ID evidence survives replay, deletion and a lost secondary admission index write. */
|
|
1112
|
+
async readRunEvidence(runId) {
|
|
1113
|
+
const current = (await this.listRuns()).find((record) => record.runId === runId);
|
|
1114
|
+
if (current) return current;
|
|
1115
|
+
const file = (0, node_path.resolve)(this.homeDir, "receipts", `${(0, node_crypto.createHash)("sha256").update(runId).digest("hex")}.json`);
|
|
1116
|
+
if (!await this.pathExists(file)) return void 0;
|
|
1117
|
+
return this.readRunRecord(file);
|
|
1118
|
+
}
|
|
1119
|
+
async listRunEvidence() {
|
|
1120
|
+
const directory = (0, node_path.resolve)(this.homeDir, "receipts");
|
|
1121
|
+
if (!await this.pathExists(directory)) return this.listRuns();
|
|
1122
|
+
const entries = (0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.listDir(directory, this.runtime.ioContext));
|
|
1123
|
+
const archived = await Promise.all(entries.filter((entry) => entry.name.endsWith(".json")).map((entry) => this.readRunRecord((0, node_path.resolve)(directory, entry.name))));
|
|
1124
|
+
return [...new Map([...archived, ...await this.listRuns()].map((record) => [record.runId, record])).values()];
|
|
1125
|
+
}
|
|
1126
|
+
async preserveRunEvidence(record, runDir = this.runDirectoryFor(record)) {
|
|
1127
|
+
if (!record.runId || record.stage === RUNNING_STAGE) return;
|
|
1128
|
+
const directory = (0, node_path.resolve)(this.homeDir, "receipts");
|
|
1129
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.createDir(directory, { recursive: true }, this.runtime.ioContext));
|
|
1130
|
+
const file = (0, node_path.resolve)(directory, `${(0, node_crypto.createHash)("sha256").update(record.runId).digest("hex")}.json`);
|
|
1131
|
+
if (await this.pathExists(file)) return;
|
|
1132
|
+
const progress = await readWorkflowProgress(this.runtime, runDir);
|
|
1133
|
+
try {
|
|
1134
|
+
await this.writeExclusiveJson(file, {
|
|
1135
|
+
...record,
|
|
1136
|
+
archivedProgress: progress.filter((event) => Date.parse(event.at) >= Date.parse(record.startedAt))
|
|
1137
|
+
});
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
if (error.code !== "EEXIST") throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/** All registry instances use the same stable workspace/run-key lock, including while stage dirs move. */
|
|
1143
|
+
async withRunLock(recordPath, action) {
|
|
1144
|
+
return this.writeCoordinator.forKey(recordPath)(async () => {
|
|
1145
|
+
const runDir = (0, node_path.dirname)(recordPath);
|
|
1146
|
+
const workspaceDir = (0, node_path.dirname)((0, node_path.dirname)(runDir));
|
|
1147
|
+
const lockPath = (0, node_path.resolve)(workspaceDir, `${(0, node_crypto.createHash)("sha256").update(runDir.split("/").at(-1)).digest("hex")}.operation.lock`);
|
|
1148
|
+
const release = await this.runtime.workflowHost.acquireRecoveryLock(lockPath, {
|
|
1149
|
+
lockfilePath: lockPath,
|
|
1150
|
+
realpath: false,
|
|
1151
|
+
retries: {
|
|
1152
|
+
retries: 100,
|
|
1153
|
+
minTimeout: 10,
|
|
1154
|
+
maxTimeout: 10
|
|
1155
|
+
},
|
|
1156
|
+
stale: DEFAULT_RECOVERY_CLAIM_TTL_MS
|
|
1157
|
+
});
|
|
1158
|
+
try {
|
|
1159
|
+
return await action();
|
|
1160
|
+
} finally {
|
|
1161
|
+
await release();
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
/** Persist the epoch under the authoritative writer, await policy unlocked, then fence and acknowledge atomically. */
|
|
1166
|
+
async authorizeExecution(handle, resume = false) {
|
|
1167
|
+
const prepared = await this.withRunLock(handle.recordPath, async () => {
|
|
1168
|
+
const current = await this.readRunRecord(handle.recordPath);
|
|
1169
|
+
if (current.runId !== handle.runId || current.stage !== RUNNING_STAGE) throw new Error("Workflow generation changed before policy admission.");
|
|
1170
|
+
const request = resume ? await this.readPauseRequest(handle) : void 0;
|
|
1171
|
+
if (resume && (request?.action !== "resume" || current.executionState !== "resume_requested" && current.executionState !== "paused")) throw new Error("Workflow resume request changed.");
|
|
1172
|
+
if (await this.hasStopRequest(handle)) throw new Error("Workflow was stopped before policy admission.");
|
|
1173
|
+
if (!current.admission?.binding) return {
|
|
1174
|
+
record: current,
|
|
1175
|
+
request
|
|
1176
|
+
};
|
|
1177
|
+
const admission = structuredClone(current.admission);
|
|
1178
|
+
if (resume) {
|
|
1179
|
+
admission.reason = "resume";
|
|
1180
|
+
admission.binding.epoch.operationId = (0, node_crypto.randomUUID)();
|
|
1181
|
+
}
|
|
1182
|
+
admission.ready = false;
|
|
1183
|
+
const record = {
|
|
1184
|
+
...current,
|
|
1185
|
+
admission
|
|
1186
|
+
};
|
|
1187
|
+
await this.writeRunRecord(handle.recordPath, record);
|
|
1188
|
+
return {
|
|
1189
|
+
record,
|
|
1190
|
+
request
|
|
1191
|
+
};
|
|
1192
|
+
});
|
|
1193
|
+
const admission = prepared.record.admission;
|
|
1194
|
+
let environment = {};
|
|
1195
|
+
if (admission?.binding) {
|
|
1196
|
+
const before = this.runtime.beforeWorkflowExecution;
|
|
1197
|
+
if (!before) throw new Error(`Workflow policy coordinator is unavailable: ${admission.binding.association.coordinator}`);
|
|
1198
|
+
const signal = this.runtime.workflowSignal ?? new AbortController().signal;
|
|
1199
|
+
signal.throwIfAborted();
|
|
1200
|
+
const result = await before({
|
|
1201
|
+
binding: structuredClone(admission.binding),
|
|
1202
|
+
reason: admission.reason,
|
|
1203
|
+
ownerSessionId: admission.ownerSessionId,
|
|
1204
|
+
launcherSessionId: admission.launcherSessionId,
|
|
1205
|
+
run: this.runRef(prepared.record),
|
|
1206
|
+
previousRun: admission.previousRun,
|
|
1207
|
+
signal
|
|
1208
|
+
});
|
|
1209
|
+
signal.throwIfAborted();
|
|
1210
|
+
environment = result?.environment ?? {};
|
|
1211
|
+
for (const [key, value] of Object.entries(environment)) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || /^(PO_|WORKFLOW_)/.test(key) || typeof value !== "string") throw new Error("Invalid workflow policy environment patch.");
|
|
1212
|
+
} else if (!resume) return prepared.record;
|
|
1213
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1214
|
+
const current = await this.readRunRecord(handle.recordPath);
|
|
1215
|
+
const request = resume ? await this.readPauseRequest(handle) : void 0;
|
|
1216
|
+
if (current.runId !== handle.runId || current.stage !== RUNNING_STAGE || current.admission?.binding?.epoch.operationId !== admission?.binding?.epoch.operationId || resume && (current.executionState !== prepared.record.executionState || request?.action !== "resume" || request.requestId !== prepared.request?.requestId) || await this.hasStopRequest(handle)) throw new Error("Workflow policy operation changed or stopped.");
|
|
1217
|
+
const updated = { ...current };
|
|
1218
|
+
if (admission?.binding) {
|
|
1219
|
+
updated.admission = {
|
|
1220
|
+
...current.admission,
|
|
1221
|
+
ready: true,
|
|
1222
|
+
environment: {
|
|
1223
|
+
...current.admission?.environment,
|
|
1224
|
+
...environment
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
updated.env = {
|
|
1228
|
+
...current.env,
|
|
1229
|
+
...environment
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
if (resume) {
|
|
1233
|
+
const cursor = current.executionCursor;
|
|
1234
|
+
updated.executionState = "running";
|
|
1235
|
+
updated.executionCursor = {
|
|
1236
|
+
...cursor,
|
|
1237
|
+
phase: cursor?.phase ?? "step",
|
|
1238
|
+
resumeMode: cursor?.resumeMode ?? "replay-current",
|
|
1239
|
+
executionGeneration: (cursor?.executionGeneration ?? 1) + (cursor?.resumeMode === "continue-next" ? 0 : 1),
|
|
1240
|
+
requestId: request.requestId,
|
|
1241
|
+
revision: (cursor?.revision ?? 0) + 1
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
await this.writeRunRecord(handle.recordPath, updated);
|
|
1245
|
+
return updated;
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1019
1248
|
/**
|
|
1020
1249
|
* Create or resume a workflow run in the `running` stage for its workspace.
|
|
1021
1250
|
*/
|
|
@@ -1023,97 +1252,104 @@ var WorkflowRegistryService = class {
|
|
|
1023
1252
|
const workspace = this.resolveWorkspace(input.workspace, input.workflowWorkspace);
|
|
1024
1253
|
const runKey = this.slugifySegment(input.displayName, FALLBACK_RUN_KEY);
|
|
1025
1254
|
await this.ensureWorkspaceStructure(workspace);
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1255
|
+
if (await this.findRunStage(workspace, runKey) === RUNNING_STAGE) await this.reconcileStaleRunningRecord(workspace, runKey);
|
|
1256
|
+
return this.withRunLock((0, node_path.resolve)(this.getRunDirectory(workspace, RUNNING_STAGE, runKey), RUN_RECORD_FILENAME$1), async () => {
|
|
1257
|
+
const existingStage = await this.findRunStage(workspace, runKey);
|
|
1258
|
+
if (existingStage === RUNNING_STAGE) throw new WorkflowRunConflictError(workspace, runKey);
|
|
1259
|
+
const runningDir = this.getRunDirectory(workspace, RUNNING_STAGE, runKey);
|
|
1260
|
+
if (existingStage) {
|
|
1261
|
+
const existingDir = this.getRunDirectory(workspace, existingStage, runKey);
|
|
1262
|
+
await this.preserveRunEvidence(await this.readRunRecord((0, node_path.resolve)(existingDir, RUN_RECORD_FILENAME$1)));
|
|
1263
|
+
let releaseRecoveryLock;
|
|
1264
|
+
try {
|
|
1265
|
+
if (existingStage === ERROR_STAGE$2) {
|
|
1266
|
+
releaseRecoveryLock = await this.acquireRecoveryClaimLock(existingDir);
|
|
1267
|
+
await this.assertRecoveryPromotion(existingDir, workspace, runKey, input.recoveryClaimId);
|
|
1268
|
+
} else if (input.recoveryClaimId) throw new Error(`Workflow recovery claim does not target a failed run: ${workspace}/${runKey}`);
|
|
1269
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.remove(runningDir, {
|
|
1270
|
+
recursive: true,
|
|
1271
|
+
force: true
|
|
1272
|
+
}, this.runtime.ioContext));
|
|
1273
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.renameFile(existingDir, runningDir, this.runtime.ioContext));
|
|
1274
|
+
} finally {
|
|
1275
|
+
await releaseRecoveryLock?.();
|
|
1276
|
+
}
|
|
1277
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, FIX_FILENAME$2));
|
|
1278
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, PROCEED_FILENAME$1));
|
|
1279
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, STOP_REQUEST_FILENAME));
|
|
1280
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, PAUSE_REQUEST_FILENAME$1));
|
|
1281
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, FINALIZING_FILENAME));
|
|
1282
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, RECOVERY_CLAIM_FILENAME));
|
|
1283
|
+
await this.removeFileIfExists((0, node_path.resolve)(runningDir, RECOVERY_ADOPTION_FILENAME));
|
|
1284
|
+
} else {
|
|
1285
|
+
if (input.recoveryClaimId) throw new Error(`Workflow recovery claim target no longer exists: ${workspace}/${runKey}`);
|
|
1286
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.createDir(runningDir, { recursive: true }, this.runtime.ioContext));
|
|
1287
|
+
}
|
|
1288
|
+
const runId = (0, node_crypto.randomUUID)();
|
|
1289
|
+
const runRecord = {
|
|
1290
|
+
activeRepairId: input.activeRepairId,
|
|
1291
|
+
admission: input.admission,
|
|
1292
|
+
configuration: input.configuration,
|
|
1293
|
+
continueOnError: input.continueOnError,
|
|
1294
|
+
displayName: input.displayName,
|
|
1295
|
+
dryRun: input.dryRun,
|
|
1296
|
+
env: input.env,
|
|
1297
|
+
inputs: input.inputs,
|
|
1298
|
+
job: input.job,
|
|
1299
|
+
keepWorktree: input.keepWorktree,
|
|
1300
|
+
launcher: input.launcher,
|
|
1301
|
+
runKey,
|
|
1302
|
+
prompt: input.prompt,
|
|
1303
|
+
runner: input.runner,
|
|
1304
|
+
...input.command === void 0 ? {} : { command: input.command },
|
|
1305
|
+
...input.choice === void 0 ? {} : { choice: input.choice },
|
|
1306
|
+
...input.profile === void 0 ? {} : { profile: input.profile },
|
|
1307
|
+
runDirectory: input.runDirectory,
|
|
1308
|
+
runId,
|
|
1309
|
+
secretFile: input.secretFile,
|
|
1310
|
+
skipLaunch: input.skipLaunch,
|
|
1311
|
+
statusFile: input.statusFile,
|
|
1312
|
+
terminalSocket: input.terminalSocket,
|
|
1313
|
+
executionState: "running",
|
|
1314
|
+
executionCursor: {
|
|
1315
|
+
executionGeneration: 1,
|
|
1316
|
+
phase: "starting",
|
|
1317
|
+
resumeMode: "continue-next",
|
|
1318
|
+
revision: 0
|
|
1319
|
+
},
|
|
1320
|
+
stage: RUNNING_STAGE,
|
|
1321
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1322
|
+
pid: input.pid ?? this.runtime.workflowHost.processId,
|
|
1323
|
+
workflowPath: input.workflowPath,
|
|
1324
|
+
workflowId: input.workflowId,
|
|
1325
|
+
workflowName: input.workflowName,
|
|
1326
|
+
workspace
|
|
1327
|
+
};
|
|
1328
|
+
const recordPath = (0, node_path.resolve)(runningDir, RUN_RECORD_FILENAME$1);
|
|
1329
|
+
await this.writeRunRecord(recordPath, runRecord);
|
|
1330
|
+
return {
|
|
1331
|
+
changelogPath: (0, node_path.resolve)(runningDir, CHANGELOG_FILENAME),
|
|
1332
|
+
contextPath: (0, node_path.resolve)(runningDir, CONTEXT_FILENAME),
|
|
1333
|
+
displayName: input.displayName,
|
|
1334
|
+
pauseRequestPath: (0, node_path.resolve)(runningDir, PAUSE_REQUEST_FILENAME$1),
|
|
1335
|
+
recordPath,
|
|
1336
|
+
runDir: runningDir,
|
|
1337
|
+
runId,
|
|
1338
|
+
runKey,
|
|
1339
|
+
stopRequestPath: (0, node_path.resolve)(runningDir, STOP_REQUEST_FILENAME),
|
|
1340
|
+
workspace
|
|
1341
|
+
};
|
|
1342
|
+
});
|
|
1108
1343
|
}
|
|
1109
1344
|
/**
|
|
1110
1345
|
* Finalize a workflow run by moving it into a terminal stage and updating metadata.
|
|
1111
1346
|
*/
|
|
1112
1347
|
async finalizeRun(handle, input) {
|
|
1113
|
-
return this.
|
|
1348
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1114
1349
|
await this.ensureWorkspaceStructure(handle.workspace);
|
|
1115
1350
|
const nextDir = this.getRunDirectory(handle.workspace, input.stage, handle.runKey);
|
|
1116
1351
|
const currentRecord = await this.readRunRecord(handle.recordPath);
|
|
1352
|
+
if (currentRecord.runId !== handle.runId) throw new Error("Workflow generation changed before finalization.");
|
|
1117
1353
|
const finalRecord = {
|
|
1118
1354
|
...currentRecord,
|
|
1119
1355
|
errorMessage: input.errorMessage,
|
|
@@ -1246,7 +1482,7 @@ var WorkflowRegistryService = class {
|
|
|
1246
1482
|
resumeMode: "replay-current"
|
|
1247
1483
|
},
|
|
1248
1484
|
requestId: request.requestId,
|
|
1249
|
-
resumeMode: "replay-current",
|
|
1485
|
+
resumeMode: record.executionCursor?.resumeMode ?? "replay-current",
|
|
1250
1486
|
revision: revision + 1
|
|
1251
1487
|
}, revision);
|
|
1252
1488
|
await this.writeControlRequest(handle, handle.pauseRequestPath ?? (0, node_path.resolve)(handle.runDir, PAUSE_REQUEST_FILENAME$1), request);
|
|
@@ -1265,7 +1501,7 @@ var WorkflowRegistryService = class {
|
|
|
1265
1501
|
}
|
|
1266
1502
|
}
|
|
1267
1503
|
async updateExecutionState(handle, executionState, executionCursor, expectedRevision) {
|
|
1268
|
-
return this.
|
|
1504
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1269
1505
|
const current = await this.readRunRecord(handle.recordPath);
|
|
1270
1506
|
if (current.stage !== RUNNING_STAGE || current.runId && current.runId !== handle.runId) throw new Error(`Workflow run is no longer controllable: ${handle.workspace}/${handle.runKey}`);
|
|
1271
1507
|
const currentRevision = current.executionCursor?.revision ?? 0;
|
|
@@ -1280,7 +1516,7 @@ var WorkflowRegistryService = class {
|
|
|
1280
1516
|
});
|
|
1281
1517
|
}
|
|
1282
1518
|
async updateExecutionCursor(handle, patch) {
|
|
1283
|
-
return this.
|
|
1519
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1284
1520
|
const current = await this.readRunRecord(handle.recordPath);
|
|
1285
1521
|
if (current.stage !== RUNNING_STAGE || current.runId && current.runId !== handle.runId) throw new Error(`Workflow run is no longer controllable: ${handle.workspace}/${handle.runKey}`);
|
|
1286
1522
|
const currentCursor = current.executionCursor ?? {
|
|
@@ -1305,7 +1541,7 @@ var WorkflowRegistryService = class {
|
|
|
1305
1541
|
async markResumable(run, reason) {
|
|
1306
1542
|
const runDir = this.getRunDirectory(run.workspace, RUNNING_STAGE, run.runKey);
|
|
1307
1543
|
const recordPath = (0, node_path.resolve)(runDir, RUN_RECORD_FILENAME$1);
|
|
1308
|
-
return this.
|
|
1544
|
+
return this.withRunLock(recordPath, async () => {
|
|
1309
1545
|
const current = await this.readRunRecord(recordPath);
|
|
1310
1546
|
if (current.stage !== RUNNING_STAGE || current.runId !== run.runId) throw new Error(`Workflow run is no longer controllable: ${run.workspace}/${run.runKey}`);
|
|
1311
1547
|
if (current.executionState === "paused" || await this.hasStopRequest({
|
|
@@ -1328,8 +1564,18 @@ var WorkflowRegistryService = class {
|
|
|
1328
1564
|
});
|
|
1329
1565
|
}
|
|
1330
1566
|
/** Save worktree identity as soon as it exists, including for a later interrupted run. */
|
|
1567
|
+
async updateConfiguration(handle, configuration) {
|
|
1568
|
+
await this.withRunLock(handle.recordPath, async () => {
|
|
1569
|
+
const record = await this.readRunRecord(handle.recordPath);
|
|
1570
|
+
if (record.runId !== handle.runId || record.stage !== RUNNING_STAGE) throw new Error("Workflow configuration generation changed.");
|
|
1571
|
+
await this.writeRunRecord(handle.recordPath, {
|
|
1572
|
+
...record,
|
|
1573
|
+
configuration
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1331
1577
|
async updateWorktree(handle, metadata) {
|
|
1332
|
-
return this.
|
|
1578
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1333
1579
|
const current = await this.readRunRecord(handle.recordPath);
|
|
1334
1580
|
if (current.stage !== RUNNING_STAGE || current.runId !== handle.runId) throw new Error(`Workflow run is no longer controllable: ${handle.workspace}/${handle.runKey}`);
|
|
1335
1581
|
await this.writeRunRecord(handle.recordPath, {
|
|
@@ -1408,8 +1654,10 @@ var WorkflowRegistryService = class {
|
|
|
1408
1654
|
const tempPath = `${path}.${this.runtime.workflowHost.processId}.${(0, node_crypto.randomUUID)()}.tmp`;
|
|
1409
1655
|
try {
|
|
1410
1656
|
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, this.runtime.ioContext));
|
|
1411
|
-
await this.
|
|
1412
|
-
|
|
1657
|
+
await this.withRunLock(handle.recordPath, async () => {
|
|
1658
|
+
await this.assertCurrentRun(handle);
|
|
1659
|
+
(0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.renameFile(tempPath, path, this.runtime.ioContext));
|
|
1660
|
+
});
|
|
1413
1661
|
} catch (error) {
|
|
1414
1662
|
await this.removeFileIfExists(tempPath);
|
|
1415
1663
|
throw error;
|
|
@@ -1630,7 +1878,7 @@ var WorkflowRegistryService = class {
|
|
|
1630
1878
|
* Persist the active repair pointer and fix-cycle progress on a running record.
|
|
1631
1879
|
*/
|
|
1632
1880
|
async updateRepairState(handle, input) {
|
|
1633
|
-
return this.
|
|
1881
|
+
return this.withRunLock(handle.recordPath, async () => {
|
|
1634
1882
|
const current = await this.readRunRecord(handle.recordPath);
|
|
1635
1883
|
const updated = {
|
|
1636
1884
|
...current,
|
|
@@ -1671,7 +1919,7 @@ var WorkflowRegistryService = class {
|
|
|
1671
1919
|
};
|
|
1672
1920
|
const runDir = this.getRunDirectory(input.workspace, input.stage, input.runKey);
|
|
1673
1921
|
const errorStage = input.stage === ERROR_STAGE$2;
|
|
1674
|
-
return this.
|
|
1922
|
+
return this.withRunLock((0, node_path.resolve)(runDir, RUN_RECORD_FILENAME$1), async () => {
|
|
1675
1923
|
const releaseLock = errorStage ? await this.acquireRecoveryClaimLock(runDir) : void 0;
|
|
1676
1924
|
try {
|
|
1677
1925
|
if (!await this.pathExists(runDir)) throw new Error(`Workflow run not found: ${input.workspace}/${input.stage}/${input.runKey}`);
|
|
@@ -1881,16 +2129,6 @@ var WorkflowRegistryService = class {
|
|
|
1881
2129
|
return [...records.values()];
|
|
1882
2130
|
}
|
|
1883
2131
|
/**
|
|
1884
|
-
* Resolve the run stage, first converting dead running records into
|
|
1885
|
-
* interrupted terminal records so reruns are unblocked.
|
|
1886
|
-
*/
|
|
1887
|
-
async findReusableRunStage(workspace, runKey) {
|
|
1888
|
-
const existingStage = await this.findRunStage(workspace, runKey);
|
|
1889
|
-
if (existingStage !== RUNNING_STAGE) return existingStage;
|
|
1890
|
-
await this.reconcileStaleRunningRecord(workspace, runKey);
|
|
1891
|
-
return this.findRunStage(workspace, runKey);
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
2132
|
* Read a running record for status displays without mutating it on disk.
|
|
1895
2133
|
* If the recorded PID is gone, mark the returned record as stale but keep
|
|
1896
2134
|
* the persisted state untouched so the original failure metadata is not
|
|
@@ -1949,7 +2187,7 @@ var WorkflowRegistryService = class {
|
|
|
1949
2187
|
async reconcileStaleRunningRecord(workspace, runKey, expected) {
|
|
1950
2188
|
const runningDir = this.getRunDirectory(workspace, RUNNING_STAGE, runKey);
|
|
1951
2189
|
const runningRecordPath = (0, node_path.resolve)(runningDir, RUN_RECORD_FILENAME$1);
|
|
1952
|
-
return this.
|
|
2190
|
+
return this.withRunLock(runningRecordPath, async () => {
|
|
1953
2191
|
const currentRecord = await this.readRunRecord(runningRecordPath);
|
|
1954
2192
|
if (expected !== void 0 && (currentRecord.runId !== expected.runId || currentRecord.env?.PO_SESSION_ID !== expected.ownerSessionId)) {
|
|
1955
2193
|
if (expected.stop) throw new Error(`Workflow run ownership changed: ${workspace}/${runKey}`);
|
|
@@ -2017,6 +2255,7 @@ var WorkflowRegistryService = class {
|
|
|
2017
2255
|
*/
|
|
2018
2256
|
async writeRunRecord(recordPath, record) {
|
|
2019
2257
|
await this.writeAtomic(recordPath, record);
|
|
2258
|
+
if (record.stage !== RUNNING_STAGE) await this.preserveRunEvidence(record, (0, node_path.dirname)(recordPath));
|
|
2020
2259
|
}
|
|
2021
2260
|
/**
|
|
2022
2261
|
* Validate the persisted run record shape before returning it to callers.
|
|
@@ -4451,6 +4690,19 @@ var StepRunnerService = class StepRunnerService {
|
|
|
4451
4690
|
* progress log and telemetry instead of existing only as a log line.
|
|
4452
4691
|
*/
|
|
4453
4692
|
async executeStep(step, context, options, report) {
|
|
4693
|
+
let policyEnvironment = {};
|
|
4694
|
+
const runDir = context.env[WORKFLOW_RUN_DIR_ENV_KEY];
|
|
4695
|
+
if (!options.dryRun && runDir) {
|
|
4696
|
+
const file = (0, node_path.join)(runDir, RUN_RECORD_FILENAME);
|
|
4697
|
+
if (await (0, _agimon_ai_po_core_platform.pathExists)(this.runtime.fs, file, this.runtime.ioContext)) {
|
|
4698
|
+
const record = await new WorkflowRegistryService(this.runtime).readRunRecord(file);
|
|
4699
|
+
if (record.admission?.binding) {
|
|
4700
|
+
if (record.runId !== context.env.WORKFLOW_RUN_ID || !record.admission.ready) throw new Error("Managed workflow execution is not admitted for this generation.");
|
|
4701
|
+
policyEnvironment = record.admission.environment ?? {};
|
|
4702
|
+
Object.assign(context.env, policyEnvironment);
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4454
4706
|
const fail = (reason) => {
|
|
4455
4707
|
this.logger.error(` ${RED$3}${STATUS_ERROR}${RESET$4} ${reason}`);
|
|
4456
4708
|
report.reason = reason;
|
|
@@ -4530,6 +4782,7 @@ var StepRunnerService = class StepRunnerService {
|
|
|
4530
4782
|
...stepContext.env
|
|
4531
4783
|
};
|
|
4532
4784
|
if (step.env) for (const [key, val] of Object.entries(step.env)) stepEnv[key] = this.parser.interpolate(String(val), stepContext);
|
|
4785
|
+
Object.assign(stepEnv, policyEnvironment);
|
|
4533
4786
|
for (const key of [
|
|
4534
4787
|
"WORKFLOW_RUN_ID",
|
|
4535
4788
|
"WORKFLOW_RUN_KEY",
|
|
@@ -4619,8 +4872,9 @@ var StepRunnerService = class StepRunnerService {
|
|
|
4619
4872
|
});
|
|
4620
4873
|
await this.markPaused(options.stopRequestPath, stepName, pausePolicy);
|
|
4621
4874
|
await options.onExecutionStateChange?.("paused", stepName);
|
|
4622
|
-
await this.waitForResume(options.stopRequestPath, stepName);
|
|
4875
|
+
Object.assign(stepContext.env, await this.waitForResume(options.stopRequestPath, stepName, stepContext.env.WORKFLOW_RUN_ID));
|
|
4623
4876
|
await options.onExecutionStateChange?.("resumed", stepName);
|
|
4877
|
+
return this.executeStep(step, stepContext, options, report);
|
|
4624
4878
|
}
|
|
4625
4879
|
let maxTimeoutRetries;
|
|
4626
4880
|
try {
|
|
@@ -4693,12 +4947,11 @@ var StepRunnerService = class StepRunnerService {
|
|
|
4693
4947
|
});
|
|
4694
4948
|
await this.markPaused(options.stopRequestPath, stepName, pausePolicy, executionResult.survivors);
|
|
4695
4949
|
await options.onExecutionStateChange?.("paused", stepName);
|
|
4696
|
-
await this.waitForResume(options.stopRequestPath, stepName);
|
|
4950
|
+
Object.assign(stepContext.env, await this.waitForResume(options.stopRequestPath, stepName, stepContext.env.WORKFLOW_RUN_ID));
|
|
4697
4951
|
await options.onExecutionStateChange?.("resumed", stepName);
|
|
4698
4952
|
if (pausePolicy === "protected") return true;
|
|
4699
4953
|
await this.logStepLifecycle("info", "workflow.step.replay", stepEnv, { "workflow.step.outcome": "replay-current" });
|
|
4700
|
-
|
|
4701
|
-
continue;
|
|
4954
|
+
return this.executeStep(step, stepContext, options, report);
|
|
4702
4955
|
}
|
|
4703
4956
|
if (executionResult.status === "stop_requested") {
|
|
4704
4957
|
this.logger.info(`\n ${YELLOW$2}${STATUS_INTERRUPTED}${RESET$4} ${stepName} (${STATUS_STOP})`);
|
|
@@ -6207,8 +6460,8 @@ var StepRunnerService = class StepRunnerService {
|
|
|
6207
6460
|
throw error;
|
|
6208
6461
|
}
|
|
6209
6462
|
}
|
|
6210
|
-
async waitForResume(stopRequestPath, stepName) {
|
|
6211
|
-
if (!stopRequestPath) return;
|
|
6463
|
+
async waitForResume(stopRequestPath, stepName, expectedRunId) {
|
|
6464
|
+
if (!stopRequestPath) return {};
|
|
6212
6465
|
const pauseRequestPath = (0, node_path.join)((0, node_path.dirname)(stopRequestPath), PAUSE_REQUEST_FILENAME);
|
|
6213
6466
|
const recordPath = (0, node_path.join)((0, node_path.dirname)(stopRequestPath), RUN_RECORD_FILENAME);
|
|
6214
6467
|
this.logger.info(` ${YELLOW$2}${STATUS_WAIT}${RESET$4} ${stepName} paused; waiting for resume`);
|
|
@@ -6222,26 +6475,34 @@ var StepRunnerService = class StepRunnerService {
|
|
|
6222
6475
|
context: this.createInterruptContext(stepName, "")
|
|
6223
6476
|
});
|
|
6224
6477
|
if (await (0, _agimon_ai_po_core_platform.pathExists)(this.runtime.fs, pauseRequestPath, this.runtime.ioContext) && await this.readControlAction(pauseRequestPath) === "resume") {
|
|
6225
|
-
const
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
const
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6478
|
+
const registry = new WorkflowRegistryService(this.runtime);
|
|
6479
|
+
const current = await registry.readRunRecord(recordPath);
|
|
6480
|
+
if (expectedRunId !== void 0 && current.runId !== expectedRunId) throw new Error("Workflow generation changed while paused.");
|
|
6481
|
+
const handle = {
|
|
6482
|
+
runId: current.runId,
|
|
6483
|
+
workspace: current.workspace,
|
|
6484
|
+
runKey: current.runKey,
|
|
6485
|
+
runDir: (0, node_path.dirname)(recordPath),
|
|
6486
|
+
recordPath,
|
|
6487
|
+
displayName: current.displayName,
|
|
6488
|
+
stopRequestPath,
|
|
6489
|
+
changelogPath: (0, node_path.join)((0, node_path.dirname)(recordPath), "changelog.md"),
|
|
6490
|
+
contextPath: (0, node_path.join)((0, node_path.dirname)(recordPath), "context.md")
|
|
6236
6491
|
};
|
|
6237
|
-
await
|
|
6238
|
-
return;
|
|
6492
|
+
return (await registry.authorizeExecution(handle, true)).admission?.environment ?? {};
|
|
6239
6493
|
}
|
|
6240
6494
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, STOP_REQUEST_POLL_INTERVAL_MS));
|
|
6241
6495
|
}
|
|
6242
6496
|
}
|
|
6243
6497
|
async writeControlRecord(recordPath, record) {
|
|
6244
|
-
|
|
6498
|
+
const parsed = WorkflowRunRecordSchema.parse(record);
|
|
6499
|
+
if (!parsed.runId) throw new Error("Workflow pause record has no generation.");
|
|
6500
|
+
await new WorkflowRegistryService(this.runtime).updateExecutionState({
|
|
6501
|
+
recordPath,
|
|
6502
|
+
runId: parsed.runId,
|
|
6503
|
+
workspace: parsed.workspace,
|
|
6504
|
+
runKey: parsed.runKey
|
|
6505
|
+
}, "paused", parsed.executionCursor, (parsed.executionCursor?.revision ?? 1) - 1);
|
|
6245
6506
|
}
|
|
6246
6507
|
captureTerminalState() {
|
|
6247
6508
|
if (!this.ownsTerminal) return null;
|
|
@@ -11567,6 +11828,7 @@ var RunWorkflowService = class {
|
|
|
11567
11828
|
* hooks. `post:` is the exception, and it is not a harmless one.
|
|
11568
11829
|
*/
|
|
11569
11830
|
let delegated = false;
|
|
11831
|
+
let policyAuthorized = input.admission?.binding === void 0;
|
|
11570
11832
|
let progress;
|
|
11571
11833
|
let result = null;
|
|
11572
11834
|
let runError = null;
|
|
@@ -11671,6 +11933,8 @@ var RunWorkflowService = class {
|
|
|
11671
11933
|
if (dryRun && input.recovery) registryRun = await this.createRecoveryDryRunHandle(input, resolvedWorkspace);
|
|
11672
11934
|
else registryRun = await this.registry.createRun({
|
|
11673
11935
|
activeRepairId: input.recovery?.activeRepairId,
|
|
11936
|
+
admission: input.admission,
|
|
11937
|
+
configuration: configuration(),
|
|
11674
11938
|
continueOnError: input.continueOnError,
|
|
11675
11939
|
displayName: workflowName,
|
|
11676
11940
|
dryRun,
|
|
@@ -11696,6 +11960,11 @@ var RunWorkflowService = class {
|
|
|
11696
11960
|
workflowWorkspace: workflow.workspace,
|
|
11697
11961
|
workspace: input.workspace
|
|
11698
11962
|
});
|
|
11963
|
+
if (!dryRun && input.admission?.binding) {
|
|
11964
|
+
const admitted = await this.registry.authorizeExecution(registryRun);
|
|
11965
|
+
Object.assign(workflowEnv, admitted.admission?.environment);
|
|
11966
|
+
policyAuthorized = true;
|
|
11967
|
+
}
|
|
11699
11968
|
if (input.recovery?.activeRepairId && !dryRun) activeRepair = await this.repairService.readRepair(registryRun.runDir, input.recovery.activeRepairId);
|
|
11700
11969
|
workflowEnv.WORKFLOW_RUN_DIR = registryRun.runDir;
|
|
11701
11970
|
workflowEnv.WORKFLOW_RUN_ID = registryRun.runId;
|
|
@@ -11906,6 +12175,7 @@ var RunWorkflowService = class {
|
|
|
11906
12175
|
while (!result) {
|
|
11907
12176
|
workflow = this.parser.selectRunner(await this.parser.parseWorkflowFile(await currentWorkflowPath()), runner);
|
|
11908
12177
|
this.useCommandSelection(workflow, input);
|
|
12178
|
+
if (!dryRun) await this.registry.updateConfiguration(registryRun, configuration());
|
|
11909
12179
|
input.onConfigChanged?.({
|
|
11910
12180
|
runKey: registryRun.runKey,
|
|
11911
12181
|
runId: registryRun.runId,
|
|
@@ -12275,7 +12545,7 @@ var RunWorkflowService = class {
|
|
|
12275
12545
|
}
|
|
12276
12546
|
}
|
|
12277
12547
|
}
|
|
12278
|
-
if (workflow.post && !delegated) {
|
|
12548
|
+
if (workflow.post && !delegated && policyAuthorized) {
|
|
12279
12549
|
this.stepRunner.resetInterruption();
|
|
12280
12550
|
const settled = this.resolveRunOutcome(result, runError);
|
|
12281
12551
|
workflowEnv[WORKFLOW_PHASE_ENV] = "post";
|
|
@@ -12962,12 +13232,29 @@ var RecoverWorkflowService = class {
|
|
|
12962
13232
|
async recoverForSession(input, ownerSessionId) {
|
|
12963
13233
|
return await this.recoverWithOwner(input, ownerSessionId);
|
|
12964
13234
|
}
|
|
13235
|
+
/** Read-only reconstruction reused by admission validation; no claim, process cleanup, or runner call. */
|
|
13236
|
+
async prepareRecovery(input) {
|
|
13237
|
+
const record = await this.registry.readRunByKey(input.workspace, ERROR_STAGE, input.runKey);
|
|
13238
|
+
if (this.hooks.expectedRunId !== void 0 && record.runId !== this.hooks.expectedRunId) throw new Error("Workflow recovery generation changed.");
|
|
13239
|
+
return this.buildReplay(input, void 0, record.admission?.ownerSessionId, record);
|
|
13240
|
+
}
|
|
13241
|
+
async recoverClaimed(input, claimId, owner, record) {
|
|
13242
|
+
const replay = await this.buildReplay(input, claimId, owner, record);
|
|
13243
|
+
return this.runner.run({
|
|
13244
|
+
...replay,
|
|
13245
|
+
onRegistered: (registration) => this.hooks.onRegistered?.(registration, replay),
|
|
13246
|
+
onConfigChanged: (registration) => this.hooks.onConfigChanged?.(registration, replay)
|
|
13247
|
+
});
|
|
13248
|
+
}
|
|
12965
13249
|
async recoverWithOwner(input, ownerSessionId) {
|
|
12966
13250
|
const record = await this.registry.readRunByKey(input.workspace, ERROR_STAGE, input.runKey);
|
|
13251
|
+
if (this.hooks.expectedRunId !== void 0 && record.runId !== this.hooks.expectedRunId) throw new Error("Workflow recovery generation changed.");
|
|
13252
|
+
if (record.admission && ownerSessionId !== record.admission.ownerSessionId) throw new Error("Managed workflow recovery must run in its original owner session.");
|
|
12967
13253
|
if (input.dryRun || record.dryRun) return await this.recoverClaimed(input, void 0, void 0, record);
|
|
12968
13254
|
const claim = input.recoveryClaimId ? await this.registry.adoptRunRecoveryClaim(record.workspace, record.runKey, input.recoveryClaimId) : await this.registry.claimRunRecovery(record.workspace, record.runKey, ownerSessionId);
|
|
12969
13255
|
try {
|
|
12970
13256
|
if (!input.recoveryClaimId) await this.registry.adoptRunRecoveryClaim(record.workspace, record.runKey, claim.claimId);
|
|
13257
|
+
if ((await this.registry.readRunByKey(record.workspace, ERROR_STAGE, record.runKey)).runId !== record.runId) throw new Error("Workflow recovery generation changed after claiming.");
|
|
12971
13258
|
const runDir = this.registry.runDirectoryFor(record);
|
|
12972
13259
|
const paneRefs = (await readWorkflowProgress(this.runtime, runDir)).flatMap((event) => event.ref?.kind === "pane" ? [event.ref.id] : []);
|
|
12973
13260
|
await new NativeTerminalService$1(this.runtime).killRunPanes(record.runId, paneRefs);
|
|
@@ -12978,7 +13265,7 @@ var RecoverWorkflowService = class {
|
|
|
12978
13265
|
throw error;
|
|
12979
13266
|
}
|
|
12980
13267
|
}
|
|
12981
|
-
async
|
|
13268
|
+
async buildReplay(input, recoveryClaimId, claimedOwnerSessionId, failedRecord) {
|
|
12982
13269
|
const record = failedRecord ?? await this.registry.readRunByKey(input.workspace, ERROR_STAGE, input.runKey);
|
|
12983
13270
|
const activeRepair = record.activeRepairId ? await this.registry.readRunRepair(record.workspace, ERROR_STAGE, record.runKey, record.activeRepairId) : void 0;
|
|
12984
13271
|
if (activeRepair && !["pending", "active"].includes(activeRepair.status)) throw new Error(`Workflow run "${record.workspace}/${record.runKey}" points to non-active repair ${activeRepair.repairId} (${activeRepair.status})`);
|
|
@@ -13002,9 +13289,26 @@ var RecoverWorkflowService = class {
|
|
|
13002
13289
|
].includes(value))) throw new Error("Retired workflow execution cannot be replayed. Select a supported runner explicitly.");
|
|
13003
13290
|
const originalJob = fixRestartFrom && !activeRepair ? void 0 : record.job;
|
|
13004
13291
|
if (!this.parser.resolveJobOrder(workflow.jobs, originalJob ?? null).includes(recoveredJob)) throw new Error(`Workflow run "${record.workspace}/${record.runKey}" recovery target "${recoveredJob}" is not in the workflow job order`);
|
|
13005
|
-
const ownerSessionId = claimedOwnerSessionId?.trim();
|
|
13292
|
+
const ownerSessionId = record.admission?.ownerSessionId ?? claimedOwnerSessionId?.trim();
|
|
13293
|
+
const admissionId = (0, node_crypto.randomUUID)();
|
|
13294
|
+
const admission = this.hooks.admission ?? (record.admission ? {
|
|
13295
|
+
...record.admission,
|
|
13296
|
+
attemptId: `recover:${admissionId}`,
|
|
13297
|
+
admissionId,
|
|
13298
|
+
reason: this.hooks.reason ?? "recover",
|
|
13299
|
+
previousRun: this.registry.runRef(record),
|
|
13300
|
+
binding: record.admission.binding ? {
|
|
13301
|
+
...record.admission.binding,
|
|
13302
|
+
epoch: {
|
|
13303
|
+
admissionId,
|
|
13304
|
+
operationId: (0, node_crypto.randomUUID)()
|
|
13305
|
+
}
|
|
13306
|
+
} : void 0
|
|
13307
|
+
} : void 0);
|
|
13308
|
+
if (record.admission && admission && (admission.ownerSessionId !== record.admission.ownerSessionId || admission.launcherSessionId !== record.admission.launcherSessionId || JSON.stringify(admission.binding?.association) !== JSON.stringify(record.admission.binding?.association))) throw new Error("Workflow recovery policy ownership changed.");
|
|
13006
13309
|
const envOverrides = ownerSessionId ? { [PO_SESSION_ENV]: ownerSessionId } : void 0;
|
|
13007
|
-
|
|
13310
|
+
return {
|
|
13311
|
+
admission,
|
|
13008
13312
|
runner,
|
|
13009
13313
|
command: record.command,
|
|
13010
13314
|
choice: record.choice,
|
|
@@ -13035,11 +13339,6 @@ var RecoverWorkflowService = class {
|
|
|
13035
13339
|
worktreePath: record.worktreePath
|
|
13036
13340
|
}
|
|
13037
13341
|
};
|
|
13038
|
-
return await this.runner.run({
|
|
13039
|
-
...replay,
|
|
13040
|
-
onRegistered: (registration) => this.hooks.onRegistered?.(registration, replay),
|
|
13041
|
-
onConfigChanged: (registration) => this.hooks.onConfigChanged?.(registration, replay)
|
|
13042
|
-
});
|
|
13043
13342
|
}
|
|
13044
13343
|
parseTerminalFix(fixContent, allowTerminalStop) {
|
|
13045
13344
|
if (!fixContent.startsWith(FIX_FRONTMATTER_DELIMITER)) return {};
|
|
@@ -13343,4 +13642,4 @@ Object.defineProperty(exports, "workflowShellOutput", {
|
|
|
13343
13642
|
}
|
|
13344
13643
|
});
|
|
13345
13644
|
|
|
13346
|
-
//# sourceMappingURL=RecoverWorkflowService-
|
|
13645
|
+
//# sourceMappingURL=RecoverWorkflowService-C-iiu-ku.cjs.map
|