@osolmaz/pi-workflows 0.15.3 → 0.16.1

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 (207) hide show
  1. package/README.md +24 -8
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/activity.d.ts +2 -0
  15. package/dist/client/activity.js +6 -0
  16. package/dist/client/activity.js.map +1 -0
  17. package/dist/client/client.d.ts +102 -0
  18. package/dist/client/client.js +737 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.js +3 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/materialize.d.ts +7 -0
  24. package/dist/client/materialize.js +177 -0
  25. package/dist/client/materialize.js.map +1 -0
  26. package/dist/client/protocol.d.ts +60 -0
  27. package/dist/client/protocol.js +275 -0
  28. package/dist/client/protocol.js.map +1 -0
  29. package/dist/client/resolver.d.ts +23 -0
  30. package/dist/client/resolver.js +2 -0
  31. package/dist/client/resolver.js.map +1 -0
  32. package/dist/client/view.d.ts +144 -0
  33. package/dist/client/view.js +3 -0
  34. package/dist/client/view.js.map +1 -0
  35. package/dist/controllers/index.d.ts +1 -1
  36. package/dist/controllers/index.js.map +1 -1
  37. package/dist/controllers/sqlite.d.ts +47 -157
  38. package/dist/controllers/sqlite.js +230 -406
  39. package/dist/controllers/sqlite.js.map +1 -1
  40. package/dist/extension/index.d.ts +23 -1
  41. package/dist/extension/index.js +734 -555
  42. package/dist/extension/index.js.map +1 -1
  43. package/dist/extension/recorder.d.ts +95 -0
  44. package/dist/extension/recorder.js +530 -0
  45. package/dist/extension/recorder.js.map +1 -0
  46. package/dist/extension/remote-recorder-store.d.ts +25 -0
  47. package/dist/extension/remote-recorder-store.js +81 -0
  48. package/dist/extension/remote-recorder-store.js.map +1 -0
  49. package/dist/extension/session-events.d.ts +134 -0
  50. package/dist/extension/session-events.js +60 -0
  51. package/dist/extension/session-events.js.map +1 -0
  52. package/dist/extension/session-view.d.ts +9 -2
  53. package/dist/extension/session-view.js +117 -51
  54. package/dist/extension/session-view.js.map +1 -1
  55. package/dist/extension/step-message.d.ts +6 -4
  56. package/dist/extension/step-message.js +12 -6
  57. package/dist/extension/step-message.js.map +1 -1
  58. package/dist/extension/widget.d.ts +3 -2
  59. package/dist/extension/widget.js +41 -24
  60. package/dist/extension/widget.js.map +1 -1
  61. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  62. package/dist/extension/workflow-message-coordinator.js +294 -0
  63. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  64. package/dist/host/channel-effects.d.ts +72 -0
  65. package/dist/host/channel-effects.js +271 -0
  66. package/dist/host/channel-effects.js.map +1 -0
  67. package/dist/host/channel-supervisor.d.ts +21 -0
  68. package/dist/host/channel-supervisor.js +54 -0
  69. package/dist/host/channel-supervisor.js.map +1 -0
  70. package/dist/host/child-worker-supervisor.js +1 -1
  71. package/dist/host/child-worker-supervisor.js.map +1 -1
  72. package/dist/host/resolver-entry.d.ts +2 -23
  73. package/dist/host/resolver-entry.js +1 -1
  74. package/dist/host/resolver-entry.js.map +1 -1
  75. package/dist/host/runner.d.ts +54 -8
  76. package/dist/host/runner.js +1997 -305
  77. package/dist/host/runner.js.map +1 -1
  78. package/dist/host/state.d.ts +25 -19
  79. package/dist/host/state.js +303 -89
  80. package/dist/host/state.js.map +1 -1
  81. package/dist/host/view.d.ts +83 -0
  82. package/dist/host/view.js +975 -0
  83. package/dist/host/view.js.map +1 -0
  84. package/dist/host/worker-protocol.js +1 -1
  85. package/dist/host/worker-protocol.js.map +1 -1
  86. package/dist/state/database.d.ts +1 -0
  87. package/dist/state/database.js +15 -0
  88. package/dist/state/database.js.map +1 -1
  89. package/dist/state/prune.d.ts +3 -1
  90. package/dist/state/prune.js +9 -33
  91. package/dist/state/prune.js.map +1 -1
  92. package/dist/state/schema.js +88 -98
  93. package/dist/state/schema.js.map +1 -1
  94. package/dist/state/workflow-messages.d.ts +96 -0
  95. package/dist/state/workflow-messages.js +335 -0
  96. package/dist/state/workflow-messages.js.map +1 -0
  97. package/dist/viewer/backup.d.ts +2 -0
  98. package/dist/viewer/backup.js +28 -0
  99. package/dist/viewer/backup.js.map +1 -0
  100. package/dist/viewer/cli.d.ts +4 -0
  101. package/dist/viewer/cli.js +150 -170
  102. package/dist/viewer/cli.js.map +1 -1
  103. package/dist/viewer/render.js +1 -1
  104. package/dist/viewer/render.js.map +1 -1
  105. package/dist/viewer/tui.d.ts +5 -7
  106. package/dist/viewer/tui.js +245 -108
  107. package/dist/viewer/tui.js.map +1 -1
  108. package/dist/workflows/human-decision.d.ts +2 -0
  109. package/dist/workflows/human-decision.js +8 -2
  110. package/dist/workflows/human-decision.js.map +1 -1
  111. package/dist/workflows/index.d.ts +1 -1
  112. package/dist/workflows/index.js.map +1 -1
  113. package/dist/workflows/settings.d.ts +1 -8
  114. package/dist/workflows/settings.js.map +1 -1
  115. package/dist/workflows/store.d.ts +67 -20
  116. package/dist/workflows/store.js +451 -378
  117. package/dist/workflows/store.js.map +1 -1
  118. package/dist/workflows/tool-input.d.ts +21 -0
  119. package/dist/workflows/tool-input.js +23 -1
  120. package/dist/workflows/tool-input.js.map +1 -1
  121. package/dist/workflows/types.d.ts +5 -5
  122. package/dist/workflows/workflow-message-content.d.ts +38 -0
  123. package/dist/workflows/workflow-message-content.js +157 -0
  124. package/dist/workflows/workflow-message-content.js.map +1 -0
  125. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  126. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  127. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  128. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  129. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  130. package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
  131. package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
  132. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  133. package/docs/DEFERRED_TURNS.md +66 -294
  134. package/docs/HUMAN_DECISIONS.md +29 -30
  135. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  136. package/docs/SQLITE_STATE.md +31 -25
  137. package/docs/WORKFLOW_HOST.md +155 -77
  138. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  139. package/docs/development.md +2 -1
  140. package/docs/live-replay-protocol.md +74 -132
  141. package/docs/tui-viewer.md +12 -14
  142. package/docs/workflows.md +78 -28
  143. package/herdr-plugin.toml +1 -1
  144. package/package.json +9 -3
  145. package/plugins/herdr/viewer.mjs +14 -5
  146. package/protocol/client.v1.schema.json +137 -0
  147. package/protocol/fixtures/client-v1.json +23 -0
  148. package/src/channels/adapter-entry.ts +220 -0
  149. package/src/channels/config.ts +296 -0
  150. package/src/channels/protocol.ts +333 -0
  151. package/src/channels/telegram.ts +335 -0
  152. package/src/client/activity.ts +6 -0
  153. package/src/client/client.ts +939 -0
  154. package/src/client/index.ts +25 -0
  155. package/src/client/materialize.ts +228 -0
  156. package/src/client/protocol.ts +333 -0
  157. package/src/client/resolver.ts +26 -0
  158. package/src/client/view.ts +163 -0
  159. package/src/controllers/index.ts +0 -1
  160. package/src/controllers/sqlite.ts +339 -808
  161. package/src/extension/index.ts +853 -665
  162. package/src/extension/recorder.ts +687 -0
  163. package/src/extension/remote-recorder-store.ts +126 -0
  164. package/src/extension/session-events.ts +119 -0
  165. package/src/extension/session-view.ts +157 -51
  166. package/src/extension/step-message.ts +19 -9
  167. package/src/extension/widget.ts +53 -25
  168. package/src/extension/workflow-message-coordinator.ts +334 -0
  169. package/src/host/channel-effects.ts +465 -0
  170. package/src/host/channel-supervisor.ts +73 -0
  171. package/src/host/child-worker-supervisor.ts +1 -1
  172. package/src/host/resolver-entry.ts +11 -26
  173. package/src/host/runner.ts +2639 -450
  174. package/src/host/state.ts +404 -137
  175. package/src/host/view.ts +1212 -0
  176. package/src/host/worker-protocol.ts +1 -1
  177. package/src/state/database.ts +13 -0
  178. package/src/state/prune.ts +14 -42
  179. package/src/state/schema.ts +88 -98
  180. package/src/state/workflow-messages.ts +548 -0
  181. package/src/viewer/backup.ts +29 -0
  182. package/src/viewer/cli.ts +171 -185
  183. package/src/viewer/render.ts +1 -5
  184. package/src/viewer/tui.ts +261 -124
  185. package/src/workflows/human-decision.ts +18 -2
  186. package/src/workflows/index.ts +0 -1
  187. package/src/workflows/settings.ts +1 -20
  188. package/src/workflows/store.ts +650 -511
  189. package/src/workflows/tool-input.ts +36 -1
  190. package/src/workflows/types.ts +5 -5
  191. package/src/workflows/workflow-message-content.ts +197 -0
  192. package/dist/extension/decision-channels.d.ts +0 -134
  193. package/dist/extension/decision-channels.js +0 -1307
  194. package/dist/extension/decision-channels.js.map +0 -1
  195. package/dist/host/client.d.ts +0 -48
  196. package/dist/host/client.js +0 -216
  197. package/dist/host/client.js.map +0 -1
  198. package/dist/host/protocol.d.ts +0 -38
  199. package/dist/host/protocol.js +0 -156
  200. package/dist/host/protocol.js.map +0 -1
  201. package/dist/viewer/watch.d.ts +0 -6
  202. package/dist/viewer/watch.js +0 -46
  203. package/dist/viewer/watch.js.map +0 -1
  204. package/src/extension/decision-channels.ts +0 -1826
  205. package/src/host/client.ts +0 -293
  206. package/src/host/protocol.ts +0 -196
  207. package/src/viewer/watch.ts +0 -51
