@osolmaz/pi-workflows 0.16.4 → 0.16.6

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 (173) hide show
  1. package/README.md +5 -5
  2. package/dist/builtins/autoimplement.workflow.d.ts +5 -1
  3. package/dist/builtins/autoimplement.workflow.js +29 -10
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/client/client.js +3 -1
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/protocol.d.ts +1 -1
  8. package/dist/client/protocol.js +18 -1
  9. package/dist/client/protocol.js.map +1 -1
  10. package/dist/client/view.d.ts +7 -5
  11. package/dist/extension/index.d.ts +2 -1
  12. package/dist/extension/index.js +115 -165
  13. package/dist/extension/index.js.map +1 -1
  14. package/dist/extension/recorder.d.ts +1 -1
  15. package/dist/extension/recorder.js +2 -2
  16. package/dist/extension/recorder.js.map +1 -1
  17. package/dist/extension/step-message.d.ts +1 -1
  18. package/dist/extension/step-message.js +4 -4
  19. package/dist/extension/step-message.js.map +1 -1
  20. package/dist/extension/workflow-message-coordinator.d.ts +12 -1
  21. package/dist/extension/workflow-message-coordinator.js +65 -54
  22. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  23. package/dist/resource-managers/index.d.ts +1 -2
  24. package/dist/resource-managers/index.js +1 -2
  25. package/dist/resource-managers/index.js.map +1 -1
  26. package/dist/resource-managers/sqlite.d.ts +2 -295
  27. package/dist/resource-managers/sqlite.js +71 -1528
  28. package/dist/resource-managers/sqlite.js.map +1 -1
  29. package/dist/resource-managers/types.d.ts +2 -7
  30. package/dist/server/resource-runner-entry.js +2 -6
  31. package/dist/server/resource-runner-entry.js.map +1 -1
  32. package/dist/server/rpc-bridge.js +1 -1
  33. package/dist/server/rpc-bridge.js.map +1 -1
  34. package/dist/server/rpc-executor.js +2 -4
  35. package/dist/server/rpc-executor.js.map +1 -1
  36. package/dist/server/server-entry.js +3 -0
  37. package/dist/server/server-entry.js.map +1 -1
  38. package/dist/server/server.d.ts +22 -8
  39. package/dist/server/server.js +498 -543
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/state.d.ts +16 -29
  42. package/dist/server/state.js +105 -319
  43. package/dist/server/state.js.map +1 -1
  44. package/dist/server/view.d.ts +4 -5
  45. package/dist/server/view.js +51 -51
  46. package/dist/server/view.js.map +1 -1
  47. package/dist/server/workflow-runner-entry.d.ts +1 -1
  48. package/dist/server/workflow-runner-entry.js +1 -49
  49. package/dist/server/workflow-runner-entry.js.map +1 -1
  50. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  51. package/dist/server/workflow-runner-protocol.js +5 -10
  52. package/dist/server/workflow-runner-protocol.js.map +1 -1
  53. package/dist/server/workflow-runner-store.d.ts +5 -6
  54. package/dist/server/workflow-runner-store.js +7 -14
  55. package/dist/server/workflow-runner-store.js.map +1 -1
  56. package/dist/state/attempt-time.d.ts +21 -0
  57. package/dist/state/attempt-time.js +95 -0
  58. package/dist/state/attempt-time.js.map +1 -0
  59. package/dist/state/json.d.ts +2 -1
  60. package/dist/state/json.js.map +1 -1
  61. package/dist/state/project-store.d.ts +39 -0
  62. package/dist/state/project-store.js +154 -0
  63. package/dist/state/project-store.js.map +1 -0
  64. package/dist/state/prune.d.ts +28 -0
  65. package/dist/state/prune.js +278 -83
  66. package/dist/state/prune.js.map +1 -1
  67. package/dist/state/schema.js +23 -23
  68. package/dist/state/workflow-messages.d.ts +1 -1
  69. package/dist/state/workflow-messages.js +3 -1
  70. package/dist/state/workflow-messages.js.map +1 -1
  71. package/dist/viewer/backup.js +5 -0
  72. package/dist/viewer/backup.js.map +1 -1
  73. package/dist/workflows/diagnostics.d.ts +8 -0
  74. package/dist/workflows/diagnostics.js +61 -0
  75. package/dist/workflows/diagnostics.js.map +1 -0
  76. package/dist/workflows/engine.d.ts +2 -15
  77. package/dist/workflows/engine.js +125 -245
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +3 -7
  80. package/dist/workflows/human-decision.js +2 -56
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +2 -1
  83. package/dist/workflows/index.js +1 -0
  84. package/dist/workflows/index.js.map +1 -1
  85. package/dist/workflows/queue.d.ts +267 -0
  86. package/dist/workflows/queue.js +1248 -0
  87. package/dist/workflows/queue.js.map +1 -0
  88. package/dist/workflows/requests.d.ts +47 -0
  89. package/dist/workflows/requests.js +158 -0
  90. package/dist/workflows/requests.js.map +1 -0
  91. package/dist/workflows/schema.js +2 -4
  92. package/dist/workflows/schema.js.map +1 -1
  93. package/dist/workflows/store.d.ts +11 -16
  94. package/dist/workflows/store.js +174 -169
  95. package/dist/workflows/store.js.map +1 -1
  96. package/dist/workflows/tool-input.d.ts +6 -9
  97. package/dist/workflows/tool-input.js +15 -12
  98. package/dist/workflows/tool-input.js.map +1 -1
  99. package/dist/workflows/transitions.d.ts +38 -0
  100. package/dist/workflows/transitions.js +276 -0
  101. package/dist/workflows/transitions.js.map +1 -0
  102. package/dist/workflows/types.d.ts +9 -42
  103. package/dist/workflows/workflow-message-content.d.ts +7 -1
  104. package/dist/workflows/workflow-message-content.js +22 -4
  105. package/dist/workflows/workflow-message-content.js.map +1 -1
  106. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  107. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  108. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  109. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  110. package/docs/DEFERRED_TURNS.md +84 -105
  111. package/docs/HUMAN_DECISIONS.md +17 -19
  112. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  113. package/docs/SQLITE_STATE.md +87 -30
  114. package/docs/WORKFLOW_SERVER.md +50 -11
  115. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  116. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  117. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  118. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  119. package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
  120. package/docs/workflows.md +109 -44
  121. package/examples/workflows/autoresearch.workflow.ts +0 -2
  122. package/examples/workflows/branch.workflow.ts +0 -4
  123. package/examples/workflows/echo.workflow.ts +0 -2
  124. package/examples/workflows/live-settings.workflow.ts +0 -2
  125. package/examples/workflows/two-turn.workflow.ts +0 -2
  126. package/herdr-plugin.toml +1 -1
  127. package/package.json +1 -1
  128. package/protocol/fixtures/client-v1.json +1 -1
  129. package/skills/pi-workflows/SKILL.md +8 -7
  130. package/src/builtins/autoimplement.workflow.ts +31 -10
  131. package/src/client/client.ts +3 -1
  132. package/src/client/protocol.ts +21 -1
  133. package/src/client/view.ts +9 -5
  134. package/src/extension/index.ts +117 -189
  135. package/src/extension/recorder.ts +2 -2
  136. package/src/extension/step-message.ts +4 -4
  137. package/src/extension/workflow-message-coordinator.ts +78 -50
  138. package/src/resource-managers/index.ts +1 -9
  139. package/src/resource-managers/sqlite.ts +330 -2655
  140. package/src/resource-managers/types.ts +2 -3
  141. package/src/server/resource-runner-entry.ts +2 -7
  142. package/src/server/rpc-bridge.ts +1 -1
  143. package/src/server/rpc-executor.ts +4 -9
  144. package/src/server/server-entry.ts +3 -0
  145. package/src/server/server.ts +568 -644
  146. package/src/server/state.ts +122 -466
  147. package/src/server/view.ts +56 -60
  148. package/src/server/workflow-runner-entry.ts +2 -55
  149. package/src/server/workflow-runner-protocol.ts +5 -15
  150. package/src/server/workflow-runner-store.ts +12 -27
  151. package/src/state/attempt-time.ts +122 -0
  152. package/src/state/json.ts +2 -1
  153. package/src/state/project-store.ts +218 -0
  154. package/src/state/prune.ts +408 -95
  155. package/src/state/schema.ts +23 -23
  156. package/src/state/workflow-messages.ts +4 -2
  157. package/src/viewer/backup.ts +7 -0
  158. package/src/workflows/diagnostics.ts +78 -0
  159. package/src/workflows/engine.ts +133 -291
  160. package/src/workflows/human-decision.ts +3 -81
  161. package/src/workflows/index.ts +8 -2
  162. package/src/workflows/queue.ts +2091 -0
  163. package/src/workflows/requests.ts +283 -0
  164. package/src/workflows/schema.ts +2 -6
  165. package/src/workflows/store.ts +223 -231
  166. package/src/workflows/tool-input.ts +20 -12
  167. package/src/workflows/transitions.ts +330 -0
  168. package/src/workflows/types.ts +9 -46
  169. package/src/workflows/workflow-message-content.ts +28 -4
  170. package/dist/resource-managers/runtime.d.ts +0 -59
  171. package/dist/resource-managers/runtime.js +0 -423
  172. package/dist/resource-managers/runtime.js.map +0 -1
  173. package/src/resource-managers/runtime.ts +0 -576
