@osolmaz/pi-workflows 0.15.3 → 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 (118) 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 +43 -0
  24. package/dist/controllers/sqlite.js +137 -2
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +278 -183
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-view.d.ts +7 -2
  30. package/dist/extension/session-view.js +60 -52
  31. package/dist/extension/session-view.js.map +1 -1
  32. package/dist/extension/widget.d.ts +2 -1
  33. package/dist/extension/widget.js +14 -7
  34. package/dist/extension/widget.js.map +1 -1
  35. package/dist/host/child-worker-supervisor.js +1 -1
  36. package/dist/host/child-worker-supervisor.js.map +1 -1
  37. package/dist/host/resolver-entry.d.ts +2 -23
  38. package/dist/host/resolver-entry.js +1 -1
  39. package/dist/host/resolver-entry.js.map +1 -1
  40. package/dist/host/runner.d.ts +11 -0
  41. package/dist/host/runner.js +473 -17
  42. package/dist/host/runner.js.map +1 -1
  43. package/dist/host/state.d.ts +6 -3
  44. package/dist/host/state.js +52 -26
  45. package/dist/host/state.js.map +1 -1
  46. package/dist/host/view.d.ts +73 -0
  47. package/dist/host/view.js +871 -0
  48. package/dist/host/view.js.map +1 -0
  49. package/dist/host/worker-protocol.js +1 -1
  50. package/dist/host/worker-protocol.js.map +1 -1
  51. package/dist/state/database.d.ts +1 -0
  52. package/dist/state/database.js +15 -0
  53. package/dist/state/database.js.map +1 -1
  54. package/dist/state/prune.d.ts +3 -1
  55. package/dist/state/prune.js +6 -8
  56. package/dist/state/prune.js.map +1 -1
  57. package/dist/state/schema.js +12 -1
  58. package/dist/state/schema.js.map +1 -1
  59. package/dist/viewer/backup.d.ts +2 -0
  60. package/dist/viewer/backup.js +28 -0
  61. package/dist/viewer/backup.js.map +1 -0
  62. package/dist/viewer/cli.d.ts +4 -0
  63. package/dist/viewer/cli.js +150 -170
  64. package/dist/viewer/cli.js.map +1 -1
  65. package/dist/viewer/tui.d.ts +5 -7
  66. package/dist/viewer/tui.js +188 -108
  67. package/dist/viewer/tui.js.map +1 -1
  68. package/dist/workflows/store.d.ts +62 -1
  69. package/dist/workflows/store.js +350 -44
  70. package/dist/workflows/store.js.map +1 -1
  71. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  72. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  73. package/docs/SQLITE_STATE.md +13 -11
  74. package/docs/WORKFLOW_HOST.md +74 -61
  75. package/docs/development.md +2 -1
  76. package/docs/live-replay-protocol.md +70 -132
  77. package/docs/tui-viewer.md +10 -14
  78. package/docs/workflows.md +44 -1
  79. package/herdr-plugin.toml +1 -1
  80. package/package.json +9 -3
  81. package/protocol/client.v1.schema.json +137 -0
  82. package/protocol/fixtures/client-v1.json +23 -0
  83. package/src/client/activity.ts +6 -0
  84. package/src/client/client.ts +935 -0
  85. package/src/client/index.ts +24 -0
  86. package/src/client/materialize.ts +228 -0
  87. package/src/client/protocol.ts +327 -0
  88. package/src/client/resolver.ts +26 -0
  89. package/src/client/view.ts +138 -0
  90. package/src/controllers/sqlite.ts +213 -2
  91. package/src/extension/index.ts +349 -208
  92. package/src/extension/session-view.ts +73 -50
  93. package/src/extension/widget.ts +16 -8
  94. package/src/host/child-worker-supervisor.ts +1 -1
  95. package/src/host/resolver-entry.ts +11 -26
  96. package/src/host/runner.ts +648 -48
  97. package/src/host/state.ts +71 -43
  98. package/src/host/view.ts +1084 -0
  99. package/src/host/worker-protocol.ts +1 -1
  100. package/src/state/database.ts +13 -0
  101. package/src/state/prune.ts +11 -11
  102. package/src/state/schema.ts +12 -1
  103. package/src/viewer/backup.ts +29 -0
  104. package/src/viewer/cli.ts +171 -185
  105. package/src/viewer/tui.ts +196 -124
  106. package/src/workflows/store.ts +500 -45
  107. package/dist/host/client.d.ts +0 -48
  108. package/dist/host/client.js +0 -216
  109. package/dist/host/client.js.map +0 -1
  110. package/dist/host/protocol.d.ts +0 -38
  111. package/dist/host/protocol.js +0 -156
  112. package/dist/host/protocol.js.map +0 -1
  113. package/dist/viewer/watch.d.ts +0 -6
  114. package/dist/viewer/watch.js +0 -46
  115. package/dist/viewer/watch.js.map +0 -1
  116. package/src/host/client.ts +0 -293
  117. package/src/host/protocol.ts +0 -196
  118. 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 = {
@@ -201,53 +205,65 @@ export class HostStateStore {
201
205
  };
202
206
  }
