@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.
- package/dist/controllers/sqlite.d.ts +21 -0
- package/dist/controllers/sqlite.js +82 -1
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.js +137 -4
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +16 -0
- package/dist/extension/session-view.js +119 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.js +2 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +92 -26
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +2 -0
- package/dist/host/state.js +7 -1
- package/dist/host/state.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/WORKFLOW_HOST.md +8 -4
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/workflows.md +12 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/controllers/sqlite.ts +129 -1
- package/src/extension/index.ts +174 -4
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +131 -0
- package/src/extension/widget.ts +3 -2
- package/src/host/runner.ts +103 -29
- package/src/host/state.ts +11 -1
package/src/host/runner.ts
CHANGED
|
@@ -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
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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">):
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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: {
|
|
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,
|
|
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" &&
|