@@ -115,11 +115,9 @@ CREATE TABLE runs (
115
115
  project_id TEXT REFERENCES projects(project_id),
116
116
  parent_run_id TEXT REFERENCES runs(run_id),
117
117
  root_run_id TEXT NOT NULL REFERENCES runs(run_id),
118
- lineage_kind TEXT CHECK (lineage_kind IS NULL OR lineage_kind IN ('continuation', 'restart')),
118
+ lineage_kind TEXT CHECK (lineage_kind IS NULL OR lineage_kind IN ('restart')),
119
119
  restart_number INTEGER NOT NULL DEFAULT 0 CHECK (restart_number >= 0),
120
- parent_terminal_fingerprint BLOB CHECK (
121
- parent_terminal_fingerprint IS NULL OR length(parent_terminal_fingerprint) = 32
122
- ),
120
+ parent_run_revision INTEGER CHECK (parent_run_revision IS NULL OR parent_run_revision >= 0),
123
121
  definition_digest BLOB NOT NULL REFERENCES workflow_definitions(definition_digest),
124
122
  workflow_ref TEXT NOT NULL,
125
123
  launch_options_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
@@ -132,15 +130,14 @@ CREATE TABLE runs (
132
130
  input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
133
131
  final_output_hash BLOB REFERENCES blobs(blob_hash),
134
132
  error_hash BLOB REFERENCES blobs(blob_hash),
135
- presentation_prompt_hash BLOB REFERENCES blobs(blob_hash),
136
133
  created_at INTEGER NOT NULL,
137
134
  updated_at INTEGER NOT NULL,
138
135
  finished_at INTEGER,
139
- CHECK ((status IN ('waiting', 'completed', 'failed', 'timed_out', 'cancelled')) = (finished_at IS NOT NULL)),
136
+ CHECK ((status IN ('completed', 'failed', 'timed_out', 'cancelled')) = (finished_at IS NOT NULL)),
140
137
  CHECK ((parent_run_id IS NULL) = (lineage_kind IS NULL)),
141
138
  CHECK (
142
- (lineage_kind = 'restart' AND parent_terminal_fingerprint IS NOT NULL) OR
143
- (lineage_kind IS NOT 'restart' AND parent_terminal_fingerprint IS NULL)
139
+ (lineage_kind = 'restart' AND parent_run_revision IS NOT NULL) OR
140
+ (lineage_kind IS NOT 'restart' AND parent_run_revision IS NULL)
144
141
  ),
145
142
  CHECK (parent_run_id IS NOT NULL OR restart_number = 0)
146
143
  ) STRICT;
@@ -278,8 +275,7 @@ CREATE TABLE run_queue (
278
275
  'queued', 'starting', 'running', 'parked', 'done', 'failed', 'cancelled'
279
276
  )),
