@osolmaz/pi-workflows 0.16.0 → 0.16.2

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 (175) hide show
  1. package/README.md +18 -2
  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/client.d.ts +1 -0
  15. package/dist/client/client.js +5 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/protocol.d.ts +1 -1
  19. package/dist/client/protocol.js +11 -5
  20. package/dist/client/protocol.js.map +1 -1
  21. package/dist/client/view.d.ts +48 -16
  22. package/dist/client/view.js +1 -0
  23. package/dist/client/view.js.map +1 -1
  24. package/dist/controllers/index.d.ts +1 -1
  25. package/dist/controllers/index.js.map +1 -1
  26. package/dist/controllers/sqlite.d.ts +20 -165
  27. package/dist/controllers/sqlite.js +178 -436
  28. package/dist/controllers/sqlite.js.map +1 -1
  29. package/dist/extension/index.d.ts +23 -2
  30. package/dist/extension/index.js +637 -534
  31. package/dist/extension/index.js.map +1 -1
  32. package/dist/extension/recorder.d.ts +95 -0
  33. package/dist/extension/recorder.js +530 -0
  34. package/dist/extension/recorder.js.map +1 -0
  35. package/dist/extension/remote-recorder-store.d.ts +25 -0
  36. package/dist/extension/remote-recorder-store.js +81 -0
  37. package/dist/extension/remote-recorder-store.js.map +1 -0
  38. package/dist/extension/session-events.d.ts +134 -0
  39. package/dist/extension/session-events.js +60 -0
  40. package/dist/extension/session-events.js.map +1 -0
  41. package/dist/extension/session-view.d.ts +2 -0
  42. package/dist/extension/session-view.js +60 -2
  43. package/dist/extension/session-view.js.map +1 -1
  44. package/dist/extension/step-message.d.ts +6 -4
  45. package/dist/extension/step-message.js +12 -6
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.d.ts +2 -2
  48. package/dist/extension/widget.js +28 -18
  49. package/dist/extension/widget.js.map +1 -1
  50. package/dist/extension/workflow-message-coordinator.d.ts +28 -0
  51. package/dist/extension/workflow-message-coordinator.js +346 -0
  52. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  53. package/dist/host/channel-effects.d.ts +72 -0
  54. package/dist/host/channel-effects.js +271 -0
  55. package/dist/host/channel-effects.js.map +1 -0
  56. package/dist/host/channel-supervisor.d.ts +21 -0
  57. package/dist/host/channel-supervisor.js +54 -0
  58. package/dist/host/channel-supervisor.js.map +1 -0
  59. package/dist/host/runner.d.ts +50 -8
  60. package/dist/host/runner.js +1632 -341
  61. package/dist/host/runner.js.map +1 -1
  62. package/dist/host/state.d.ts +19 -16
  63. package/dist/host/state.js +251 -63
  64. package/dist/host/state.js.map +1 -1
  65. package/dist/host/view.d.ts +19 -9
  66. package/dist/host/view.js +227 -119
  67. package/dist/host/view.js.map +1 -1
  68. package/dist/host/worker-entry.d.ts +4 -1
  69. package/dist/host/worker-entry.js +23 -24
  70. package/dist/host/worker-entry.js.map +1 -1
  71. package/dist/host/worker-protocol.d.ts +15 -0
  72. package/dist/host/worker-protocol.js.map +1 -1
  73. package/dist/state/prune.js +3 -25
  74. package/dist/state/prune.js.map +1 -1
  75. package/dist/state/schema.js +76 -97
  76. package/dist/state/schema.js.map +1 -1
  77. package/dist/state/workflow-messages.d.ts +98 -0
  78. package/dist/state/workflow-messages.js +366 -0
  79. package/dist/state/workflow-messages.js.map +1 -0
  80. package/dist/viewer/render.js +1 -1
  81. package/dist/viewer/render.js.map +1 -1
  82. package/dist/viewer/tui.d.ts +1 -1
  83. package/dist/viewer/tui.js +65 -8
  84. package/dist/viewer/tui.js.map +1 -1
  85. package/dist/workflows/composition.js +0 -4
  86. package/dist/workflows/composition.js.map +1 -1
  87. package/dist/workflows/definition.js +0 -8
  88. package/dist/workflows/definition.js.map +1 -1
  89. package/dist/workflows/engine.js +4 -5
  90. package/dist/workflows/engine.js.map +1 -1
  91. package/dist/workflows/human-decision.d.ts +2 -0
  92. package/dist/workflows/human-decision.js +8 -2
  93. package/dist/workflows/human-decision.js.map +1 -1
  94. package/dist/workflows/index.d.ts +1 -1
  95. package/dist/workflows/index.js.map +1 -1
  96. package/dist/workflows/schema.js +0 -4
  97. package/dist/workflows/schema.js.map +1 -1
  98. package/dist/workflows/settings.d.ts +1 -8
  99. package/dist/workflows/settings.js.map +1 -1
  100. package/dist/workflows/store.d.ts +20 -19
  101. package/dist/workflows/store.js +163 -354
  102. package/dist/workflows/store.js.map +1 -1
  103. package/dist/workflows/tool-input.d.ts +21 -0
  104. package/dist/workflows/tool-input.js +23 -1
  105. package/dist/workflows/tool-input.js.map +1 -1
  106. package/dist/workflows/types.d.ts +6 -8
  107. package/dist/workflows/workflow-message-content.d.ts +38 -0
  108. package/dist/workflows/workflow-message-content.js +157 -0
  109. package/dist/workflows/workflow-message-content.js.map +1 -0
  110. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  111. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  112. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  113. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  114. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  115. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  116. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  117. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  118. package/docs/2026-09-04-workflow-run-state-plan.md +363 -0
  119. package/docs/DEFERRED_TURNS.md +66 -294
  120. package/docs/HUMAN_DECISIONS.md +29 -30
  121. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  122. package/docs/SQLITE_STATE.md +25 -17
  123. package/docs/WORKFLOW_HOST.md +105 -31
  124. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  125. package/docs/live-replay-protocol.md +8 -4
  126. package/docs/tui-viewer.md +3 -1
  127. package/docs/workflows.md +44 -28
  128. package/herdr-plugin.toml +1 -1
  129. package/package.json +1 -1
  130. package/plugins/herdr/viewer.mjs +14 -5
  131. package/src/channels/adapter-entry.ts +220 -0
  132. package/src/channels/config.ts +296 -0
  133. package/src/channels/protocol.ts +333 -0
  134. package/src/channels/telegram.ts +335 -0
  135. package/src/client/client.ts +6 -2
  136. package/src/client/index.ts +2 -1
  137. package/src/client/protocol.ts +11 -5
  138. package/src/client/view.ts +51 -18
  139. package/src/controllers/index.ts +0 -1
  140. package/src/controllers/sqlite.ts +247 -845
  141. package/src/extension/index.ts +712 -647
  142. package/src/extension/recorder.ts +687 -0
  143. package/src/extension/remote-recorder-store.ts +126 -0
  144. package/src/extension/session-events.ts +119 -0
  145. package/src/extension/session-view.ts +86 -3
  146. package/src/extension/step-message.ts +19 -9
  147. package/src/extension/widget.ts +37 -17
  148. package/src/extension/workflow-message-coordinator.ts +398 -0
  149. package/src/host/channel-effects.ts +465 -0
  150. package/src/host/channel-supervisor.ts +73 -0
  151. package/src/host/runner.ts +2163 -525
  152. package/src/host/state.ts +333 -94
  153. package/src/host/view.ts +254 -122
  154. package/src/host/worker-entry.ts +38 -30
  155. package/src/host/worker-protocol.ts +11 -0
  156. package/src/state/prune.ts +3 -31
  157. package/src/state/schema.ts +76 -97
  158. package/src/state/workflow-messages.ts +599 -0
  159. package/src/viewer/render.ts +1 -5
  160. package/src/viewer/tui.ts +71 -6
  161. package/src/workflows/composition.ts +0 -5
  162. package/src/workflows/definition.ts +0 -8
  163. package/src/workflows/engine.ts +4 -6
  164. package/src/workflows/human-decision.ts +18 -2
  165. package/src/workflows/index.ts +0 -1
  166. package/src/workflows/schema.ts +0 -6
  167. package/src/workflows/settings.ts +1 -20
  168. package/src/workflows/store.ts +237 -489
  169. package/src/workflows/tool-input.ts +36 -1
  170. package/src/workflows/types.ts +6 -8
  171. package/src/workflows/workflow-message-content.ts +197 -0
  172. package/dist/extension/decision-channels.d.ts +0 -134
  173. package/dist/extension/decision-channels.js +0 -1307
  174. package/dist/extension/decision-channels.js.map +0 -1
  175. package/src/extension/decision-channels.ts +0 -1826