package/src/host/state.ts CHANGED
@@ -1,9 +1,22 @@
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";
11
+ import {
12
+ WorkflowMessageStore,
13
+ workflowMessageIdFor,
14
+ type WorkflowStepReason,
15
+ } from "../state/workflow-messages.js";
16
+ import {
17
+ decisionWorkflowMessageContent,
18
+ stepWorkflowMessageContent,
19
+ } from "../workflows/workflow-message-content.js";
7
20
  import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
8
21
 
9
22
  export type HostClaim = {
@@ -64,10 +77,8 @@ export type InteractiveRequestRecord = {
64
77
  kind: "agent" | "assistant" | "decision";
65
78
  contract: JsonValue;
66
79
  revision: number;
67
- status: "pending" | "presenting" | "settled" | "cancelled";
68
- presenterId: string | null;
69
- presentationClaimExpiresAt: string | null;
70
- presentationSessionEntryId: string | null;
80
+ status: "pending" | "settled" | "cancelled";
81
+ unproductiveTurnEnds: number;
71
82
  acceptedSubmissionId: string | null;
72
83
  createdAt: string;
73
84
  updatedAt: string;
@@ -78,6 +89,7 @@ export type InteractiveRequestRecord = {
78
89
  export class HostStateStore {
79
90
  readonly state: StateDatabase;
80
91
  private readonly ownsState: boolean;
92
+ readonly workflowMessages: WorkflowMessageStore;
81
93
 
82
94
  constructor(databasePath: string, options: { state?: StateDatabase; readOnly?: boolean } = {}) {
83
95
  this.ownsState = options.state === undefined;
@@ -87,6 +99,7 @@ export class HostStateStore {
87
99
  filePath: databasePath,
88
100
  mode: options.readOnly === true ? "read-only" : "read-write",
89
101
  });
102
+ this.workflowMessages = new WorkflowMessageStore(this.state);
90
103
  }
91
104
 
92
105
  close(): void {
@@ -201,53 +214,65 @@ export class HostStateStore {
201
214
  };
202
215
  }
203
216
 
204
- readCommand(request: HostRequest): HostResponse | undefined {
217
+ readCommand(request: ClientRequest): ClientResponse | undefined {
205
218
  const row = this.state.connection
206
219
  .prepare(
207
- `SELECT request_fingerprint AS requestFingerprint, outcome, accepted_revision AS revision,
220
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome, accepted_revision AS revision,
208
221
  receipt_hash AS receiptHash, error_hash AS errorHash
209
222
  FROM host_commands WHERE request_id = ?`,
210
223
  )
211
224
  .get(request.requestId);
212
225
  if (!isCommandRow(row)) return undefined;
213
- if (!row.requestFingerprint.equals(requestFingerprint(request))) {
226
+ if (!row.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
214
227
  return conflictResponse(request.requestId, "Request ID was reused with another payload");
215
228
  }
216
229
  return this.commandResponse(request.requestId, row);
217
230
  }
218
231
 
219
- executeCommand(
220
- request: HostRequest,
221
- hostEpoch: number,
222
- operation: () => Omit<HostResponse, "schema" | "requestId">,
223
- ): HostResponse {
232
+ adoptCommand(request: ClientRequest): ClientResponse | undefined {
224
233
  const existing = this.readCommand(request);
225
234
  if (existing !== undefined) {
226
- return existing.outcome === "conflict" ? existing : { ...existing, outcome: "adopted" };
235
+ return existing.outcome === "accepted" || existing.outcome === "adopted"
236
+ ? { ...existing, outcome: "adopted" }
237
+ : existing;
238
+ }
239
+ const idempotent = this.state.connection
240
+ .prepare(
241
+ `SELECT request_id AS requestId, request_fingerprint AS clientRequestFingerprint,
242
+ outcome, accepted_revision AS revision, receipt_hash AS receiptHash,
243
+ error_hash AS errorHash
244
+ FROM host_commands WHERE client_id = ? AND idempotency_key = ?`,
245
+ )
246
+ .get(request.clientId, request.idempotencyKey);
247
+ if (!isIdempotentCommandRow(idempotent)) return undefined;
248
+ if (!idempotent.clientRequestFingerprint.equals(clientRequestFingerprint(request))) {
249
+ return conflictResponse(request.requestId, "Idempotency key was reused with another payload");
227
250
  }
251
+ const adopted = this.commandResponse(idempotent.requestId, idempotent);
252
+ return {
253
+ ...adopted,
254
+ requestId: request.requestId,
255
+ ...(adopted.outcome === "accepted" || adopted.outcome === "adopted"
256
+ ? { outcome: "adopted" as const }
257
+ : {}),
258
+ };
259
+ }
260
+
261
+ executeCommand(
262
+ request: ClientRequest,
263
+ hostEpoch: number,
264
+ operation: () => Omit<ClientResponse, "schema" | "type" | "requestId">,
265
+ ): ClientResponse {
266
+ const existing = this.adoptCommand(request);
267
+ if (existing !== undefined) return existing;
228
268
  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
- }
269
+ const idempotent = this.adoptCommand(request);
270
+ if (idempotent !== undefined) return idempotent;
247
271
 
248
272
  const result = operation();
249
- const response: HostResponse = {
250
- schema: "pi-workflows.host-response.v1",
273
+ const response: ClientResponse = {
274
+ schema: CLIENT_PROTOCOL_SCHEMA,
275
+ type: "response",
251
276
  requestId: request.requestId,
252
277
  ...result,
253
278
  };
@@ -269,7 +294,7 @@ export class HostStateStore {
269
294
  request.clientId,
270
295
  request.operation,
271
296
  request.idempotencyKey,
272
- requestFingerprint(request),
297
+ clientRequestFingerprint(request),
273
298
  request.runId ?? null,
274
299
  response.revision ?? null,
275
300
  response.outcome,
@@ -352,13 +377,13 @@ export class HostStateStore {
352
377
  readWorkerMessage(message: WorkerMessage): WorkerResponse | undefined {
353
378
  const row = this.state.connection
354
379
  .prepare(
355
- `SELECT request_fingerprint AS requestFingerprint, outcome,
380
+ `SELECT request_fingerprint AS clientRequestFingerprint, outcome,
356
381
  accepted_revision AS revision, result_hash AS resultHash, error_hash AS errorHash
357
382
  FROM worker_messages WHERE worker_epoch = ? AND message_id = ?`,
358
383
  )
359
384
  .get(message.workerEpoch, message.messageId);
360
385
  if (!isWorkerMessageRow(row)) return undefined;
361
- if (!row.requestFingerprint.equals(workerMessageFingerprint(message))) {
386
+ if (!row.clientRequestFingerprint.equals(workerMessageFingerprint(message))) {
362
387
  return {
363
388
  schema: "pi-workflows.worker-response.v1",
364
389
  messageId: message.messageId,
@@ -422,15 +447,16 @@ export class HostStateStore {
422
447
  this.state.connection
423
448
  .prepare(
424
449
  `UPDATE interactive_requests
425
- SET status = 'pending', presenter_id = NULL,
426
- presentation_claim_expires_at = NULL, presentation_session_entry_id = NULL,
427
- accepted_submission_id = NULL, settled_at = NULL, consumed_at = NULL,
428
- revision = revision + 1, updated_at = ?
450
+ SET status = 'pending', accepted_submission_id = NULL,
451
+ settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
429
452
  WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`,
430
453
  )
431
454
  .run(now, options.requestId);
432
- return this.requireInteractiveRequest(options.requestId);
455
+ const reopened = this.requireInteractiveRequest(options.requestId);
456
+ this.ensureInteractionMessage(reopened, "resumed", now);
457
+ return reopened;
433
458
  }
459
+ if (existing.status === "pending") this.ensureInteractionMessage(existing, "initial", now);
434
460
  return existing;
435
461
  }
436
462
  const contractHash = this.state.putJson(options.contract, now);
@@ -451,10 +477,213 @@ export class HostStateStore {
451
477
  now,
452
478
  now,
453
479
  );
454
- return this.requireInteractiveRequest(options.requestId);
480
+ const created = this.requireInteractiveRequest(options.requestId);
481
+ this.ensureInteractionMessage(created, "initial", now);
482
+ return created;
483
+ });
484
+ }
485
+
486
+ ensureInteractionMessage(
487
+ request: InteractiveRequestRecord,
488
+ reason: WorkflowStepReason,
489
+ now: number = Date.now(),
490
+ ) {
491
+ const kind = request.kind === "decision" ? "decision" : "step";
492
+ const idempotencyKey = `${request.revision}:${reason}`;
493
+ const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
494
+ const content =
495
+ kind === "decision"
496
+ ? decisionWorkflowMessageContent({
497
+ workflowMessageId,
498
+ requestId: request.requestId,
499
+ runId: request.runId,
500
+ contract: request.contract,
501
+ })
502
+ : stepWorkflowMessageContent({
503
+ workflowMessageId,
504
+ requestId: request.requestId,
505
+ contract: request.contract,
506
+ reason,
507
+ });
508
+ return this.workflowMessages.create({
509
+ workflowMessageId,
510
+ runId: request.runId,
511
+ targetSessionId: request.targetSessionId,
512
+ kind,
513
+ sourceId: request.requestId,
514
+ idempotencyKey,
515
+ content,
516
+ now,
455
517
  });
456
518
  }
457
519
 
520
+ resumePendingInteraction(runId: string, now: number = Date.now()): InteractiveRequestRecord {
521
+ return this.state.transaction(() => {
522
+ const row = this.state.connection
523
+ .prepare(
524
+ `SELECT request_id AS requestId FROM interactive_requests
525
+ WHERE run_id = ? AND status = 'pending' ORDER BY created_at LIMIT 1`,
526
+ )
527
+ .get(runId);
528
+ if (!isRequestIdRow(row)) throw new Error("Pending workflow interaction is missing");
529
+ const current = this.requireInteractiveRequest(row.requestId);
530
+ if (current.kind === "decision") return current;
531
+ this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
532
+ const changed = this.state.connection
533
+ .prepare(
534
+ `UPDATE interactive_requests SET revision = revision + 1, updated_at = ?
535
+ WHERE request_id = ? AND status = 'pending'`,
536
+ )
537
+ .run(now, row.requestId);
538
+ if (changed.changes !== 1) throw new Error("Pending workflow interaction resume is stale");
539
+ const request = this.requireInteractiveRequest(row.requestId);
540
+ this.ensureInteractionMessage(request, "resumed", now);
541
+ return request;
542
+ });
543
+ }
544
+
545
+ beginInteractionModelTurn(requestId: string, now: number = Date.now()): void {
546
+ const row = this.state.connection
547
+ .prepare(
548
+ `SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
549
+ a.started_at AS startedAt, a.deadline_at AS deadlineAt,
550
+ (SELECT MAX(t.ended_at)
551
+ FROM workflow_messages m
552
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
553
+ WHERE m.source_id = i.request_id AND m.kind = 'step'
554
+ AND t.state = 'ended') AS previousTurnEndedAt
555
+ FROM interactive_requests i
556
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
557
+ WHERE i.request_id = ? AND i.status = 'pending'`,
558
+ )
559
+ .get(requestId);
560
+ if (!isInteractionTimeoutRow(row)) {
561
+ throw new Error("Pending workflow interaction timeout is missing");
562
+ }
563
+ this.shiftInteractionTimeout(
564
+ row,
565
+ Math.max(0, now - (row.previousTurnEndedAt ?? row.startedAt ?? row.createdAt)),
566
+ now,
567
+ );
568
+ }
569
+
570
+ interactionPauseStartedAt(runId: string): number | undefined {
571
+ const row = this.state.connection
572
+ .prepare(
573
+ `SELECT r.updated_at AS pausedAt
574
+ FROM runs r
575
+ JOIN interactive_requests i ON i.run_id = r.run_id AND i.status = 'pending'
576
+ WHERE r.run_id = ? AND r.paused = 1 LIMIT 1`,
577
+ )
578
+ .get(runId);
579
+ return isPausedAtRow(row) ? row.pausedAt : undefined;
580
+ }
581
+
582
+ resumeInteractionModelTurn(
583
+ runId: string,
584
+ pausedAt: number | undefined,
585
+ now: number = Date.now(),
586
+ ): void {
587
+ if (pausedAt === undefined) return;
588
+ const row = this.state.connection
589
+ .prepare(
590
+ `SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
591
+ a.started_at AS startedAt, a.deadline_at AS deadlineAt,
592
+ t.ended_at AS previousTurnEndedAt
593
+ FROM interactive_requests i
594
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
595
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
596
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
597
+ WHERE i.run_id = ? AND i.status = 'pending' AND t.started_at <= ?
598
+ AND (t.ended_at IS NULL OR t.ended_at > ?)
599
+ ORDER BY t.started_at DESC LIMIT 1`,
600
+ )
601
+ .get(runId, pausedAt, pausedAt);
602
+ if (!isInteractionTimeoutRow(row)) return;
603
+ const modelTurnStoppedAt = Math.min(now, row.previousTurnEndedAt ?? now);
604
+ this.shiftInteractionTimeout(row, Math.max(0, modelTurnStoppedAt - pausedAt), now);
605
+ }
606
+
607
+ markSessionModelTurnsInactive(targetSessionId: string, now: number = Date.now()): void {
608
+ this.state.connection
609
+ .prepare(
610
+ `UPDATE node_attempts SET status = 'interrupted', updated_at = ?
611
+ WHERE status <> 'interrupted' AND attempt_id IN (
612
+ SELECT DISTINCT i.attempt_id
613
+ FROM interactive_requests i
614
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
615
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
616
+ AND t.state = 'started'
617
+ WHERE i.target_session_id = ? AND i.status = 'pending'
618
+ )`,
619
+ )
620
+ .run(now, targetSessionId);
621
+ }
622
+
623
+ recoverInactiveModelTurns(
624
+ previousHeartbeatAt: number | undefined,
625
+ now: number = Date.now(),
626
+ ): void {
627
+ const rows = this.state.connection
628
+ .prepare(
629
+ `SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
630
+ a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
631
+ MAX(t.started_at) AS turnStartedAt
632
+ FROM interactive_requests i
633
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
634
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
635
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
636
+ AND t.state = 'started'
637
+ WHERE i.status = 'pending'
638
+ GROUP BY i.attempt_id`,
639
+ )
640
+ .all()
641
+ .filter(isInactiveInteractionTimeoutRow);
642
+ for (const row of rows) {
643
+ const inactiveAt =
644
+ row.status === "interrupted"
645
+ ? row.updatedAt
646
+ : previousHeartbeatAt === undefined
647
+ ? row.updatedAt
648
+ : Math.max(previousHeartbeatAt, row.turnStartedAt);
649
+ this.shiftInteractionTimeout(row, Math.max(0, now - inactiveAt), now);
650
+ this.state.connection
651
+ .prepare(
652
+ `UPDATE node_attempts SET status = 'interrupted', updated_at = ?
653
+ WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`,
654
+ )
655
+ .run(now, row.attemptId);
656
+ }
657
+ }
658
+
659
+ resumeSessionModelTurns(targetSessionId: string, now: number = Date.now()): void {
660
+ const rows = this.state.connection
661
+ .prepare(
662
+ `SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
663
+ a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
664
+ MAX(t.started_at) AS turnStartedAt
665
+ FROM interactive_requests i
666
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
667
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
668
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
669
+ AND t.state = 'started'
670
+ WHERE i.target_session_id = ? AND i.status = 'pending'
671
+ AND a.status = 'interrupted'
672
+ GROUP BY i.attempt_id`,
673
+ )
674
+ .all(targetSessionId)
675
+ .filter(isInactiveInteractionTimeoutRow);
676
+ for (const row of rows) {
677
+ this.shiftInteractionTimeout(row, Math.max(0, now - row.updatedAt), now);
678
+ this.state.connection
679
+ .prepare(
680
+ `UPDATE node_attempts SET status = 'waiting', updated_at = ?
681
+ WHERE attempt_id = ? AND status = 'interrupted'`,
682
+ )
683
+ .run(now, row.attemptId);
684
+ }
685
+ }
686
+
458
687
  getInteraction(requestId: string): InteractiveRequestRecord | undefined {
459
688
  return this.interactiveRequest(requestId);
460
689
  }
@@ -505,7 +734,7 @@ export class HostStateStore {
505
734
  FROM interactive_requests i
506
735
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
507
736
  JOIN interactive_submissions s ON s.request_id = i.request_id
508
- WHERE i.run_id = ? AND i.status IN ('pending', 'presenting')
737
+ WHERE i.run_id = ? AND i.status = 'pending'
509
738
  AND s.outcome = 'validating'
510
739
  ORDER BY s.submitted_at DESC LIMIT 1`,
511
740
  )
@@ -527,7 +756,7 @@ export class HostStateStore {
527
756
  FROM interactive_requests i
528
757
  JOIN interactive_submissions s ON s.request_id = i.request_id
529
758
  JOIN run_queue q ON q.run_id = i.run_id
530
- WHERE i.status IN ('pending', 'presenting') AND s.outcome = 'validating'
759
+ WHERE i.status = 'pending' AND s.outcome = 'validating'
531
760
  AND q.status NOT IN ('done', 'failed', 'cancelled')
532
761
  GROUP BY i.run_id
533
762
  ORDER BY MIN(s.submitted_at), i.run_id`,
@@ -536,21 +765,27 @@ export class HostStateStore {
536
765
  .flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
537
766
  }
538
767
 
539
- expiredInteractionRunIds(now: number = Date.now()): string[] {
768
+ expiredInteractionRuns(now: number = Date.now()): ExpiredInteractionRunRow[] {
540
769
  return this.state.connection
541
770
  .prepare(
542
- `SELECT i.run_id AS runId
771
+ `SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
543
772
  FROM interactive_requests i
544
773
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
774
+ JOIN runs r ON r.run_id = i.run_id
545
775
  JOIN run_queue q ON q.run_id = i.run_id
546
- WHERE i.status IN ('pending', 'presenting')
776
+ WHERE i.status = 'pending' AND r.paused = 0
547
777
  AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
548
778
  AND q.status NOT IN ('done', 'failed', 'cancelled')
549
- GROUP BY i.run_id
779
+ AND EXISTS (
780
+ SELECT 1 FROM workflow_messages m
781
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
782
+ WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
783
+ )
784
+ GROUP BY i.run_id, i.target_session_id
550
785
  ORDER BY MIN(a.deadline_at), i.run_id`,
551
786
  )
552
787
  .all(now)
553
- .flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
788
+ .filter(isExpiredInteractionRunRow);
554
789
  }
555
790
 
556
791
  timedOutInteraction(runId: string): TimedOutInteractionRow | undefined {
@@ -561,8 +796,13 @@ export class HostStateStore {
561
796
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
562
797
  JOIN runs r ON r.run_id = i.run_id
563
798
  WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
564
- AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
565
- AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
799
+ AND r.paused = 0 AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
800
+ AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
801
+ AND EXISTS (
802
+ SELECT 1 FROM workflow_messages m
803
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
804
+ WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
805
+ )
566
806
  ORDER BY a.deadline_at, i.request_id LIMIT 1`,
567
807
  )
568
808
  .get(runId, Date.now());
@@ -624,13 +864,14 @@ export class HostStateStore {
624
864
  const changed = this.state.connection
625
865
  .prepare(
626
866
  `UPDATE interactive_requests
627
- SET status = 'settled', presenter_id = NULL,
628
- presentation_claim_expires_at = NULL, accepted_submission_id = ?,
867
+ SET status = 'settled', accepted_submission_id = ?,
629
868
  revision = revision + 1, updated_at = ?, settled_at = ?
630
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')`,
869
+ WHERE request_id = ? AND revision = ? AND status = 'pending'`,
631
870
  )
632
871
  .run(options.submissionId, now, now, options.requestId, request.revision);
633
872
  if (changed.changes !== 1) throw new Error("Interactive request validation is stale");
873
+ this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
874
+ this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
634
875
  }
635
876
  const settled = this.interactionSubmission(options.requestId, options.submissionId);
636
877
  if (settled === undefined) throw new Error("Interactive submission result is missing");
@@ -653,7 +894,7 @@ export class HostStateStore {
653
894
  return this.state.connection
654
895
  .prepare(
655
896
  `SELECT request_id AS requestId FROM interactive_requests
656
- WHERE target_session_id = ? AND status IN ('pending', 'presenting')
897
+ WHERE target_session_id = ? AND status = 'pending'
657
898
  ORDER BY created_at, request_id`,
658
899
  )
659
900
  .all(sessionId)
@@ -662,60 +903,17 @@ export class HostStateStore {
662
903
  );
663
904
  }
664
905
 
665
- claimInteractionPresentation(options: {
666
- requestId: string;
667
- expectedRevision: number;
668
- presenterId: string;
669
- leaseMs: number;
670
- }): InteractiveRequestRecord {
671
- requireLeaseMs(options.leaseMs);
672
- const now = Date.now();
673
- const changed = this.state.connection
674
- .prepare(
675
- `UPDATE interactive_requests
676
- SET status = 'presenting', presenter_id = ?, presentation_claim_expires_at = ?,
677
- revision = revision + 1, updated_at = ?
678
- WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
679
- AND (
680
- status = 'pending'
681
- OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
682
- )`,
683
- )
684
- .run(
685
- options.presenterId,
686
- now + options.leaseMs,
687
- now,
688
- options.requestId,
689
- options.expectedRevision,
690
- now,
691
- );
692
- if (changed.changes !== 1) throw new Error("Interactive request presentation claim conflict");
693
- return this.requireInteractiveRequest(options.requestId);
694
- }
695
-
696
- markInteractionPresented(options: {
697
- requestId: string;
698
- expectedRevision: number;
699
- sessionEntryId: string;
700
- }): InteractiveRequestRecord {
701
- const now = Date.now();
702
- const changed = this.state.connection
906
+ listPendingDecisionInteractions(): InteractiveRequestRecord[] {
907
+ return this.state.connection
703
908
  .prepare(
704
- `UPDATE interactive_requests
705
- SET status = 'presenting', presenter_id = NULL, presentation_claim_expires_at = NULL,
706
- presentation_session_entry_id = ?, revision = revision + 1, updated_at = ?
707
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')
708
- AND (presentation_session_entry_id IS NULL OR presentation_session_entry_id = ?)`,
909
+ `SELECT request_id AS requestId FROM interactive_requests
910
+ WHERE kind = 'decision' AND status = 'pending'
911
+ ORDER BY created_at, request_id`,
709
912
  )
710
- .run(
711
- options.sessionEntryId,
712
- now,
713
- options.requestId,
714
- options.expectedRevision,
715
- options.sessionEntryId,
913
+ .all()
914
+ .flatMap((row) =>
915
+ isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : [],
716
916
  );
717
- if (changed.changes !== 1) throw new Error("Interactive request presentation conflict");
718
- return this.requireInteractiveRequest(options.requestId);
719
917
  }
720
918
 
721
919
  submitInteraction(options: {
@@ -728,6 +926,7 @@ export class HostStateStore {
728
926
  receipt?: JsonValue;
729
927
  }): {
730
928
  interaction: InteractiveRequestRecord;
929
+ submissionId: string;
731
930
  outcome: "accepted" | "adopted";
732
931
  receipt: JsonValue;
733
932
  } {
@@ -747,6 +946,7 @@ export class HostStateStore {
747
946
  receipt?: JsonValue;
748
947
  }): {
749
948
  interaction: InteractiveRequestRecord;
949
+ submissionId: string;
750
950
  outcome: "accepted" | "adopted";
751
951
  receipt: JsonValue;
752
952
  } {
@@ -768,6 +968,7 @@ export class HostStateStore {
768
968
  receipt?: JsonValue;
769
969
  }): {
770
970
  interaction: InteractiveRequestRecord;
971
+ submissionId: string;
771
972
  outcome: "accepted" | "adopted";
772
973
  receipt: JsonValue;
773
974
  } {
@@ -787,6 +988,7 @@ export class HostStateStore {
787
988
  }
788
989
  return {
789
990
  interaction: this.requireInteractiveRequest(options.requestId),
991
+ submissionId: existing.submissionId,
790
992
  outcome: "adopted",
791
993
  receipt:
792
994
  existing.receiptHash === null
@@ -806,10 +1008,7 @@ export class HostStateStore {
806
1008
  }
807
1009
  }
808
1010
  const request = this.requireInteractiveRequest(options.requestId);
809
- if (
810
- request.revision !== options.expectedRevision ||
811
- !["pending", "presenting"].includes(request.status)
812
- ) {
1011
+ if (request.revision !== options.expectedRevision || request.status !== "pending") {
813
1012
  throw new Error("Interactive request revision conflict");
814
1013
  }
815
1014
  const savedPayloadHash = this.state.putJson(options.payload, now);
@@ -836,15 +1035,17 @@ export class HostStateStore {
836
1035
  this.state.connection
837
1036
  .prepare(
838
1037
  `UPDATE interactive_requests
839
- SET status = 'settled', presenter_id = NULL, presentation_claim_expires_at = NULL,
840
- accepted_submission_id = ?, revision = revision + 1,
1038
+ SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
841
1039
  updated_at = ?, settled_at = ?
842
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')`,
1040
+ WHERE request_id = ? AND revision = ? AND status = 'pending'`,
843
1041
  )
844
1042
  .run(options.submissionId, now, now, options.requestId, options.expectedRevision);
1043
+ this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
1044
+ this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
845
1045
  }
846
1046
  return {
847
1047
  interaction: this.requireInteractiveRequest(options.requestId),
1048
+ submissionId: options.submissionId,
848
1049
  outcome: "accepted",
849
1050
  receipt: options.receipt ?? {
850
1051
  requestId: options.requestId,
@@ -867,14 +1068,15 @@ export class HostStateStore {
867
1068
  return row;
868
1069
  }
869
1070
 
870
- private commandResponse(requestId: string, row: CommandRow): HostResponse {
1071
+ private commandResponse(requestId: string, row: CommandRow): ClientResponse {
871
1072
  const receipt = row.receiptHash === null ? undefined : this.state.readJson(row.receiptHash);
872
1073
  const error =
873
1074
  row.errorHash === null
874
1075
  ? undefined
875
1076
  : this.state.readBlob(row.errorHash)?.content.toString("utf8");
876
1077
  return {
877
- schema: "pi-workflows.host-response.v1",
1078
+ schema: CLIENT_PROTOCOL_SCHEMA,
1079
+ type: "response",
878
1080
  requestId,
879
1081
  outcome: row.outcome,
880
1082
  ...(row.revision === null ? {} : { revision: row.revision }),
@@ -888,9 +1090,7 @@ export class HostStateStore {
888
1090
  .prepare(
889
1091
  `SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
890
1092
  target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
891
- revision, status, presenter_id AS presenterId,
892
- presentation_claim_expires_at AS presentationClaimExpiresAt,
893
- presentation_session_entry_id AS presentationSessionEntryId,
1093
+ revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
894
1094
  accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
895
1095
  updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
896
1096
  FROM interactive_requests WHERE request_id = ?`,
@@ -906,9 +1106,7 @@ export class HostStateStore {
906
1106
  contract: this.state.readJson(row.contractHash),
907
1107
  revision: row.revision,
908
1108
  status: row.status,
909
- presenterId: row.presenterId,
910
- presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
911
- presentationSessionEntryId: row.presentationSessionEntryId,
1109
+ unproductiveTurnEnds: row.unproductiveTurnEnds,
912
1110
  acceptedSubmissionId: row.acceptedSubmissionId,
913
1111
  createdAt: new Date(row.createdAt).toISOString(),
914
1112
  updatedAt: new Date(row.updatedAt).toISOString(),
@@ -917,6 +1115,23 @@ export class HostStateStore {
917
1115
  };
918
1116
  }
919
1117
 
1118
+ private shiftInteractionTimeout(
1119
+ row: Pick<InteractionTimeoutRow, "attemptId" | "startedAt" | "deadlineAt">,
1120
+ idleMs: number,
1121
+ now: number,
1122
+ ): void {
1123
+ if (idleMs === 0 || row.deadlineAt === null) return;
1124
+ const changed = this.state.connection
1125
+ .prepare(
1126
+ `UPDATE node_attempts
1127
+ SET started_at = CASE WHEN started_at IS NULL THEN NULL ELSE started_at + ? END,
1128
+ deadline_at = deadline_at + ?, updated_at = ?
1129
+ WHERE attempt_id = ? AND deadline_at = ?`,
1130
+ )
1131
+ .run(idleMs, idleMs, now, row.attemptId, row.deadlineAt);
1132
+ if (changed.changes !== 1) throw new Error("Workflow interaction timeout changed concurrently");
1133
+ }
1134
+
920
1135
  private requireInteractiveRequest(requestId: string): InteractiveRequestRecord {
921
1136
  const request = this.interactiveRequest(requestId);
922
1137
  if (request === undefined) throw new Error(`Interactive request not found: ${requestId}`);
@@ -924,8 +1139,14 @@ export class HostStateStore {
924
1139
  }
925
1140
  }
926
1141
 
927
- function conflictResponse(requestId: string, error: string): HostResponse {
928
- return { schema: "pi-workflows.host-response.v1", requestId, outcome: "conflict", error };
1142
+ function conflictResponse(requestId: string, error: string): ClientResponse {
1143
+ return {
1144
+ schema: CLIENT_PROTOCOL_SCHEMA,
1145
+ type: "response",
1146
+ requestId,
1147
+ outcome: "conflict",
1148
+ error,
1149
+ };
929
1150
  }
930
1151
 
931
1152
  function requireLeaseMs(value: number): void {
@@ -949,8 +1170,8 @@ type HostRow = {
949
1170
  };
950
1171
 
951
1172
  type CommandRow = {
952
- requestFingerprint: Buffer;
953
- outcome: HostResponse["outcome"];
1173
+ clientRequestFingerprint: Buffer;
1174
+ outcome: ClientResponse["outcome"];
954
1175
  revision: number | null;
955
1176
  receiptHash: Buffer | null;
956
1177
  errorHash: Buffer | null;
@@ -976,7 +1197,7 @@ type SubmissionDetailRow = {
976
1197
  submittedAt: number;
977
1198
  };
978
1199
  type WorkerMessageRow = {
979
- requestFingerprint: Buffer;
1200
+ clientRequestFingerprint: Buffer;
980
1201
  outcome: WorkerResponse["outcome"];
981
1202
  revision: number | null;
982
1203
  resultHash: Buffer | null;
@@ -994,6 +1215,23 @@ type TimedOutInteractionRow = {
994
1215
  attemptId: string;
995
1216
  nodeId: string;
996
1217
  };
1218
+ type InteractionTimeoutRow = {
1219
+ attemptId: string;
1220
+ createdAt: number;
1221
+ startedAt: number | null;
1222
+ deadlineAt: number | null;
1223
+ previousTurnEndedAt: number | null;
1224
+ };
1225
+ type InactiveInteractionTimeoutRow = {
1226
+ attemptId: string;
1227
+ status: "pending" | "running" | "waiting" | "interrupted";
1228
+ startedAt: number | null;
1229
+ deadlineAt: number | null;
1230
+ updatedAt: number;
1231
+ turnStartedAt: number;
1232
+ };
1233
+ type ExpiredInteractionRunRow = { runId: string; targetSessionId: string };
1234
+ type PausedAtRow = { pausedAt: number };
997
1235
  type InteractiveRequestRow = {
998
1236
  requestId: string;
999
1237
  runId: string;
@@ -1003,9 +1241,7 @@ type InteractiveRequestRow = {
1003
1241
  contractHash: Buffer;
1004
1242
  revision: number;
1005
1243
  status: InteractiveRequestRecord["status"];
1006
- presenterId: string | null;
1007
- presentationClaimExpiresAt: number | null;
1008
- presentationSessionEntryId: string | null;
1244
+ unproductiveTurnEnds: number;
1009
1245
  acceptedSubmissionId: string | null;
1010
1246
  createdAt: number;
1011
1247
  updatedAt: number;
@@ -1030,7 +1266,7 @@ function isHostRow(value: unknown): value is HostRow {
1030
1266
  function isCommandRow(value: unknown): value is CommandRow {
1031
1267
  return (
1032
1268
  isRecord(value) &&
1033
- Buffer.isBuffer(value.requestFingerprint) &&
1269
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1034
1270
  typeof value.outcome === "string" &&
1035
1271
  nullableNumber(value.revision) &&
1036
1272
  (value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
@@ -1057,7 +1293,7 @@ function isRunIdRow(value: unknown): value is RunIdRow {
1057
1293
  function isWorkerMessageRow(value: unknown): value is WorkerMessageRow {
1058
1294
  return (
1059
1295
  isRecord(value) &&
1060
- Buffer.isBuffer(value.requestFingerprint) &&
1296
+ Buffer.isBuffer(value.clientRequestFingerprint) &&
1061
1297
  ["accepted", "adopted", "rejected", "claimLost"].includes(value.outcome as string) &&
1062
1298
  nullableNumber(value.revision) &&
1063
1299
  (value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&
@@ -1085,6 +1321,39 @@ function isTimedOutInteractionRow(value: unknown): value is TimedOutInteractionR
1085
1321
  );
1086
1322
  }
1087
1323
 
1324
+ function isInteractionTimeoutRow(value: unknown): value is InteractionTimeoutRow {
1325
+ return (
1326
+ isRecord(value) &&
1327
+ typeof value.attemptId === "string" &&
1328
+ typeof value.createdAt === "number" &&
1329
+ nullableNumber(value.startedAt) &&
1330
+ nullableNumber(value.deadlineAt) &&
1331
+ nullableNumber(value.previousTurnEndedAt)
1332
+ );
1333
+ }
1334
+
1335
+ function isInactiveInteractionTimeoutRow(value: unknown): value is InactiveInteractionTimeoutRow {
1336
+ return (
1337
+ isRecord(value) &&
1338
+ typeof value.attemptId === "string" &&
1339
+ ["pending", "running", "waiting", "interrupted"].includes(value.status as string) &&
1340
+ nullableNumber(value.startedAt) &&
1341
+ nullableNumber(value.deadlineAt) &&
1342
+ typeof value.updatedAt === "number" &&
1343
+ typeof value.turnStartedAt === "number"
1344
+ );
1345
+ }
1346
+
1347
+ function isExpiredInteractionRunRow(value: unknown): value is ExpiredInteractionRunRow {
1348
+ return (
1349
+ isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string"
1350
+ );
1351
+ }
1352
+
1353
+ function isPausedAtRow(value: unknown): value is PausedAtRow {
1354
+ return isRecord(value) && typeof value.pausedAt === "number";
1355
+ }
1356
+
1088
1357
  function isSubmissionRow(value: unknown): value is SubmissionRow {
1089
1358
  return (
1090
1359
  isRecord(value) &&
@@ -1122,10 +1391,8 @@ function isInteractiveRequestRow(value: unknown): value is InteractiveRequestRow
1122
1391
  ["agent", "assistant", "decision"].includes(value.kind as string) &&
1123
1392
  Buffer.isBuffer(value.contractHash) &&
1124
1393
  typeof value.revision === "number" &&
1125
- ["pending", "presenting", "settled", "cancelled"].includes(value.status as string) &&
1126
- nullableString(value.presenterId) &&
1127
- nullableNumber(value.presentationClaimExpiresAt) &&
1128
- nullableString(value.presentationSessionEntryId) &&
1394
+ ["pending", "settled", "cancelled"].includes(value.status as string) &&
1395
+ typeof value.unproductiveTurnEnds === "number" &&
1129
1396
  nullableString(value.acceptedSubmissionId) &&
1130
1397
  typeof value.createdAt === "number" &&
1131
1398
  typeof value.updatedAt === "number" &&