@osolmaz/pi-workflows 0.15.2 → 0.15.3

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.
@@ -526,14 +526,37 @@ export class WorkflowHost {
526
526
  case "run.pause": {
527
527
  const runId = requireRunId(request);
528
528
  const active = this.activeRuns.get(runId);
529
- if (active === undefined) return { outcome: "rejected", error: "Run is not active" };
530
- this.commitActivePause(active);
531
- active.control = "pause";
532
- afterCommit.push(() => void active.supervisor.stop("cancelled"));
533
- return { outcome: "accepted", receipt: { runId, status: "parked", paused: true } };
529
+ if (active !== undefined) {
530
+ if (active.control === "pause") {
531
+ return { outcome: "adopted", receipt: { runId, status: "parked", paused: true } };
532
+ }
533
+ if (active.control === "handoff") {
534
+ const paused = this.queue.pauseParkedWorkflowRun({ runId });
535
+ return paused
536
+ ? { outcome: "accepted", receipt: { runId, status: "parked", paused: true } }
537
+ : { outcome: "rejected", error: "Run handoff is not pausable" };
538
+ }
539
+ if (active.control !== undefined) {
540
+ return { outcome: "rejected", error: `Run is already handling ${active.control}` };
541
+ }
542
+ this.commitActivePause(active);
543
+ active.control = "pause";
544
+ afterCommit.push(() => void active.supervisor.stop("cancelled"));
545
+ return { outcome: "accepted", receipt: { runId, status: "parked", paused: true } };
546
+ }
547
+ const paused = this.queue.pauseParkedWorkflowRun({ runId });
548
+ return paused
549
+ ? { outcome: "accepted", receipt: { runId, status: "parked", paused: true } }
550
+ : { outcome: "rejected", error: "Run is not pausable" };
534
551
  }
535
552
  case "run.resume": {
536
553
  const runId = requireRunId(request);
554
+ if (this.queue.resumePausedInteraction({ runId })) {
555
+ return {
556
+ outcome: "accepted",
557
+ receipt: { runId, status: "parked", paused: false, waitingForInteraction: true },
558
+ };
559
+ }
537
560
  if (this.activeRuns.has(runId) || this.pendingStarts.has(runId)) {
538
561
  return { outcome: "adopted", receipt: { runId, active: true } };
539
562
  }
@@ -570,20 +593,27 @@ export class WorkflowHost {
570
593
  case "interaction.update": {
571
594
  const payload = requireRecord(request.payload, "interaction update payload");
572
595
  if (payload.claimPresentation === true) {
573
- const interaction = this.hostState.claimInteractionPresentation({
574
- requestId: requireString(payload.requestId, "requestId"),
575
- expectedRevision: requireNonNegativeInteger(
576
- request.expectedRevision,
577
- "expectedRevision",
578
- ),
579
- presenterId: request.clientId,
580
- leaseMs: PRESENTATION_CLAIM_LEASE_MS,
581
- });
582
- return {
583
- outcome: "accepted",
584
- revision: interaction.revision,
585
- receipt: interaction as unknown as JsonValue,
586
- };
596
+ try {
597
+ const interaction = this.hostState.claimInteractionPresentation({
598
+ requestId: requireString(payload.requestId, "requestId"),
599
+ expectedRevision: requireNonNegativeInteger(
600
+ request.expectedRevision,
601
+ "expectedRevision",
602
+ ),
603
+ presenterId: request.clientId,
604
+ leaseMs: PRESENTATION_CLAIM_LEASE_MS,
605
+ });
606
+ return {
607
+ outcome: "accepted",
608
+ revision: interaction.revision,
609
+ receipt: interaction as unknown as JsonValue,
610
+ };
611
+ } catch (error) {
612
+ if (errorMessage(error) === "Interactive request presentation claim conflict") {
613
+ return { outcome: "conflict", error: errorMessage(error) };
614
+ }
615
+ throw error;
616
+ }
587
617
  }
588
618
  if (typeof payload.sessionEntryId === "string") {
589
619
  const interaction = this.hostState.markInteractionPresented({
@@ -722,17 +752,18 @@ export class WorkflowHost {
722
752
  };
723
753
  }
724
754
 
725
- private rememberDeliveryClaim(options: Omit<DeliveryClaim, "expiresAt">): string {
755
+ private rememberDeliveryClaim(options: Omit<DeliveryClaim, "expiresAt">): {
756
+ claimId: string;
757
+ claimExpiresAt: string;
758
+ } {
726
759
  const now = Date.now();
727
760
  for (const [claimId, claim] of this.deliveryClaims) {
728
761
  if (claim.expiresAt <= now) this.deliveryClaims.delete(claimId);
729
762
  }
730
763
  const claimId = randomUUID();
731
- this.deliveryClaims.set(claimId, {
732
- ...options,
733
- expiresAt: now + DELIVERY_CLAIM_LEASE_MS,
734
- });
735
- return claimId;
764
+ const expiresAt = now + DELIVERY_CLAIM_LEASE_MS;
765
+ this.deliveryClaims.set(claimId, { ...options, expiresAt });
766
+ return { claimId, claimExpiresAt: new Date(expiresAt).toISOString() };
736
767
  }
737
768
 
738
769
  private deliveryClaim(
@@ -757,8 +788,39 @@ export class WorkflowHost {
757
788
  return claim;
758
789
  }
759
790
 
791
+ private validateDelivery(
792
+ command: HostRequest,
793
+ payload: Record<string, unknown>,
794
+ kind: DeliveryClaim["kind"],
795
+ ): Omit<HostResponse, "schema" | "requestId"> {
796
+ const resourceId = requireString(payload.resourceId, "resourceId");
797
+ const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
798
+ const claimId = requireString(payload.claimId, "claimId");
799
+ const claim = this.deliveryClaim(claimId, command.clientId, kind, resourceId, targetSessionId);
800
+ if (claim === undefined) {
801
+ return { outcome: "accepted", receipt: { claimId, live: false } };
802
+ }
803
+ const live =
804
+ kind === "notification"
805
+ ? this.queue.isWorkflowNotificationClaimLive({
806
+ notificationId: resourceId,
807
+ targetSessionId,
808
+ claimToken: claim.token,
809
+ })
810
+ : this.queue.isWorkflowTurnIntentClaimLive({
811
+ intentId: resourceId,
812
+ targetSessionId,
813
+ claimToken: claim.token,
814
+ });
815
+ if (!live) this.deliveryClaims.delete(claimId);
816
+ return { outcome: "accepted", receipt: { claimId, live } };
817
+ }
818
+
760
819
  private claimNotification(command: HostRequest): Omit<HostResponse, "schema" | "requestId"> {
761
820
  const payload = requireRecord(command.payload, "notification claim payload");
821
+ if (payload.validateClaim === true) {
822
+ return this.validateDelivery(command, payload, "notification");
823
+ }
762
824
  const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
763
825
  const token = randomUUID();
764
826
  const notification = this.queue.claimPendingWorkflowNotifications({
@@ -770,7 +832,7 @@ export class WorkflowHost {
770
832
  if (notification === undefined) {
771
833
  return { outcome: "accepted", receipt: { notification: null } };
772
834
  }
773
- const claimId = this.rememberDeliveryClaim({
835
+ const claim = this.rememberDeliveryClaim({
774
836
  clientId: command.clientId,
775
837
  token,
776
838
  targetSessionId,
@@ -779,7 +841,7 @@ export class WorkflowHost {
779
841
  });
780
842
  return {
781
843
  outcome: "accepted",
782
- receipt: { claimId, notification } as unknown as JsonValue,
844
+ receipt: { ...claim, notification } as unknown as JsonValue,
783
845
  };
784
846
  }
785
847
 
@@ -811,6 +873,9 @@ export class WorkflowHost {
811
873
 
812
874
  private claimTurn(command: HostRequest): Omit<HostResponse, "schema" | "requestId"> {
813
875
  const payload = requireRecord(command.payload, "turn claim payload");
876
+ if (payload.validateClaim === true) {
877
+ return this.validateDelivery(command, payload, "turn");
878
+ }
814
879
  const targetSessionId = requireString(payload.targetSessionId, "targetSessionId");
815
880
  const token = randomUUID();
816
881
  const intent = this.queue.claimEligibleWorkflowTurnIntents({
@@ -822,7 +887,7 @@ export class WorkflowHost {
822
887
  if (intent === undefined) {
823
888
  return { outcome: "accepted", receipt: { turn: null } };
824
889
  }
825
- const claimId = this.rememberDeliveryClaim({
890
+ const claim = this.rememberDeliveryClaim({
826
891
  clientId: command.clientId,
827
892
  token,
828
893
  targetSessionId,
@@ -831,7 +896,7 @@ export class WorkflowHost {
831
896
  });
832
897
  return {
833
898
  outcome: "accepted",
834
- receipt: { claimId, turn: intent } as unknown as JsonValue,
899
+ receipt: { ...claim, turn: intent } as unknown as JsonValue,
835
900
  };
836
901
  }
837
902
 
@@ -913,6 +978,9 @@ export class WorkflowHost {
913
978
  if (interaction === undefined || interaction.kind !== "decision") {
914
979
  return { outcome: "notFound", error: `Decision request not found: ${requestId}` };
915
980
  }
981
+ if (this.queue.isWorkflowRunPaused(interaction.runId)) {
982
+ return { outcome: "conflict", error: "Workflow run is paused" };
983
+ }
916
984
  const request = interaction.contract as unknown as HumanDecisionRequest;
917
985
  const response = payload.response as HumanDecisionResponse;
918
986
  const accepted = this.decisions.acceptSync(request, {
@@ -1085,6 +1153,9 @@ export class WorkflowHost {
1085
1153
  if (interaction === undefined || interaction.runId !== runId) {
1086
1154
  return { outcome: "notFound", error: `Interactive request not found: ${requestId}` };
1087
1155
  }
1156
+ if (this.queue.isWorkflowRunPaused(runId)) {
1157
+ return { outcome: "conflict", error: "Workflow run is paused" };
1158
+ }
1088
1159
  const attemptId = requireString(payload.attempt, "attempt");
1089
1160
  const nodeId = requireString(payload.step, "step");
1090
1161
  const storedContract = requireRecord(interaction.contract, "interactive contract");
@@ -1148,6 +1219,9 @@ export class WorkflowHost {
1148
1219
  if (current === undefined || current.runId !== requireRunId(request)) {
1149
1220
  return { outcome: "notFound", error: `Interactive request not found: ${requestId}` };
1150
1221
  }
1222
+ if (this.queue.isWorkflowRunPaused(current.runId)) {
1223
+ return { outcome: "conflict", error: "Workflow run is paused" };
1224
+ }
1151
1225
  const attemptId = requireString(payload.attempt, "attempt");
1152
1226
  const nodeId = requireString(payload.step, "step");
1153
1227
  const storedContract = requireRecord(current.contract, "interactive contract");
package/src/host/state.ts CHANGED
@@ -65,6 +65,8 @@ export type InteractiveRequestRecord = {
65
65
  contract: JsonValue;
66
66
  revision: number;
67
67
  status: "pending" | "presenting" | "settled" | "cancelled";
68
+ presenterId: string | null;
69
+ presentationClaimExpiresAt: string | null;
68
70
  presentationSessionEntryId: string | null;
69
71
  acceptedSubmissionId: string | null;
70
72
  createdAt: string;
@@ -886,7 +888,9 @@ export class HostStateStore {
886
888
  .prepare(
887
889
  `SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
888
890
  target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
889
- revision, status, presentation_session_entry_id AS presentationSessionEntryId,
891
+ revision, status, presenter_id AS presenterId,
892
+ presentation_claim_expires_at AS presentationClaimExpiresAt,
893
+ presentation_session_entry_id AS presentationSessionEntryId,
890
894
  accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
891
895
  updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
892
896
  FROM interactive_requests WHERE request_id = ?`,
@@ -902,6 +906,8 @@ export class HostStateStore {
902
906
  contract: this.state.readJson(row.contractHash),
903
907
  revision: row.revision,
904
908
  status: row.status,
909
+ presenterId: row.presenterId,
910
+ presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
905
911
  presentationSessionEntryId: row.presentationSessionEntryId,
906
912
  acceptedSubmissionId: row.acceptedSubmissionId,
907
913
  createdAt: new Date(row.createdAt).toISOString(),
@@ -997,6 +1003,8 @@ type InteractiveRequestRow = {
997
1003
  contractHash: Buffer;
998
1004
  revision: number;
999
1005
  status: InteractiveRequestRecord["status"];
1006
+ presenterId: string | null;
1007
+ presentationClaimExpiresAt: number | null;
1000
1008
  presentationSessionEntryId: string | null;
1001
1009
  acceptedSubmissionId: string | null;
1002
1010
  createdAt: number;
@@ -1115,6 +1123,8 @@ function isInteractiveRequestRow(value: unknown): value is InteractiveRequestRow
1115
1123
  Buffer.isBuffer(value.contractHash) &&
1116
1124
  typeof value.revision === "number" &&
1117
1125
  ["pending", "presenting", "settled", "cancelled"].includes(value.status as string) &&
1126
+ nullableString(value.presenterId) &&
1127
+ nullableNumber(value.presentationClaimExpiresAt) &&
1118
1128
  nullableString(value.presentationSessionEntryId) &&
1119
1129
  nullableString(value.acceptedSubmissionId) &&
1120
1130
  typeof value.createdAt === "number" &&