@@ -1,6 +1,7 @@
1
1
  import { type ClientRequest, type ClientResponse } from "../client/protocol.js";
2
2
  import { StateDatabase } from "../state/database.js";
3
3
  import { type JsonValue } from "../state/json.js";
4
+ import { WorkflowMessageStore, type WorkflowStepReason } from "../state/workflow-messages.js";
4
5
  import type { WorkerMessage, WorkerResponse } from "./worker-protocol.js";
5
6
  export type HostClaim = {
6
7
  hostId: string;
@@ -49,10 +50,8 @@ export type InteractiveRequestRecord = {
49
50
  kind: "agent" | "assistant" | "decision";
50
51
  contract: JsonValue;
51
52
  revision: number;
52
- status: "pending" | "presenting" | "settled" | "cancelled";
53
- presenterId: string | null;
54
- presentationClaimExpiresAt: string | null;
55
- presentationSessionEntryId: string | null;
53
+ status: "pending" | "settled" | "cancelled";
54
+ unproductiveTurnEnds: number;
56
55
  acceptedSubmissionId: string | null;
57
56
  createdAt: string;
58
57
  updatedAt: string;
@@ -62,6 +61,7 @@ export type InteractiveRequestRecord = {
62
61
  export declare class HostStateStore {
63
62
  readonly state: StateDatabase;
64
63
  private readonly ownsState;
64
+ readonly workflowMessages: WorkflowMessageStore;
65
65
  constructor(databasePath: string, options?: {
66
66
  state?: StateDatabase;
67
67
  readOnly?: boolean;
@@ -100,6 +100,14 @@ export declare class HostStateStore {
100
100
  kind: InteractiveRequestRecord["kind"];
101
101
  contract: JsonValue;
102
102
  }): InteractiveRequestRecord;
103
+ ensureInteractionMessage(request: InteractiveRequestRecord, reason: WorkflowStepReason, now?: number): import("../state/workflow-messages.js").WorkflowMessage;
104
+ resumePendingInteraction(runId: string, now?: number): InteractiveRequestRecord;
105
+ beginInteractionModelTurn(requestId: string, now?: number): void;
106
+ interactionPauseStartedAt(runId: string): number | undefined;
107
+ resumeInteractionModelTurn(runId: string, pausedAt: number | undefined, now?: number): void;
108
+ markSessionModelTurnsInactive(targetSessionId: string, now?: number): void;
109
+ recoverInactiveModelTurns(previousHeartbeatAt: number | undefined, now?: number): void;
110
+ resumeSessionModelTurns(targetSessionId: string, now?: number): void;
103
111
  getInteraction(requestId: string): InteractiveRequestRecord | undefined;
104
112
  acceptedInteraction(runId: string): {
105
113
  requestId: string;
@@ -116,7 +124,7 @@ export declare class HostStateStore {
116
124
  payload: JsonValue;
117
125
  } | undefined;
118
126
  validatingInteractionRunIds(): string[];
119
- expiredInteractionRunIds(now?: number): string[];
127
+ expiredInteractionRuns(now?: number): ExpiredInteractionRunRow[];
120
128
  timedOutInteraction(runId: string): TimedOutInteractionRow | undefined;
121
129
  interactionSubmission(requestId: string, submissionId: string): InteractiveSubmissionRecord | undefined;
122
130
  finishInteractionValidation(options: {
@@ -127,17 +135,7 @@ export declare class HostStateStore {
127
135
  }): InteractiveSubmissionRecord;
128
136
  consumeAcceptedInteraction(runId: string, attemptId: string, now?: number): boolean;
129
137
  listPendingInteractions(sessionId: string): InteractiveRequestRecord[];
130
- claimInteractionPresentation(options: {
131
- requestId: string;
132
- expectedRevision: number;
133
- presenterId: string;
134
- leaseMs: number;
135
- }): InteractiveRequestRecord;
136
- markInteractionPresented(options: {
137
- requestId: string;
138
- expectedRevision: number;
139
- sessionEntryId: string;
140
- }): InteractiveRequestRecord;
138
+ listPendingDecisionInteractions(): InteractiveRequestRecord[];
141
139
  submitInteraction(options: {
142
140
  requestId: string;
143
141
  submissionId: string;
@@ -169,6 +167,7 @@ export declare class HostStateStore {
169
167
  private hostRow;
170
168
  private commandResponse;
171
169
  private interactiveRequest;
170
+ private shiftInteractionTimeout;
172
171
  private requireInteractiveRequest;
173
172
  }
174
173
  type TimedOutInteractionRow = {
@@ -176,4 +175,8 @@ type TimedOutInteractionRow = {
176
175
  attemptId: string;
177
176
  nodeId: string;
178
177
  };
178
+ type ExpiredInteractionRunRow = {
179
+ runId: string;
180
+ targetSessionId: string;
181
+ };
179
182
  export {};
@@ -3,9 +3,12 @@ import { CLIENT_PROTOCOL_SCHEMA, clientRequestFingerprint, } from "../client/pro
3
3
  import { StateDatabase } from "../state/database.js";
4
4
  import { canonicalJson } from "../state/json.js";
5
5
  import { tokenHash } from "../state/mutation.js";
6
+ import { WorkflowMessageStore, workflowMessageIdFor, } from "../state/workflow-messages.js";
7
+ import { decisionWorkflowMessageContent, stepWorkflowMessageContent, } from "../workflows/workflow-message-content.js";
6
8
  export class HostStateStore {
7
9
  state;
8
10
  ownsState;
11
+ workflowMessages;
9
12
  constructor(databasePath, options = {}) {
10
13
  this.ownsState = options.state === undefined;
11
14
  this.state =
@@ -14,6 +17,7 @@ export class HostStateStore {
14
17
  filePath: databasePath,
15
18
  mode: options.readOnly === true ? "read-only" : "read-write",
16
19
  });
20
+ this.workflowMessages = new WorkflowMessageStore(this.state);
17
21
  }
18
22
  close() {
19
23
  if (this.ownsState)
@@ -242,14 +246,16 @@ export class HostStateStore {
242
246
  if (existing.status === "settled" && existing.consumedAt === null) {
243
247
  this.state.connection
244
248
  .prepare(`UPDATE interactive_requests
245
- SET status = 'pending', presenter_id = NULL,
246
- presentation_claim_expires_at = NULL, presentation_session_entry_id = NULL,
247
- accepted_submission_id = NULL, settled_at = NULL, consumed_at = NULL,
248
- revision = revision + 1, updated_at = ?
249
+ SET status = 'pending', accepted_submission_id = NULL,
250
+ settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
249
251
  WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`)
250
252
  .run(now, options.requestId);
251
- return this.requireInteractiveRequest(options.requestId);
253
+ const reopened = this.requireInteractiveRequest(options.requestId);
254
+ this.ensureInteractionMessage(reopened, "resumed", now);
255
+ return reopened;
252
256
  }
257
+ if (existing.status === "pending")
258
+ this.ensureInteractionMessage(existing, "initial", now);
253
259
  return existing;
254
260
  }
255
261
  const contractHash = this.state.putJson(options.contract, now);
@@ -259,9 +265,172 @@ export class HostStateStore {
259
265
  revision, status, created_at, updated_at
260
266
  ) VALUES (?, ?, ?, ?, ?, ?, 1, 'pending', ?, ?)`)
261
267
  .run(options.requestId, options.runId, options.attemptId, options.targetSessionId, options.kind, contractHash, now, now);
262
- return this.requireInteractiveRequest(options.requestId);
268
+ const created = this.requireInteractiveRequest(options.requestId);
269
+ this.ensureInteractionMessage(created, "initial", now);
270
+ return created;
263
271
  });
264
272
  }
273
+ ensureInteractionMessage(request, reason, now = Date.now()) {
274
+ const kind = request.kind === "decision" ? "decision" : "step";
275
+ const idempotencyKey = `${request.revision}:${reason}`;
276
+ const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
277
+ const content = kind === "decision"
278
+ ? decisionWorkflowMessageContent({
279
+ workflowMessageId,
280
+ requestId: request.requestId,
281
+ runId: request.runId,
282
+ contract: request.contract,
283
+ })
284
+ : stepWorkflowMessageContent({
285
+ workflowMessageId,
286
+ requestId: request.requestId,
287
+ contract: request.contract,
288
+ reason,
289
+ });
290
+ return this.workflowMessages.create({
291
+ workflowMessageId,
292
+ runId: request.runId,
293
+ targetSessionId: request.targetSessionId,
294
+ kind,
295
+ sourceId: request.requestId,
296
+ idempotencyKey,
297
+ content,
298
+ now,
299
+ });
300
+ }
301
+ resumePendingInteraction(runId, now = Date.now()) {
302
+ return this.state.transaction(() => {
303
+ const row = this.state.connection
304
+ .prepare(`SELECT request_id AS requestId FROM interactive_requests
305
+ WHERE run_id = ? AND status = 'pending' ORDER BY created_at LIMIT 1`)
306
+ .get(runId);
307
+ if (!isRequestIdRow(row))
308
+ throw new Error("Pending workflow interaction is missing");
309
+ const current = this.requireInteractiveRequest(row.requestId);
310
+ if (current.kind === "decision")
311
+ return current;
312
+ this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
313
+ const changed = this.state.connection
314
+ .prepare(`UPDATE interactive_requests SET revision = revision + 1, updated_at = ?
315
+ WHERE request_id = ? AND status = 'pending'`)
316
+ .run(now, row.requestId);
317
+ if (changed.changes !== 1)
318
+ throw new Error("Pending workflow interaction resume is stale");
319
+ const request = this.requireInteractiveRequest(row.requestId);
320
+ this.ensureInteractionMessage(request, "resumed", now);
321
+ return request;
322
+ });
323
+ }
324
+ beginInteractionModelTurn(requestId, now = Date.now()) {
325
+ const row = this.state.connection
326
+ .prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
327
+ a.started_at AS startedAt, a.deadline_at AS deadlineAt,
328
+ (SELECT MAX(t.ended_at)
329
+ FROM workflow_messages m
330
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
331
+ WHERE m.source_id = i.request_id AND m.kind = 'step'
332
+ AND t.state = 'ended') AS previousTurnEndedAt
333
+ FROM interactive_requests i
334
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
335
+ WHERE i.request_id = ? AND i.status = 'pending'`)
336
+ .get(requestId);
337
+ if (!isInteractionTimeoutRow(row)) {
338
+ throw new Error("Pending workflow interaction timeout is missing");
339
+ }
340
+ this.shiftInteractionTimeout(row, Math.max(0, now - (row.previousTurnEndedAt ?? row.startedAt ?? row.createdAt)), now);
341
+ }
342
+ interactionPauseStartedAt(runId) {
343
+ const row = this.state.connection
344
+ .prepare(`SELECT r.updated_at AS pausedAt
345
+ FROM runs r
346
+ JOIN interactive_requests i ON i.run_id = r.run_id AND i.status = 'pending'
347
+ WHERE r.run_id = ? AND r.paused = 1 LIMIT 1`)
348
+ .get(runId);
349
+ return isPausedAtRow(row) ? row.pausedAt : undefined;
350
+ }
351
+ resumeInteractionModelTurn(runId, pausedAt, now = Date.now()) {
352
+ if (pausedAt === undefined)
353
+ return;
354
+ const row = this.state.connection
355
+ .prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
356
+ a.started_at AS startedAt, a.deadline_at AS deadlineAt,
357
+ t.ended_at AS previousTurnEndedAt
358
+ FROM interactive_requests i
359
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
360
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
361
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
362
+ WHERE i.run_id = ? AND i.status = 'pending' AND t.started_at <= ?
363
+ AND (t.ended_at IS NULL OR t.ended_at > ?)
364
+ ORDER BY t.started_at DESC LIMIT 1`)
365
+ .get(runId, pausedAt, pausedAt);
366
+ if (!isInteractionTimeoutRow(row))
367
+ return;
368
+ const modelTurnStoppedAt = Math.min(now, row.previousTurnEndedAt ?? now);
369
+ this.shiftInteractionTimeout(row, Math.max(0, modelTurnStoppedAt - pausedAt), now);
370
+ }
371
+ markSessionModelTurnsInactive(targetSessionId, now = Date.now()) {
372
+ this.state.connection
373
+ .prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
374
+ WHERE status <> 'interrupted' AND attempt_id IN (
375
+ SELECT DISTINCT i.attempt_id
376
+ FROM interactive_requests i
377
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
378
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
379
+ AND t.state = 'started'
380
+ WHERE i.target_session_id = ? AND i.status = 'pending'
381
+ )`)
382
+ .run(now, targetSessionId);
383
+ }
384
+ recoverInactiveModelTurns(previousHeartbeatAt, now = Date.now()) {
385
+ const rows = this.state.connection
386
+ .prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
387
+ a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
388
+ MAX(t.started_at) AS turnStartedAt
389
+ FROM interactive_requests i
390
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
391
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
392
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
393
+ AND t.state = 'started'
394
+ WHERE i.status = 'pending'
395
+ GROUP BY i.attempt_id`)
396
+ .all()
397
+ .filter(isInactiveInteractionTimeoutRow);
398
+ for (const row of rows) {
399
+ const inactiveAt = row.status === "interrupted"
400
+ ? row.updatedAt
401
+ : previousHeartbeatAt === undefined
402
+ ? row.updatedAt
403
+ : Math.max(previousHeartbeatAt, row.turnStartedAt);
404
+ this.shiftInteractionTimeout(row, Math.max(0, now - inactiveAt), now);
405
+ this.state.connection
406
+ .prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
407
+ WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
408
+ .run(now, row.attemptId);
409
+ }
410
+ }
411
+ resumeSessionModelTurns(targetSessionId, now = Date.now()) {
412
+ const rows = this.state.connection
413
+ .prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
414
+ a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
415
+ MAX(t.started_at) AS turnStartedAt
416
+ FROM interactive_requests i
417
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
418
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
419
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
420
+ AND t.state = 'started'
421
+ WHERE i.target_session_id = ? AND i.status = 'pending'
422
+ AND a.status = 'interrupted'
423
+ GROUP BY i.attempt_id`)
424
+ .all(targetSessionId)
425
+ .filter(isInactiveInteractionTimeoutRow);
426
+ for (const row of rows) {
427
+ this.shiftInteractionTimeout(row, Math.max(0, now - row.updatedAt), now);
428
+ this.state.connection
429
+ .prepare(`UPDATE node_attempts SET status = 'waiting', updated_at = ?
430
+ WHERE attempt_id = ? AND status = 'interrupted'`)
431
+ .run(now, row.attemptId);
432
+ }
433
+ }
265
434
  getInteraction(requestId) {
266
435
  return this.interactiveRequest(requestId);
267
436
  }
@@ -292,7 +461,7 @@ export class HostStateStore {
292
461
  FROM interactive_requests i
293
462
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
294
463
  JOIN interactive_submissions s ON s.request_id = i.request_id
295
- WHERE i.run_id = ? AND i.status IN ('pending', 'presenting')
464
+ WHERE i.run_id = ? AND i.status = 'pending'
296
465
  AND s.outcome = 'validating'
297
466
  ORDER BY s.submitted_at DESC LIMIT 1`)
298
467
  .get(runId);
@@ -312,26 +481,32 @@ export class HostStateStore {
312
481
  FROM interactive_requests i
313
482
  JOIN interactive_submissions s ON s.request_id = i.request_id
314
483
  JOIN run_queue q ON q.run_id = i.run_id
315
- WHERE i.status IN ('pending', 'presenting') AND s.outcome = 'validating'
484
+ WHERE i.status = 'pending' AND s.outcome = 'validating'
316
485
  AND q.status NOT IN ('done', 'failed', 'cancelled')
317
486
  GROUP BY i.run_id
318
487
  ORDER BY MIN(s.submitted_at), i.run_id`)
319
488
  .all()
320
489
  .flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
321
490
  }
322
- expiredInteractionRunIds(now = Date.now()) {
491
+ expiredInteractionRuns(now = Date.now()) {
323
492
  return this.state.connection
324
- .prepare(`SELECT i.run_id AS runId
493
+ .prepare(`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
325
494
  FROM interactive_requests i
326
495
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
496
+ JOIN runs r ON r.run_id = i.run_id
327
497
  JOIN run_queue q ON q.run_id = i.run_id
328
- WHERE i.status IN ('pending', 'presenting')
498
+ WHERE i.status = 'pending' AND r.paused = 0
329
499
  AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
330
500
  AND q.status NOT IN ('done', 'failed', 'cancelled')
331
- GROUP BY i.run_id
501
+ AND EXISTS (
502
+ SELECT 1 FROM workflow_messages m
503
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
504
+ WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
505
+ )
506
+ GROUP BY i.run_id, i.target_session_id
332
507
  ORDER BY MIN(a.deadline_at), i.run_id`)
333
508
  .all(now)
334
- .flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
509
+ .filter(isExpiredInteractionRunRow);
335
510
  }
336
511
  timedOutInteraction(runId) {
337
512
  const row = this.state.connection
@@ -340,8 +515,13 @@ export class HostStateStore {
340
515
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
341
516
  JOIN runs r ON r.run_id = i.run_id
342
517
  WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
343
- AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
344
- AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
518
+ AND r.paused = 0 AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
519
+ AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
520
+ AND EXISTS (
521
+ SELECT 1 FROM workflow_messages m
522
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
523
+ WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
524
+ )
345
525
  ORDER BY a.deadline_at, i.request_id LIMIT 1`)
346
526
  .get(runId, Date.now());
347
527
  return isTimedOutInteractionRow(row) ? row : undefined;
@@ -384,13 +564,14 @@ export class HostStateStore {
384
564
  if (options.accepted) {
385
565
  const changed = this.state.connection
386
566
  .prepare(`UPDATE interactive_requests
387
- SET status = 'settled', presenter_id = NULL,
388
- presentation_claim_expires_at = NULL, accepted_submission_id = ?,
567
+ SET status = 'settled', accepted_submission_id = ?,
389
568
  revision = revision + 1, updated_at = ?, settled_at = ?
390
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')`)
569
+ WHERE request_id = ? AND revision = ? AND status = 'pending'`)
391
570
  .run(options.submissionId, now, now, options.requestId, request.revision);
392
571
  if (changed.changes !== 1)
393
572
  throw new Error("Interactive request validation is stale");
573
+ this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
574
+ this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
394
575
  }
395
576
  const settled = this.interactionSubmission(options.requestId, options.submissionId);
396
577
  if (settled === undefined)
@@ -409,40 +590,18 @@ export class HostStateStore {
409
590
  listPendingInteractions(sessionId) {
410
591
  return this.state.connection
411
592
  .prepare(`SELECT request_id AS requestId FROM interactive_requests
412
- WHERE target_session_id = ? AND status IN ('pending', 'presenting')
593
+ WHERE target_session_id = ? AND status = 'pending'
413
594
  ORDER BY created_at, request_id`)
414
595
  .all(sessionId)
415
596
  .flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
416
597
  }
417
- claimInteractionPresentation(options) {
418
- requireLeaseMs(options.leaseMs);
419
- const now = Date.now();
420
- const changed = this.state.connection
421
- .prepare(`UPDATE interactive_requests
422
- SET status = 'presenting', presenter_id = ?, presentation_claim_expires_at = ?,
423
- revision = revision + 1, updated_at = ?
424
- WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
425
- AND (
426
- status = 'pending'
427
- OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
428
- )`)
429
- .run(options.presenterId, now + options.leaseMs, now, options.requestId, options.expectedRevision, now);
430
- if (changed.changes !== 1)
431
- throw new Error("Interactive request presentation claim conflict");
432
- return this.requireInteractiveRequest(options.requestId);
433
- }
434
- markInteractionPresented(options) {
435
- const now = Date.now();
436
- const changed = this.state.connection
437
- .prepare(`UPDATE interactive_requests
438
- SET status = 'presenting', presenter_id = NULL, presentation_claim_expires_at = NULL,
439
- presentation_session_entry_id = ?, revision = revision + 1, updated_at = ?
440
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')
441
- AND (presentation_session_entry_id IS NULL OR presentation_session_entry_id = ?)`)
442
- .run(options.sessionEntryId, now, options.requestId, options.expectedRevision, options.sessionEntryId);
443
- if (changed.changes !== 1)
444
- throw new Error("Interactive request presentation conflict");
445
- return this.requireInteractiveRequest(options.requestId);
598
+ listPendingDecisionInteractions() {
599
+ return this.state.connection
600
+ .prepare(`SELECT request_id AS requestId FROM interactive_requests
601
+ WHERE kind = 'decision' AND status = 'pending'
602
+ ORDER BY created_at, request_id`)
603
+ .all()
604
+ .flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
446
605
  }
447
606
  submitInteraction(options) {
448
607
  return this.recordInteractionSubmission({
@@ -490,8 +649,7 @@ export class HostStateStore {
490
649
  }
491
650
  }
492
651
  const request = this.requireInteractiveRequest(options.requestId);
493
- if (request.revision !== options.expectedRevision ||
494
- !["pending", "presenting"].includes(request.status)) {
652
+ if (request.revision !== options.expectedRevision || request.status !== "pending") {
495
653
  throw new Error("Interactive request revision conflict");
496
654
  }
497
655
  const savedPayloadHash = this.state.putJson(options.payload, now);
@@ -505,11 +663,12 @@ export class HostStateStore {
505
663
  if (options.settle) {
506
664
  this.state.connection
507
665
  .prepare(`UPDATE interactive_requests
508
- SET status = 'settled', presenter_id = NULL, presentation_claim_expires_at = NULL,
509
- accepted_submission_id = ?, revision = revision + 1,
666
+ SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
510
667
  updated_at = ?, settled_at = ?
511
- WHERE request_id = ? AND revision = ? AND status IN ('pending', 'presenting')`)
668
+ WHERE request_id = ? AND revision = ? AND status = 'pending'`)
512
669
  .run(options.submissionId, now, now, options.requestId, options.expectedRevision);
670
+ this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
671
+ this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
513
672
  }
514
673
  return {
515
674
  interaction: this.requireInteractiveRequest(options.requestId),
@@ -552,9 +711,7 @@ export class HostStateStore {
552
711
  const row = this.state.connection
553
712
  .prepare(`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
554
713
  target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
555
- revision, status, presenter_id AS presenterId,
556
- presentation_claim_expires_at AS presentationClaimExpiresAt,
557
- presentation_session_entry_id AS presentationSessionEntryId,
714
+ revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
558
715
  accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
559
716
  updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
560
717
  FROM interactive_requests WHERE request_id = ?`)
@@ -570,9 +727,7 @@ export class HostStateStore {
570
727
  contract: this.state.readJson(row.contractHash),
571
728
  revision: row.revision,
572
729
  status: row.status,
573
- presenterId: row.presenterId,
574
- presentationClaimExpiresAt: iso(row.presentationClaimExpiresAt),
575
- presentationSessionEntryId: row.presentationSessionEntryId,
730
+ unproductiveTurnEnds: row.unproductiveTurnEnds,
576
731
  acceptedSubmissionId: row.acceptedSubmissionId,
577
732
  createdAt: new Date(row.createdAt).toISOString(),
578
733
  updatedAt: new Date(row.updatedAt).toISOString(),
@@ -580,6 +735,18 @@ export class HostStateStore {
580
735
  consumedAt: iso(row.consumedAt),
581
736
  };
582
737
  }
738
+ shiftInteractionTimeout(row, idleMs, now) {
739
+ if (idleMs === 0 || row.deadlineAt === null)
740
+ return;
741
+ const changed = this.state.connection
742
+ .prepare(`UPDATE node_attempts
743
+ SET started_at = CASE WHEN started_at IS NULL THEN NULL ELSE started_at + ? END,
744
+ deadline_at = deadline_at + ?, updated_at = ?
745
+ WHERE attempt_id = ? AND deadline_at = ?`)
746
+ .run(idleMs, idleMs, now, row.attemptId, row.deadlineAt);
747
+ if (changed.changes !== 1)
748
+ throw new Error("Workflow interaction timeout changed concurrently");
749
+ }
583
750
  requireInteractiveRequest(requestId) {
584
751
  const request = this.interactiveRequest(requestId);
585
752
  if (request === undefined)
@@ -655,6 +822,29 @@ function isTimedOutInteractionRow(value) {
655
822
  typeof value.attemptId === "string" &&
656
823
  typeof value.nodeId === "string");
657
824
  }
825
+ function isInteractionTimeoutRow(value) {
826
+ return (isRecord(value) &&
827
+ typeof value.attemptId === "string" &&
828
+ typeof value.createdAt === "number" &&
829
+ nullableNumber(value.startedAt) &&
830
+ nullableNumber(value.deadlineAt) &&
831
+ nullableNumber(value.previousTurnEndedAt));
832
+ }
833
+ function isInactiveInteractionTimeoutRow(value) {
834
+ return (isRecord(value) &&
835
+ typeof value.attemptId === "string" &&
836
+ ["pending", "running", "waiting", "interrupted"].includes(value.status) &&
837
+ nullableNumber(value.startedAt) &&
838
+ nullableNumber(value.deadlineAt) &&
839
+ typeof value.updatedAt === "number" &&
840
+ typeof value.turnStartedAt === "number");
841
+ }
842
+ function isExpiredInteractionRunRow(value) {
843
+ return (isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string");
844
+ }
845
+ function isPausedAtRow(value) {
846
+ return isRecord(value) && typeof value.pausedAt === "number";
847
+ }
658
848
  function isSubmissionRow(value) {
659
849
  return (isRecord(value) &&
660
850
  typeof value.submissionId === "string" &&
@@ -684,10 +874,8 @@ function isInteractiveRequestRow(value) {
684
874
  ["agent", "assistant", "decision"].includes(value.kind) &&
685
875
  Buffer.isBuffer(value.contractHash) &&
686
876
  typeof value.revision === "number" &&
687
- ["pending", "presenting", "settled", "cancelled"].includes(value.status) &&
688
- nullableString(value.presenterId) &&
689
- nullableNumber(value.presentationClaimExpiresAt) &&
690
- nullableString(value.presentationSessionEntryId) &&
877
+ ["pending", "settled", "cancelled"].includes(value.status) &&
878
+ typeof value.unproductiveTurnEnds === "number" &&
691
879
  nullableString(value.acceptedSubmissionId) &&
692
880
  typeof value.createdAt === "number" &&
693
881
  typeof value.updatedAt === "number" &&