203
207
 
204
- readCommand(request: HostRequest): HostResponse | undefined {
208
+ readCommand(request: ClientRequest): ClientResponse | undefined {
205
209
  const row = this.state.connection
206
210
  .prepare(
207
- `SELECT request_fingerprint AS requestFingerprint, outcome, accepted_revision AS revision,
211
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome, accepted_revision AS revision,
208
212
  receipt_hash AS receiptHash, error_hash AS errorHash
209
213
  FROM host_commands WHERE request_id = ?`,
210
214
  )
211
215
  .get(request.requestId);
212
216
  if (!isCommandRow(row)) return undefined;
213
- if (!row.requestFingerprint.equals(requestFingerprint(request))) {
217
+ if (!row.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
214
218
  return conflictResponse(request.requestId, "Request ID was reused with another payload");
215
219
  }
216
220
  return this.commandResponse(request.requestId, row);
217
221
  }
218
222
 
219
- executeCommand(
220
- request: HostRequest,
221
- hostEpoch: number,
222
- operation: () => Omit<HostResponse, "schema" | "requestId">,
223
- ): HostResponse {
223
+ adoptCommand(request: ClientRequest): ClientResponse | undefined {
224
224
  const existing = this.readCommand(request);
225
225
  if (existing !== undefined) {
226
- 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");
227
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;
228
259
  return this.state.transaction(() => {
229
- const idempotent = this.state.connection
230
- .prepare(
231
- `SELECT request_id AS requestId, request_fingerprint AS requestFingerprint,
232
- outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
233
- error_hash AS errorHash
234
- FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
235
- )
236
- .get(request.clientId, request.idempotencyKey);
237
- if (isIdempotentCommandRow(idempotent)) {
238
- if (!idempotent.requestFingerprint.equals(requestFingerprint(request))) {
239
- return conflictResponse(
240
- request.requestId,
241
- "Idempotency key was reused with another payload",
242
- );
243
- }
244
- const adopted = this.commandResponse(idempotent.requestId, idempotent);
245
- return { ...adopted, requestId: request.requestId, outcome: "adopted" };
246
- }
260
+ const idempotent = this.adoptCommand(request);
261
+ if (idempotent !== undefined) return idempotent;
247
262
 
248
263
  const result = operation();
249
- const response: HostResponse = {
250
- schema: "pi-workflows.host-response.v1",
264
+ const response: ClientResponse = {
265
+ schema: CLIENT_PROTOCOL_SCHEMA,
266
+ type: "response",
251
267
  requestId: request.requestId,
252
268
  ...result,
253
269
  };
@@ -269,7 +285,7 @@ export class HostStateStore {
269
285
  request.clientId,
270
286
  request.operation,
271
287
  request.idempotencyKey,
272
- requestFingerprint(request),
288
+ clientRequestFingerprint(request),
273
289
  request.runId ?? null,
274
290
  response.revision ?? null,
275
291
  response.outcome,
@@ -352,13 +368,13 @@ export class HostStateStore {
352
368
  readWorkerMessage(message: WorkerMessage): WorkerResponse | undefined {
353
369
  const row = this.state.connection
354
370
  .prepare(
355
- `SELECT request_fingerprint AS requestFingerprint, outcome,
371
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome,
356
372
  accepted_revision AS revision, result_hash AS resultHash, error_hash AS errorHash
357
373
  FROM worker_messages WHERE worker_epoch = ? AND message_id = ?`,
358
374
  )
359
375
  .get(message.workerEpoch, message.messageId);
360
376
  if (!isWorkerMessageRow(row)) return undefined;
361
- if (!row.requestFingerprint.equals(workerMessageFingerprint(message))) {
377
+ if (!row.clientRequestFingerprint.equals(workerMessageFingerprint(message))) {
362
378
  return {
363
379
  schema: "pi-workflows.worker-response.v1",
364
380
  messageId: message.messageId,
@@ -728,6 +744,7 @@ export class HostStateStore {
728
744
  receipt?: JsonValue;
729
745
  }): {
730
746
  interaction: InteractiveRequestRecord;
747
+ submissionId: string;
731
748
  outcome: "accepted" | "adopted";
732
749
  receipt: JsonValue;
733
750
  } {
@@ -747,6 +764,7 @@ export class HostStateStore {
747
764
  receipt?: JsonValue;
748
765
  }): {
749
766
  interaction: InteractiveRequestRecord;
767
+ submissionId: string;
750
768
  outcome: "accepted" | "adopted";
751
769
  receipt: JsonValue;
752
770
  } {
@@ -768,6 +786,7 @@ export class HostStateStore {
768
786
  receipt?: JsonValue;
769
787
  }): {
770
788
  interaction: InteractiveRequestRecord;
789
+ submissionId: string;
771
790
  outcome: "accepted" | "adopted";
772
791
  receipt: JsonValue;
773
792
  } {
@@ -787,6 +806,7 @@ export class HostStateStore {
787
806
  }
788
807
  return {
789
808
  interaction: this.requireInteractiveRequest(options.requestId),
809
+ submissionId: existing.submissionId,
790
810
  outcome: "adopted",
791
811
  receipt:
792
812
  existing.receiptHash === null
@@ -845,6 +865,7 @@ export class HostStateStore {
845
865
  }
846
866
  return {
847
867
  interaction: this.requireInteractiveRequest(options.requestId),
868
+ submissionId: options.submissionId,
848
869
  outcome: "accepted",
849
870
  receipt: options.receipt ?? {
850
871
  requestId: options.requestId,
@@ -867,14 +888,15 @@ export class HostStateStore {
867
888
  return row;
868
889
  }
869
890
 
870
- private commandResponse(requestId: string, row: CommandRow): HostResponse {
891
+ private commandResponse(requestId: string, row: CommandRow): ClientResponse {
871
892
  const receipt = row.receiptHash === null ? undefined : this.state.readJson(row.receiptHash);
872
893
  const error =
873
894
  row.errorHash === null
874
895
  ? undefined
875
896
  : this.state.readBlob(row.errorHash)?.content.toString("utf8");
876
897
  return {
877
- schema: "pi-workflows.host-response.v1",
898
+ schema: CLIENT_PROTOCOL_SCHEMA,
899
+ type: "response",
878
900
  requestId,
879
901
  outcome: row.outcome,
880
902
  ...(row.revision === null ? {} : { revision: row.revision }),
@@ -924,8 +946,14 @@ export class HostStateStore {
924
946
  }
925
947
  }
926
948
 
927
- function conflictResponse(requestId: string, error: string): HostResponse {
928
- 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
+ };
929
957
  }
930
958
 
931
959
  function requireLeaseMs(value: number): void {
@@ -949,8 +977,8 @@ type HostRow = {
949
977
  };
950
978
 
951
979
  type CommandRow = {
952
- requestFingerprint: Buffer;
953
- outcome: HostResponse["outcome"];
980
+ clientRequestFingerprint: Buffer;
981
+ outcome: ClientResponse["outcome"];
954
982
  revision: number | null;
955
983
  receiptHash: Buffer | null;
956
984
  errorHash: Buffer | null;
@@ -976,7 +1004,7 @@ type SubmissionDetailRow = {
976
1004
  submittedAt: number;
977
1005
  };
978
1006
  type WorkerMessageRow = {
979
- requestFingerprint: Buffer;
1007
+ clientRequestFingerprint: Buffer;
980
1008
  outcome: WorkerResponse["outcome"];
981
1009
  revision: number | null;
982
1010
  resultHash: Buffer | null;
@@ -1030,7 +1058,7 @@ function isHostRow(value: unknown): value is HostRow {
1030
1058
  function isCommandRow(value: unknown): value is CommandRow {
1031
1059
  return (
1032
1060
  isRecord(value) &&
1033
- Buffer.isBuffer(value.requestFingerprint) &&
1061
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1034
1062
  typeof value.outcome === "string" &&
1035
1063
  nullableNumber(value.revision) &&
1036
1064
  (value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
@@ -1057,7 +1085,7 @@ function isRunIdRow(value: unknown): value is RunIdRow {
1057
1085
  function isWorkerMessageRow(value: unknown): value is WorkerMessageRow {
1058
1086
  return (
1059
1087
  isRecord(value) &&
1060
- Buffer.isBuffer(value.requestFingerprint) &&
1088
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1061
1089
  ["accepted", "adopted", "rejected", "claimLost"].includes(value.outcome as string) &&
1062
1090
  nullableNumber(value.revision) &&
1063
1091
  (value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&