@agimon-ai/po-workflow 0.0.0-alpha.82 → 0.0.0-alpha.84

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.
@@ -637,6 +637,21 @@ const WorkflowAdmissionMetadataSchema = zod.z.object({
637
637
  admissionId: zod.z.string().uuid(),
638
638
  operationId: zod.z.string().uuid()
639
639
  })
640
+ }).optional(),
641
+ /**
642
+ * What the bound coordinator is owed for this terminal generation. Undelivered settlements are
643
+ * retried until the coordinator acknowledges them, so a held lock is never orphaned.
644
+ */
645
+ settlement: zod.z.object({
646
+ outcome: zod.z.enum([
647
+ "success",
648
+ "skipped",
649
+ "failed",
650
+ "interrupted",
651
+ "abandoned"
652
+ ]),
653
+ delivered: zod.z.boolean(),
654
+ at: zod.z.string()
640
655
  }).optional()
641
656
  });
642
657
  const WorkflowAdmissionIntentSchema = zod.z.object({
@@ -992,6 +1007,93 @@ function summarizeWorkflowProgress(events) {
992
1007
  return [...jobs.values()];
993
1008
  }
994
1009
  //#endregion
1010
+ //#region src/services/workflowSettlement/index.ts
1011
+ /** What a terminal bound generation is owed and not yet acknowledged, or undefined when nothing is. */
1012
+ function owedSettlement(record) {
1013
+ const admission = record.admission;
1014
+ if (!admission?.binding || record.stage === "running" || admission.settlement?.delivered === true) return void 0;
1015
+ return admission.settlement?.outcome ?? record.outcome ?? (record.stage === "completed" ? "success" : "interrupted");
1016
+ }
1017
+ /**
1018
+ * Durable settlement delivery. A held lock is released only through its coordinator, so every owed
1019
+ * settlement is retried (on coordinator registration and whenever a run settles) until acknowledged.
1020
+ */
1021
+ function createWorkflowSettlement(options) {
1022
+ let sweeping;
1023
+ let again = false;
1024
+ const deliverOne = async (registry, record) => {
1025
+ const outcome = owedSettlement(record);
1026
+ const admission = record.admission;
1027
+ const run = registry.runRef(record);
1028
+ if (!outcome || !admission?.binding || !run || admission.launcherSessionId !== options.sessionId) return;
1029
+ if (await options.settle({
1030
+ binding: structuredClone(admission.binding),
1031
+ run,
1032
+ outcome,
1033
+ ...record.failedJob === void 0 ? {} : { failedJob: record.failedJob },
1034
+ ownerSessionId: admission.ownerSessionId,
1035
+ launcherSessionId: admission.launcherSessionId,
1036
+ signal: options.signal
1037
+ }) !== "unavailable") await registry.recordSettlement(record, {
1038
+ outcome,
1039
+ delivered: true
1040
+ }, outcome);
1041
+ };
1042
+ const sweep = async () => {
1043
+ for (const registry of await options.registries()) for (const record of await registry.listRuns()) {
1044
+ if (options.signal.aborted) return;
1045
+ try {
1046
+ await deliverOne(registry, record);
1047
+ } catch (error) {
1048
+ options.report(error);
1049
+ }
1050
+ }
1051
+ };
1052
+ /** Coalesces concurrent requests into one sweep, plus one more if a request arrived mid-sweep. */
1053
+ const deliver = () => {
1054
+ if (sweeping) {
1055
+ again = true;
1056
+ return sweeping;
1057
+ }
1058
+ sweeping = (async () => {
1059
+ do {
1060
+ again = false;
1061
+ await sweep();
1062
+ } while (again && !options.signal.aborted);
1063
+ })().finally(() => {
1064
+ sweeping = void 0;
1065
+ });
1066
+ return sweeping;
1067
+ };
1068
+ /**
1069
+ * A person gives up a failed bound run: its coordinator releases what it holds and the run is never
1070
+ * recovered. Takes the recovery claim so it cannot race a recovery of the same run.
1071
+ */
1072
+ const abandon = async (registry, run) => {
1073
+ const record = await registry.readRunByKey(run.registryWorkspace, "error", run.runKey);
1074
+ if (record.runId !== run.runId) throw new Error("Workflow run generation changed.");
1075
+ if (!record.admission?.binding) throw new Error("This run holds no lock, so there is nothing to abandon.");
1076
+ if (record.admission.settlement?.outcome === "abandoned") {
1077
+ deliver();
1078
+ return;
1079
+ }
1080
+ const claim = await registry.claimRunRecovery(record.workspace, record.runKey, options.sessionId);
1081
+ try {
1082
+ if (!await registry.recordSettlement(record, {
1083
+ outcome: "abandoned",
1084
+ delivered: false
1085
+ })) throw new Error("Workflow run changed before it could be abandoned.");
1086
+ } finally {
1087
+ await registry.releaseRunRecoveryClaim(record.workspace, record.runKey, claim.claimId);
1088
+ }
1089
+ await deliver();
1090
+ };
1091
+ return {
1092
+ deliver,
1093
+ abandon
1094
+ };
1095
+ }
1096
+ //#endregion
995
1097
  //#region src/services/WorkflowRegistryService/index.ts
996
1098
  /**
997
1099
  * WorkflowRegistryService
@@ -1108,6 +1210,33 @@ var WorkflowRegistryService = class {
1108
1210
  registryWorkspace: record.workspace
1109
1211
  } : void 0;
1110
1212
  }
1213
+ /**
1214
+ * Record what a terminal bound generation owes its coordinator, or that the coordinator acknowledged it.
1215
+ * Fenced by run and operation, so a replaced generation is never marked. With `expected`, it writes only
1216
+ * while that outcome is still the one owed, so an abandon that lands mid-delivery is never marked delivered.
1217
+ */
1218
+ async recordSettlement(record, settlement, expected) {
1219
+ if (record.stage === RUNNING_STAGE || !record.admission?.binding) return false;
1220
+ const recordPath = (0, node_path.resolve)(this.runDirectoryFor(record), RUN_RECORD_FILENAME$1);
1221
+ if (!await this.pathExists(recordPath)) return false;
1222
+ return this.withRunLock(recordPath, async () => {
1223
+ const current = await this.readRunRecord(recordPath);
1224
+ const admission = current.admission;
1225
+ if (current.runId !== record.runId || current.stage === RUNNING_STAGE || admission?.binding?.epoch.operationId !== record.admission?.binding?.epoch.operationId) return false;
1226
+ if (expected !== void 0 && owedSettlement(current) !== expected) return false;
1227
+ await this.writeRunRecord(recordPath, {
1228
+ ...current,
1229
+ admission: {
1230
+ ...admission,
1231
+ settlement: {
1232
+ ...settlement,
1233
+ at: (/* @__PURE__ */ new Date()).toISOString()
1234
+ }
1235
+ }
1236
+ });
1237
+ return true;
1238
+ });
1239
+ }
1111
1240
  /** Exact-ID evidence survives replay, deletion and a lost secondary admission index write. */
1112
1241
  async readRunEvidence(runId) {
1113
1242
  const current = (await this.listRuns()).find((record) => record.runId === runId);
@@ -13254,7 +13383,9 @@ var RecoverWorkflowService = class {
13254
13383
  const claim = input.recoveryClaimId ? await this.registry.adoptRunRecoveryClaim(record.workspace, record.runKey, input.recoveryClaimId) : await this.registry.claimRunRecovery(record.workspace, record.runKey, ownerSessionId);
13255
13384
  try {
13256
13385
  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.");
13386
+ const current = await this.registry.readRunByKey(record.workspace, ERROR_STAGE, record.runKey);
13387
+ if (current.runId !== record.runId) throw new Error("Workflow recovery generation changed after claiming.");
13388
+ if (current.admission?.settlement?.outcome === "abandoned") throw new Error(`Workflow run "${record.workspace}/${record.runKey}" was abandoned and cannot be recovered`);
13258
13389
  const runDir = this.registry.runDirectoryFor(record);
13259
13390
  const paneRefs = (await readWorkflowProgress(this.runtime, runDir)).flatMap((event) => event.ref?.kind === "pane" ? [event.ref.id] : []);
13260
13391
  await new NativeTerminalService$1(this.runtime).killRunPanes(record.runId, paneRefs);
@@ -13276,6 +13407,7 @@ var RecoverWorkflowService = class {
13276
13407
  const terminalFixContent = activeRepair && !terminalStopAuthorized ? null : await this.registry.readRunFixContent(record.workspace, ERROR_STAGE, record.runKey);
13277
13408
  const terminalFix = terminalFixContent ? this.parseTerminalFix(terminalFixContent, terminalStopAuthorized) : {};
13278
13409
  if (terminalFix.terminal) throw new Error(`Workflow run "${record.workspace}/${record.runKey}" has a terminal fix request and cannot be recovered`);
13410
+ if (record.admission?.settlement?.outcome === "abandoned") throw new Error(`Workflow run "${record.workspace}/${record.runKey}" was abandoned and cannot be recovered`);
13279
13411
  const fixRestartFrom = terminalFix.restartFrom;
13280
13412
  const recoveredJob = activeRepair ? this.repairService.resolveRecoveryStart(activeRepair) : fixRestartFrom ?? record.failedJob;
13281
13413
  if (!recoveredJob) throw new Error(`Workflow run "${record.workspace}/${record.runKey}" does not include a recoverable failed job`);
@@ -13291,8 +13423,11 @@ var RecoverWorkflowService = class {
13291
13423
  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`);
13292
13424
  const ownerSessionId = record.admission?.ownerSessionId ?? claimedOwnerSessionId?.trim();
13293
13425
  const admissionId = (0, node_crypto.randomUUID)();
13426
+ const { settlement: _settlement, ...inherited } = record.admission ?? {};
13294
13427
  const admission = this.hooks.admission ?? (record.admission ? {
13295
- ...record.admission,
13428
+ ...inherited,
13429
+ ownerSessionId: record.admission.ownerSessionId,
13430
+ launcherSessionId: record.admission.launcherSessionId,
13296
13431
  attemptId: `recover:${admissionId}`,
13297
13432
  admissionId,
13298
13433
  reason: this.hooks.reason ?? "recover",
@@ -13509,6 +13644,12 @@ Object.defineProperty(exports, "createWorkflowRuntime", {
13509
13644
  return createWorkflowRuntime;
13510
13645
  }
13511
13646
  });
13647
+ Object.defineProperty(exports, "createWorkflowSettlement", {
13648
+ enumerable: true,
13649
+ get: function() {
13650
+ return createWorkflowSettlement;
13651
+ }
13652
+ });
13512
13653
  Object.defineProperty(exports, "diagnoseWorkflow", {
13513
13654
  enumerable: true,
13514
13655
  get: function() {
@@ -13642,4 +13783,4 @@ Object.defineProperty(exports, "workflowShellOutput", {
13642
13783
  }
13643
13784
  });
13644
13785
 
13645
- //# sourceMappingURL=RecoverWorkflowService-mue4GOSA.cjs.map
13786
+ //# sourceMappingURL=RecoverWorkflowService-3K1OgTtI.cjs.map