@osolmaz/pi-workflows 0.15.2 → 0.16.0

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.
Files changed (124) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
package/src/host/state.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
+ import {
3
+ CLIENT_PROTOCOL_SCHEMA,
4
+ clientRequestFingerprint,
5
+ type ClientRequest,
6
+ type ClientResponse,
7
+ } from "../client/protocol.js";
2
8
  import { StateDatabase } from "../state/database.js";
3
9
  import { canonicalJson, type JsonValue } from "../state/json.js";
4
10
  import { tokenHash } from "../state/mutation.js";
5
- import type { HostRequest, HostResponse } from "./protocol.js";
6
- import { requestFingerprint } from "./protocol.js";
7
11
  import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
8
12
 
9
13
  export type HostClaim = {
@@ -65,6 +69,8 @@ export type InteractiveRequestRecord = {
65
69
  contract: JsonValue;
66
70
  revision: number;
67
71
  status: "pending" | "presenting" | "settled" | "cancelled";
72
+ presenterId: string | null;
73
+ presentationClaimExpiresAt: string | null;
68
74
  presentationSessionEntryId: string | null;
69
75
  acceptedSubmissionId: string | null;
70
76
  createdAt: string;
@@ -199,53 +205,65 @@ export class HostStateStore {
199
205
  };
200
206
  }
201
207
 
202
- readCommand(request: HostRequest): HostResponse | undefined {
208
+ readCommand(request: ClientRequest): ClientResponse | undefined {
203
209
  const row = this.state.connection
204
210
  .prepare(
205
- `SELECT request_fingerprint AS requestFingerprint, outcome, accepted_revision AS revision,
211
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome, accepted_revision AS revision,
206
212
  receipt_hash AS receiptHash, error_hash AS errorHash
207
213
  FROM host_commands WHERE request_id = ?`,
208
214
  )
209
215
  .get(request.requestId);
210
216
  if (!isCommandRow(row)) return undefined;
211
- if (!row.requestFingerprint.equals(requestFingerprint(request))) {
217
+ if (!row.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
212
218
  return conflictResponse(request.requestId, "Request ID was reused with another payload");
213
219
  }
214
220
  return this.commandResponse(request.requestId, row);
215
221
  }
216
222
 
217
- executeCommand(
218
- request: HostRequest,
219
- hostEpoch: number,
220
- operation: () => Omit<HostResponse, "schema" | "requestId">,
221
- ): HostResponse {
223
+ adoptCommand(request: ClientRequest): ClientResponse | undefined {
222
224
  const existing = this.readCommand(request);
223
225
  if (existing !== undefined) {
224
- return existing.outcome === "conflict" ? existing : { ...existing, outcome: "adopted" };
226
+ return existing.outcome === "accepted" || existing.outcome === "adopted"
227
+ ? { ...existing, outcome: "adopted" }
228
+ : existing;
229
+ }
230
+ const idempotent = this.state.connection
231
+ .prepare(
232
+ `SELECT request_id AS requestId, request_fingerprint AS clientRequestFingerprint,
233
+ outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
234
+ error_hash AS errorHash
235
+ FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
236
+ )
237
+ .get(request.clientId, request.idempotencyKey);
238
+ if (!isIdempotentCommandRow(idempotent)) return undefined;
239
+ if (!idempotent.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
240
+ return conflictResponse(request.requestId, "Idempotency key was reused with another payload");
225
241
  }
242
+ const adopted = this.commandResponse(idempotent.requestId, idempotent);
243
+ return {
244
+ ...adopted,
245
+ requestId: request.requestId,
246
+ ...(adopted.outcome === "accepted" || adopted.outcome === "adopted"
247
+ ? { outcome: "adopted" as const }
248
+ : {}),
249
+ };
250
+ }
251
+
252
+ executeCommand(
253
+ request: ClientRequest,
254
+ hostEpoch: number,
255
+ operation: () => Omit<ClientResponse, "schema" | "type" | "requestId">,
256
+ ): ClientResponse {
257
+ const existing = this.adoptCommand(request);
258
+ if (existing !== undefined) return existing;
226
259
  return this.state.transaction(() => {
227
- const idempotent = this.state.connection
228
- .prepare(
229
- `SELECT request_id AS requestId, request_fingerprint AS requestFingerprint,
230
- outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
231
- error_hash AS errorHash
232
- FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
233
- )
234
- .get(request.clientId, request.idempotencyKey);
235
- if (isIdempotentCommandRow(idempotent)) {
236
- if (!idempotent.requestFingerprint.equals(requestFingerprint(request))) {
237
- return conflictResponse(
238
- request.requestId,
239
- "Idempotency key was reused with another payload",
240
- );
241
- }
242
- const adopted = this.commandResponse(idempotent.requestId, idempotent);
243
- return { ...adopted, requestId: request.requestId, outcome: "adopted" };
244
- }
260
+ const idempotent = this.adoptCommand(request);
261
+ if (idempotent !== undefined) return idempotent;
245
262
 
246
263
  const result = operation();
247
- const response: HostResponse = {
248
- schema: "pi-workflows.host-response.v1",
264
+ const response: ClientResponse = {
265
+ schema: CLIENT_PROTOCOL_SCHEMA,
266
+ type: "response",
249
267
  requestId: request.requestId,
250
268
  ...result,
251
269
  };
@@ -267,7 +285,7 @@ export class HostStateStore {
267
285
  request.clientId,
268
286
  request.operation,
269
287
  request.idempotencyKey,
270
- requestFingerprint(request),
288
+ clientRequestFingerprint(request),
271
289
  request.runId ?? null,
272
290
  response.revision ?? null,
273
291
  response.outcome,
@@ -350,13 +368,13 @@ export class HostStateStore {
350
368
  readWorkerMessage(message: WorkerMessage): WorkerResponse | undefined {
351
369
  const row = this.state.connection
352
370
  .prepare(
353
- `SELECT request_fingerprint AS requestFingerprint, outcome,
371
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome,
354
372
  accepted_revision AS revision, result_hash AS resultHash, error_hash AS errorHash
355
373
  FROM worker_messages WHERE worker_epoch = ? AND message_id = ?`,
356
374
  )
357
375
  .get(message.workerEpoch, message.messageId);
358
376
  if (!isWorkerMessageRow(row)) return undefined;
359
- if (!row.requestFingerprint.equals(workerMessageFingerprint(message))) {
377
+ if (!row.clientRequestFingerprint.equals(workerMessageFingerprint(message))) {
360
378
  return {
361
379
  schema: "pi-workflows.worker-response.v1",
362
380
  messageId: message.messageId,
@@ -726,6 +744,7 @@ export class HostStateStore {
726
744
  receipt?: JsonValue;
727
745
  }): {
728
746
  interaction: InteractiveRequestRecord;
747
+ submissionId: string;
729
748
  outcome: "accepted" | "adopted";
730
749
  receipt: JsonValue;
731
750
  } {
@@ -745,6 +764,7 @@ export class HostStateStore {
745
764
  receipt?: JsonValue;
746
765
  }): {
747
766
  interaction: InteractiveRequestRecord;
767
+ submissionId: string;
748
768
  outcome: "accepted" | "adopted";
749
769
  receipt: JsonValue;
750
770
  } {
@@ -766,6 +786,7 @@ export class HostStateStore {
766
786
  receipt?: JsonValue;
767
787
  }): {
768
788
  interaction: InteractiveRequestRecord;
789
+ submissionId: string;
769
790
  outcome: "accepted" | "adopted";
770
791
  receipt: JsonValue;
771
792
  } {
@@ -785,6 +806,7 @@ export class HostStateStore {
785
806
  }
786
807
  return {
787
808
  interaction: this.requireInteractiveRequest(options.requestId),
809
+ submissionId: existing.submissionId,
788
810
  outcome: "adopted",
789
811
  receipt:
790
812
  existing.receiptHash === null
@@ -843,6 +865,7 @@ export class HostStateStore {
843
865
  }
844
866
  return {
845
867
  interaction: this.requireInteractiveRequest(options.requestId),
868
+ submissionId: options.submissionId,
846
869
  outcome: "accepted",
847
870
  receipt: options.receipt ?? {
848
871
  requestId: options.requestId,
@@ -865,14 +888,15 @@ export class HostStateStore {
865
888
  return row;
866
889
  }
867
890
 
868
- private commandResponse(requestId: string, row: CommandRow): HostResponse {
891
+ private commandResponse(requestId: string, row: CommandRow): ClientResponse {
869
892
  const receipt = row.receiptHash === null ? undefined : this.state.readJson(row.receiptHash);
870
893
  const error =
871
894
  row.errorHash === null
872
895
  ? undefined
873
896
  : this.state.readBlob(row.errorHash)?.content.toString("utf8");
874
897
  return {
875
- schema: "pi-workflows.host-response.v1",
898
+ schema: CLIENT_PROTOCOL_SCHEMA,
899
+ type: "response",
876
900
  requestId,
877
901
  outcome: row.outcome,
878
902
  ...(row.revision === null ? {} : { revision: row.revision }),
@@ -886,7 +910,9 @@ export class HostStateStore {
886
910
  .prepare(
887
911
  `SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
888
912
  target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
889
- revision, status, presentation_session_entry_id AS presentationSessionEntryId,
913
+ revision, status, presenter_id AS presenterId,
914
+ presentation_claim_expires_at AS presentationClaimExpiresAt,
915
+ presentation_session_entry_id AS presentationSessionEntryId,
890
916
  accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
891
917
  updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
892
918
  FROM interactive_requests WHERE request_id = ?`,
@@ -902,6 +928,8 @@ export class HostStateStore {
902
928
  contract: this.state.readJson(row.contractHash),
903
929
  revision: row.revision,
904
930
  status: row.status,
931
+ presenterId: row.presenterId,
932
+ presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
905
933
  presentationSessionEntryId: row.presentationSessionEntryId,
906
934
  acceptedSubmissionId: row.acceptedSubmissionId,
907
935
  createdAt: new Date(row.createdAt).toISOString(),
@@ -918,8 +946,14 @@ export class HostStateStore {
918
946
  }
919
947
  }
920
948
 
921
- function conflictResponse(requestId: string, error: string): HostResponse {
922
- return { schema: "pi-workflows.host-response.v1", requestId, outcome: "conflict", error };
949
+ function conflictResponse(requestId: string, error: string): ClientResponse {
950
+ return {
951
+ schema: CLIENT_PROTOCOL_SCHEMA,
952
+ type: "response",
953
+ requestId,
954
+ outcome: "conflict",
955
+ error,
956
+ };
923
957
  }
924
958
 
925
959
  function requireLeaseMs(value: number): void {
@@ -943,8 +977,8 @@ type HostRow = {
943
977
  };
944
978
 
945
979
  type CommandRow = {
946
- requestFingerprint: Buffer;
947
- outcome: HostResponse["outcome"];
980
+ clientRequestFingerprint: Buffer;
981
+ outcome: ClientResponse["outcome"];
948
982
  revision: number | null;
949
983
  receiptHash: Buffer | null;
950
984
  errorHash: Buffer | null;
@@ -970,7 +1004,7 @@ type SubmissionDetailRow = {
970
1004
  submittedAt: number;
971
1005
  };
972
1006
  type WorkerMessageRow = {
973
- requestFingerprint: Buffer;
1007
+ clientRequestFingerprint: Buffer;
974
1008
  outcome: WorkerResponse["outcome"];
975
1009
  revision: number | null;
976
1010
  resultHash: Buffer | null;
@@ -997,6 +1031,8 @@ type InteractiveRequestRow = {
997
1031
  contractHash: Buffer;
998
1032
  revision: number;
999
1033
  status: InteractiveRequestRecord["status"];
1034
+ presenterId: string | null;
1035
+ presentationClaimExpiresAt: number | null;
1000
1036
  presentationSessionEntryId: string | null;
1001
1037
  acceptedSubmissionId: string | null;
1002
1038
  createdAt: number;
@@ -1022,7 +1058,7 @@ function isHostRow(value: unknown): value is HostRow {
1022
1058
  function isCommandRow(value: unknown): value is CommandRow {
1023
1059
  return (
1024
1060
  isRecord(value) &&
1025
- Buffer.isBuffer(value.requestFingerprint) &&
1061
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1026
1062
  typeof value.outcome === "string" &&
1027
1063
  nullableNumber(value.revision) &&
1028
1064
  (value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
@@ -1049,7 +1085,7 @@ function isRunIdRow(value: unknown): value is RunIdRow {
1049
1085
  function isWorkerMessageRow(value: unknown): value is WorkerMessageRow {
1050
1086
  return (
1051
1087
  isRecord(value) &&
1052
- Buffer.isBuffer(value.requestFingerprint) &&
1088
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1053
1089
  ["accepted", "adopted", "rejected", "claimLost"].includes(value.outcome as string) &&
1054
1090
  nullableNumber(value.revision) &&
1055
1091
  (value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&
@@ -1115,6 +1151,8 @@ function isInteractiveRequestRow(value: unknown): value is InteractiveRequestRow
1115
1151
  Buffer.isBuffer(value.contractHash) &&
1116
1152
  typeof value.revision === "number" &&
1117
1153
  ["pending", "presenting", "settled", "cancelled"].includes(value.status as string) &&
1154
+ nullableString(value.presenterId) &&
1155
+ nullableNumber(value.presentationClaimExpiresAt) &&
1118
1156
  nullableString(value.presentationSessionEntryId) &&
1119
1157
  nullableString(value.acceptedSubmissionId) &&
1120
1158
  typeof value.createdAt === "number" &&