280
277
  available_at INTEGER NOT NULL,
281
- affinity_runner_id TEXT,
282
- origin_session_id TEXT NOT NULL,
278
+ origin_session_id TEXT,
283
279
  consecutive_errors INTEGER NOT NULL DEFAULT 0 CHECK (consecutive_errors >= 0),
284
280
  error_code TEXT,
285
281
  error_hash BLOB REFERENCES blobs(blob_hash),
@@ -291,7 +287,7 @@ CREATE TABLE run_queue (
291
287
 
292
288
  CREATE INDEX run_queue_claim_idx ON run_queue(status, available_at, created_at);
293
289
  CREATE UNIQUE INDEX run_queue_active_session_idx ON run_queue(origin_session_id)
294
- WHERE status IN ('queued', 'starting', 'running');
290
+ WHERE origin_session_id IS NOT NULL AND status IN ('queued', 'starting', 'running', 'parked');
295
291
 
296
292
  CREATE TABLE host_commands (
297
293
  request_id TEXT PRIMARY KEY,
@@ -377,7 +373,7 @@ CREATE TABLE node_attempts (
377
373
  settings_change_number INTEGER CHECK (settings_change_number IS NULL OR settings_change_number >= 0),
378
374
  settings_hash BLOB REFERENCES blobs(blob_hash),
379
375
  started_at INTEGER,
380
- deadline_at INTEGER,
376
+ timeout_ms REAL CHECK (timeout_ms IS NULL OR timeout_ms >= 0),
381
377
  finished_at INTEGER,
382
378
  created_at INTEGER NOT NULL,
383
379
  updated_at INTEGER NOT NULL,
@@ -387,13 +383,23 @@ CREATE TABLE node_attempts (
387
383
  CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
388
384
  WHERE status IN ('pending', 'running', 'waiting');
389
385
  CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
390
- CREATE INDEX node_attempts_deadline_idx ON node_attempts(deadline_at) WHERE deadline_at IS NOT NULL;
386
+ -- NULL means not configured; zero is an explicitly unlimited timeout.
387
+ CREATE TABLE attempt_active_intervals (
388
+ attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
389
+ interval_number INTEGER NOT NULL CHECK (interval_number > 0),
390
+ started_at INTEGER NOT NULL,
391
+ observed_at INTEGER NOT NULL,
392
+ ended_at INTEGER,
393
+ elapsed_ms REAL NOT NULL DEFAULT 0 CHECK (elapsed_ms >= 0),
394
+ PRIMARY KEY (attempt_id, interval_number)
395
+ ) STRICT;
396
+ CREATE UNIQUE INDEX attempt_active_intervals_open_idx ON attempt_active_intervals(attempt_id)
397
+ WHERE ended_at IS NULL;
391
398
 
392
399
  CREATE TABLE run_steps (
393
400
  run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
394
401
  step_index INTEGER NOT NULL CHECK (step_index >= 0),
395
402
  attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id),
396
- output_override_hash BLOB REFERENCES blobs(blob_hash),
397
403
  PRIMARY KEY (run_id, step_index),
398
404
  UNIQUE (run_id, attempt_id)
399
405
  ) STRICT;
@@ -504,11 +510,10 @@ CREATE TABLE interactive_requests (
504
510
  run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
505
511
  attempt_id TEXT NOT NULL UNIQUE REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
506
512
  target_session_id TEXT NOT NULL,
507
- kind TEXT NOT NULL CHECK (kind IN ('agent', 'assistant', 'decision')),
513
+ kind TEXT NOT NULL CHECK (kind IN ('agent', 'assistant', 'checkpoint', 'decision')),
508
514
  contract_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
509
515
  revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
510
516
  status TEXT NOT NULL CHECK (status IN ('pending', 'settled', 'cancelled')),
511
- unproductive_turn_ends INTEGER NOT NULL DEFAULT 0 CHECK (unproductive_turn_ends >= 0),
512
517
  accepted_submission_id TEXT,
513
518
  created_at INTEGER NOT NULL,
514
519
  updated_at INTEGER NOT NULL,
@@ -520,6 +525,8 @@ CREATE TABLE interactive_requests (
520
525
 
521
526
  CREATE INDEX interactive_requests_session_idx
522
527
  ON interactive_requests(target_session_id, status, created_at);
528
+ CREATE UNIQUE INDEX interactive_requests_pending_run_idx
529
+ ON interactive_requests(run_id) WHERE status = 'pending';
523
530
 
524
531
  CREATE TABLE workflow_messages (
525
532
  workflow_message_id TEXT PRIMARY KEY,
@@ -615,13 +622,6 @@ CREATE TABLE human_decision_submissions (
615
622
  PRIMARY KEY (decision_id, attempt_id)
616
623
  ) STRICT;
617
624
 
618
- CREATE TABLE continuations (
619
- decision_id TEXT PRIMARY KEY REFERENCES human_decisions(decision_id) ON DELETE CASCADE,
620
- parent_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
621
- continuation_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
622
- created_at INTEGER NOT NULL
623
- ) STRICT;
624
-
625
625
  CREATE TABLE controller_resources (
626
626
  controller_resource_id TEXT PRIMARY KEY,
627
627
  resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id) ON DELETE CASCADE,
@@ -8,7 +8,7 @@ export const WORKFLOW_TURN_SCHEMA = "pi-workflows.workflow-turn.v1" as const;
8
8
 
9
9
  export type WorkflowMessageKind = "step" | "decision" | "notification" | "terminal" | "followUp";
10
10
  export type WorkflowMessageStatus = "pending" | "sent" | "cancelled";
11
- export type WorkflowStepReason = "initial" | "reminder" | "resumed";
11
+ export type WorkflowStepReason = "initial" | "resumed";
12
12
  export type WorkflowTurnState = "started" | "ended";
13
13
  export type WorkflowTurnStopReason = "completed" | "aborted" | "error" | "lost";
14
14
 
@@ -278,6 +278,8 @@ export class WorkflowMessageStore {
278
278
  if (message.runId !== options.runId || message.targetSessionId !== options.targetSessionId) {
279
279
  throw new Error("Workflow turn does not match its message");
280
280
  }
281
+ if (!message.content.triggerTurn)
282
+ throw new Error("Workflow message does not request a model turn");
281
283
  if (message.status !== "sent") throw new Error("Workflow turn requires a sent message");
282
284
  const open = this.openTurnForMessage(options.workflowMessageId);
283
285
  if (open !== undefined) {
@@ -496,7 +498,7 @@ export function isWorkflowMessageContent(value: unknown): value is WorkflowMessa
496
498
 
497
499
  function validateContent(content: WorkflowMessageContent, kind: WorkflowMessageKind): void {
498
500
  if (!isWorkflowMessageContent(content)) throw new Error("Workflow message content is invalid");
499
- const shouldTrigger = kind === "step" || kind === "terminal" || kind === "followUp";
501
+ const shouldTrigger = kind === "step" || kind === "followUp";
500
502
  if (content.triggerTurn !== shouldTrigger) {
501
503
  throw new Error(`Workflow message kind ${kind} has invalid turn behavior`);
502
504
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { StateDatabase, workflowStatePath } from "../state/database.js";
4
+ import { workflowStateViolations } from "../workflows/diagnostics.js";
4
5
 
5
6
  /** Verify only an explicit inactive backup. Active state is verified through WorkflowClient. */
6
7
  export function verifyInactiveBackup(
@@ -15,6 +16,12 @@ export function verifyInactiveBackup(
15
16
  const state = new StateDatabase({ filePath: resolved, mode: "read-only" });
16
17
  try {
17
18
  state.integrityCheck();
19
+ const violations = workflowStateViolations(state);
20
+ if (violations.length > 0) {
21
+ throw new Error(
22
+ violations.map((item) => `${item.code} ${item.runId}: ${item.detail}`).join("\n"),
23
+ );
24
+ }
18
25
  } finally {
19
26
  state.close();
20
27
  }
@@ -0,0 +1,78 @@
1
+ import type { StateDatabase } from "../state/database.js";
2
+
3
+ export type WorkflowStateViolation = {
4
+ code:
5
+ | "queueState"
6
+ | "waitingRequest"
7
+ | "pendingRequest"
8
+ | "acceptedRequest"
9
+ | "activeClock"
10
+ | "sessionTurns";
11
+ runId: string;
12
+ detail: string;
13
+ };
14
+
15
+ /** Inspect domain facts without repairing or reinterpreting durable state. */
16
+ export function workflowStateViolations(state: StateDatabase): WorkflowStateViolation[] {
17
+ const checks: Array<{ code: WorkflowStateViolation["code"]; detail: string; sql: string }> = [
18
+ {
19
+ code: "queueState",
20
+ detail: "Execution and queue terminal states disagree.",
21
+ sql: `SELECT r.run_id AS runId FROM runs r JOIN run_queue q ON q.run_id = r.run_id
22
+ WHERE ((q.status IN ('done', 'failed', 'cancelled')) OR
23
+ (r.status IN ('completed', 'failed', 'timed_out', 'cancelled') AND NOT EXISTS (
24
+ SELECT 1 FROM effects e WHERE e.source_resource_id = r.resource_id
25
+ AND e.effect_type = 'run.settle_queue' AND e.status = 'pending')))
26
+ AND NOT ((r.status = 'completed' AND q.status = 'done')
27
+ OR (r.status IN ('failed', 'timed_out') AND q.status = 'failed')
28
+ OR (r.status = 'cancelled' AND q.status = 'cancelled'))`,
29
+ },
30
+ {
31
+ code: "waitingRequest",
32
+ detail: "A waiting run has no pending request or unconsumed accepted response.",
33
+ sql: `SELECT r.run_id AS runId FROM runs r WHERE r.status = 'waiting' AND NOT EXISTS (
34
+ SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id
35
+ AND (i.status = 'pending' OR (i.status = 'settled' AND i.consumed_at IS NULL)))`,
36
+ },
37
+ {
38
+ code: "pendingRequest",
39
+ detail: "A pending request has no matching active attempt and live run.",
40
+ sql: `SELECT i.run_id AS runId FROM interactive_requests i
41
+ JOIN runs r ON r.run_id = i.run_id JOIN node_attempts a ON a.attempt_id = i.attempt_id
42
+ WHERE i.status = 'pending' AND (a.run_id <> i.run_id
43
+ OR a.status NOT IN ('pending', 'running', 'waiting', 'interrupted')
44
+ OR r.status NOT IN ('running', 'waiting'))`,
45
+ },
46
+ {
47
+ code: "acceptedRequest",
48
+ detail: "A settled request has no matching accepted submission.",
49
+ sql: `SELECT i.run_id AS runId FROM interactive_requests i
50
+ LEFT JOIN interactive_submissions s ON s.submission_id = i.accepted_submission_id
51
+ WHERE i.status = 'settled' AND (s.submission_id IS NULL OR s.request_id <> i.request_id OR s.outcome <> 'accepted')`,
52
+ },
53
+ {
54
+ code: "activeClock",
55
+ detail: "An active interval belongs to paused, terminal, or unconfigured work.",
56
+ sql: `SELECT DISTINCT a.run_id AS runId FROM attempt_active_intervals t
57
+ JOIN node_attempts a ON a.attempt_id = t.attempt_id JOIN runs r ON r.run_id = a.run_id
58
+ WHERE t.ended_at IS NULL AND (r.paused = 1 OR r.status NOT IN ('running', 'waiting')
59
+ OR a.status NOT IN ('pending', 'running', 'waiting') OR a.timeout_ms IS NULL)`,
60
+ },
61
+ {
62
+ code: "sessionTurns",
63
+ detail: "A Pi session has more than one open workflow turn.",
64
+ sql: `SELECT DISTINCT t.run_id AS runId FROM workflow_turns t WHERE t.state = 'started'
65
+ AND t.target_session_id IN (SELECT target_session_id FROM workflow_turns
66
+ WHERE state = 'started' GROUP BY target_session_id HAVING COUNT(*) > 1)`,
67
+ },
68
+ ];
69
+ return state.readTransaction(() =>
70
+ checks.flatMap(({ code, detail, sql }) =>
71
+ (state.connection.prepare(sql).all() as { runId: string }[]).map(({ runId }) => ({
72
+ code,
73
+ runId,
74
+ detail,
75
+ })),
76
+ ),
77
+ );
78
+ }