@osolmaz/pi-workflows 0.15.2 → 0.15.3

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.
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Restore workflow session delivery and controls
3
+ author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
4
+ date: 2026-09-01
5
+ ---
6
+
7
+ # Restore workflow session delivery and controls
8
+
9
+ The out-of-process workflow host removed the Pi workflow widget and Escape-to-pause behavior. A later delivery safety fix also caused normal polling to report `Interactive request presentation claim conflict`. This plan restores those features and fixes delivery without bringing back the embedded workflow runtime.
10
+
11
+ [Workflow host](WORKFLOW_HOST.md) remains the process and state specification. [Workflow step messages](WORKFLOW_STEP_MESSAGES.md) remains the session message specification. This plan records the cause, scope, implementation order, and acceptance checks for the repair.
12
+
13
+ ## Observed problems
14
+
15
+ - The active workflow widget no longer appears in Pi.
16
+ - Escape aborts the current model turn but leaves its workflow unpaused.
17
+ - Polling can report a presentation claim conflict before the workflow step appears.
18
+ - A visible message can become eligible for another send if saving its durable receipt fails.
19
+
20
+ The affected live run remained durably parked at its interactive request. No workflow state or Pi session message was lost.
21
+
22
+ ## Root cause
23
+
24
+ The out-of-process redesign removed the embedded extension executor. The old widget and `agent_end` pause handler were coupled to that executor, so both were removed with it. The redesign did not add host-backed replacements.
25
+
26
+ The delivery coordinator checks Pi again after an asynchronous host claim. If Pi becomes busy during that claim, the coordinator currently drops the claim. The next poll tries to claim the same interaction before its ten-second presentation lease expires. The host correctly rejects that second claim. The extension incorrectly exposes the expected rejection as a workflow tool failure.
27
+
28
+ The coordinator also removes its local queued guard before durable settlement finishes. A settlement error can therefore make later polling treat a message that is already visible in Pi as sendable work.
29
+
30
+ ## Requirements
31
+
32
+ - Keep one global host as the normal workflow state writer.
33
+ - Keep workflow and controller code in supervised child processes.
34
+ - Use documented Pi extension APIs only.
35
+ - Preserve one ordered session delivery path for steps, decisions, notifications, and final results.
36
+ - Never send through an expired claim.
37
+ - Never resend a message that is visible in Pi only because its durable receipt failed.
38
+ - Pause a presented workflow interaction when its Pi model turn ends with stop reason `aborted`.
39
+ - Reject workflow updates and submissions while that interaction is paused.
40
+ - Restore the widget as a read-only view of durable host state.
41
+ - Keep schema identifiers at version 1 and add no compatibility path.
42
+
43
+ ## Delivery coordinator
44
+
45
+ The coordinator will have three in-memory states for one delivery:
46
+
47
+ 1. `claimed`: the host granted a lease, but Pi became busy before send;
48
+ 2. `queued`: `pi.sendMessage()` was called and the matching Pi entry is not yet durably settled;
49
+ 3. settled or ambiguous: the host accepted the public Pi entry ID, or settlement could not be proved.
50
+
51
+ The coordinator records `claimExpiresAt` with every claim. A later poll can use the same claim while it is live. Immediately before send, the extension revalidates that exact claim and durable resource through the host, then checks Pi and the lease again. It discards cancelled, paused, replaced, or expired work. It does not request another claim while a live claim is remembered.
52
+
53
+ The extension reads the presentation claim owner and expiry from the existing version-1 interaction row. A live claim held by any extension is normal unavailable work. It is not a tool error. Notification and terminal-turn claim receipts also include their exact expiry.
54
+
55
+ The coordinator keeps a queued delivery until durable settlement succeeds. If the Pi entry is visible and settlement fails, the coordinator reports an ambiguous receipt and keeps the send blocked. Recovery may acquire a fresh claim and adopt the existing Pi entry, but it cannot send that delivery again.
56
+
57
+ ## Widget
58
+
59
+ The extension will read the active origin-session run and render the existing workflow widget from its durable run state and definition snapshot. This path is read-only. It does not load workflow source, execute workflow code, or write workflow state.
60
+
61
+ The widget will:
62
+
63
+ - show running, waiting, and paused state;
64
+ - use the existing bounded ten-line renderer;
65
+ - support `Shift+Up` and `Shift+Down` scrolling;
66
+ - use serializable lines outside TUI mode;
67
+ - clear when the session has no active run or shuts down.
68
+
69
+ ## Escape and pause
70
+
71
+ The extension will use Pi's documented `agent_end` event and public extension context abort signal. It will act only when:
72
+
73
+ - the active context signal is aborted, or an assistant message has stop reason `aborted`;
74
+ - the origin session has a pending agent or assistant interaction;
75
+ - the same `agent_end` event contains that interaction's workflow prompt; and
76
+ - the run is not already paused.
77
+
78
+ The extension will send `run.pause` to the host. A live worker uses the existing exact-claim pause transaction. A waiting interaction has no worker and no live run claim, so the host will atomically set `paused = 1` on the parked run. The host will reject updates, submissions, and decision answers while paused.
79
+
80
+ Resume will clear the pause on the same pending interaction without creating a worker or a second prompt. Other paused work will keep the existing behavior: take a new claim generation and resume in a supervised child.
81
+
82
+ ## Scope
83
+
84
+ The change may update:
85
+
86
+ - the extension delivery coordinator and host client integration;
87
+ - the read-only workflow widget projection;
88
+ - host pause and resume handling for parked interactions;
89
+ - existing version-1 interaction response fields;
90
+ - focused unit, integration, and live Pi tests;
91
+ - the workflow host and authoring documentation.
92
+
93
+ ## Non-goals
94
+
95
+ - Do not change Pi core, private Pi APIs, or Pi session schemas.
96
+ - Do not restore the embedded workflow executor.
97
+ - Do not add a second production runtime, database, service, feature flag, migration, or compatibility reader.
98
+ - Do not claim exactly-once execution for an external effect that cannot prove it.
99
+ - Do not release until the repair passes a live test in a new Pi session.
100
+
101
+ ## Implementation order
102
+
103
+ 1. Keep a live claim in the shared delivery coordinator and add claim expiry to all delivery receipts.
104
+ 2. Treat an existing live presentation claim as unavailable work.
105
+ 3. Keep visible messages blocked until durable settlement succeeds or recovery adopts them.
106
+ 4. Add atomic pause and resume operations for a parked interaction.
107
+ 5. Reject updates and submissions while the run is paused.
108
+ 6. Add the public `agent_end` Escape handler.
109
+ 7. Restore the widget as a read-only durable-state view.
110
+ 8. Add regression tests and update the canonical documentation.
111
+ 9. Run repository checks, Pi Reviewer, a new-session live test, and CI before release.
112
+
113
+ ## Acceptance criteria
114
+
115
+ - Pi can remain busy longer than both the poll interval and presentation lease without a duplicate message or claim error.
116
+ - Every delivery ID creates at most one Pi session message and one model turn.
117
+ - A visible message with a failed receipt remains blocked from resend.
118
+ - A competing live presentation claim does not appear as a workflow tool failure.
119
+ - The widget appears for an active origin-session run and shows paused state after Escape.
120
+ - Escape pauses the matching waiting run through the host.
121
+ - A paused interaction rejects `update` and `submit`.
122
+ - Resume keeps the same request and allows submission without another prompt.
123
+ - Non-aborted turns and unrelated sessions do not pause the workflow.
124
+ - The extension and host execute no workflow or controller code in their own event loops.
125
+
126
+ ## Verification
127
+
128
+ Run:
129
+
130
+ ```bash
131
+ npm run check
132
+ npm run test:e2e
133
+ git diff --check
134
+ npx slophammer-ts@latest dry .
135
+ npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
136
+ npx -y @simpledoc/simpledoc check
137
+ ```
138
+
139
+ Then run Pi Reviewer against `main` with a ten-minute tool timeout until no P0 or P1 finding remains. Test the built package in a new Pi session and a new Herdr tab. The live test must show the widget, start one workflow step, pause it with Escape, resume it, and complete it without a duplicate prompt or claim error.
@@ -1,6 +1,6 @@
1
1
  # Workflow host
2
2
 
3
- Status: implemented. [Run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md) records the approved implementation plan.
3
+ Status: implemented. [Run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md) records the approved redesign. [Restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md) records the delivery, widget, and Escape-control repair.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -254,6 +254,8 @@ The first command set is:
254
254
  - `host.status`
255
255
  - `host.stop`
256
256
 
257
+ `notification.claim` and `turn.claim` can create a claim or revalidate the exact retained claim before delivery. Revalidation checks the in-memory client claim and its durable lease without creating another claim.
258
+
257
259
  Read operations may use the existing read-only store directly in viewers. Mutating Pi and CLI paths use the host.
258
260
 
259
261
  ## Worker protocol
@@ -316,9 +318,11 @@ Agent and assistant-message steps for an interactive run execute in the origin P
316
318
 
317
319
  The worker commits the node's resolved wall-clock deadline before it proposes `interaction.requested`. The host commits the request, changes the node attempt to waiting, parks the queue row, releases the claim, and acknowledges the worker. The worker then exits. The host continues to enforce the durable deadline while no worker exists. If the deadline passes, one control claim atomically closes the stale request and schedules a supervised timeout-resume child. The child preserves the same attempt and deadline, records `timed_out`, and follows any `$result.outcome` edge. A run with no timeout recovery edge becomes terminal and releases its session reservation. Restart recovery starts this timeout path before it schedules other work.
318
320
 
319
- The extension finds pending requests during `session_start`, after `agent_settled`, and once per second while the session is open. One shared session-delivery coordinator handles step prompts, protected decisions, notifications, and terminal presentation turns. It waits until Pi is idle and has no pending messages before it claims new work. Because the host claim is asynchronous, it checks those conditions again immediately before the synchronous call to the documented `pi.sendMessage()` API. An unused claim expires and is never sent later. After a claim, the coordinator records the delivery ID in memory before it sends. Polling only checks for delivery evidence while that ID remains queued; it cannot send the message again.
321
+ The extension finds pending requests during `session_start`, after `agent_settled`, and once per second while the session is open. One shared session-delivery coordinator handles step prompts, protected decisions, notifications, and terminal presentation turns. It waits until Pi is idle and has no pending messages before it claims new work. Because the host claim is asynchronous, it checks those conditions again immediately before the synchronous call to the documented `pi.sendMessage()` API. The coordinator remembers the claimed delivery before that final check. If Pi became busy, a later poll can send with that exact claim while its lease remains live. An expired unused claim is discarded. Polling cannot acquire a second claim or send a delivery that is already queued.
322
+
323
+ The host grants one live presentation claim. The current presenter cannot claim the same request again before that claim expires. A poll that sees any live presentation claim treats it as unavailable, not as a tool failure. When the matching custom message appears in the active Pi branch, the coordinator records its public session entry ID through the host and clears the local queued state. If Pi becomes idle without exposing a matching entry after the confirmation interval, the coordinator reports the delivery as ambiguous and keeps it blocked. A failed durable receipt also keeps the visible message blocked. Neither case can send the message again. The normal `workflow` tool contract then submits updates and results.
320
324
 
321
- The host grants one live presentation claim. The current presenter cannot claim the same request again before that claim expires. When the matching custom message appears in the active Pi branch, the coordinator records its public session entry ID through the host and clears the local queued state. If Pi becomes idle without exposing a matching entry after the confirmation interval, the coordinator reports the delivery as ambiguous and keeps it blocked. It does not infer success or retry. The normal `workflow` tool contract then submits updates and results.
325
+ The extension projects the active origin-session run into Pi's widget and status APIs by reading host-owned durable state. This projection never runs workflow code and never writes run state. `Shift+Up` and `Shift+Down` scroll the widget.
322
326
 
323
327
  A tool update or submission goes to the host. It includes the exact request, node, attempt, expected revision, and tool-call idempotency key. The host first checks this transport contract and records a provisional `validating` submission. It then schedules a supervised workflow child. Only that child loads workflow code and runs the node's `validate` function. The child reports `interaction.accepted` or `interaction.rejected` to the host. The host settles the request only after acceptance. A rejected payload leaves the same request pending and returns the stored actionable error to the model. If the child stops before it reports a result, the host rejects the provisional submission and leaves the request ready for a corrected retry.
324
328
 
@@ -340,7 +344,7 @@ The run binding records `interactive` or `headless` execution mode. Viewers show
340
344
 
341
345
  ## Pause and cancellation
342
346
 
343
- Pause atomically commits `paused = 1`, parks the queue, releases the exact claim, and stores the command receipt. The fenced worker process group then stops. An uncommitted pure node can run again after resume. Resume takes a new generation and starts another worker from the last durable boundary.
347
+ Pause atomically commits `paused = 1`, parks the queue, releases the exact claim, and stores the command receipt. The fenced worker process group then stops. If a Pi model turn ends while the public extension context signal is aborted, or with public stop reason `aborted`, the extension sends this same host pause command only when that `agent_end` event contains the pending interaction's workflow prompt. A parked interaction has no worker or live run claim, so the host marks it paused in place. While paused, updates, submissions, and decision answers are rejected. Resume clears the pause on that same pending interaction; other paused work takes a new generation and starts another worker from the last durable boundary. An uncommitted pure node can run again after resume.
344
348
 
345
349
  Cancellation against a live worker atomically commits terminal cancellation, cancels pending attempt and interaction state, settles effect recovery state, releases the exact claim, and stores the command receipt. A pending effect becomes cancelled. An applying effect becomes ambiguous because the host cannot prove its external outcome. The host then stops the fenced worker process group. A host crash after the receipt cannot resume the cancelled run or retry the ambiguous effect. If the child does not stop by the deadline, the host kills its process group.
346
350
 
@@ -1,6 +1,6 @@
1
1
  # Workflow step messages
2
2
 
3
- This specification defines how pi-workflows shows agent-step instructions in an interactive Pi session. The model receives the complete step prompt, while the user sees a small workflow card that can be expanded.
3
+ This specification defines how pi-workflows shows agent-step instructions in an interactive Pi session. The model receives the complete step prompt, while the user sees a small workflow card that can be expanded. [Restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md) records the delivery and session-control repair.
4
4
 
5
5
  ## Goal
6
6
 
@@ -48,11 +48,11 @@ pi.sendMessage(
48
48
 
49
49
  ## Session delivery
50
50
 
51
- One shared coordinator delivers step prompts, protected decisions, notifications, and final workflow results. It does not claim or send a new message while Pi is busy or another message is pending. It checks these conditions again after the asynchronous host claim and immediately before the synchronous send. If Pi became busy, the unused claim expires and is never sent later. Before it calls `pi.sendMessage()`, the coordinator records the stable delivery ID in a process-local queued map. The one-second poll can then look for the matching session entry, but it cannot send that ID again.
51
+ One shared coordinator delivers step prompts, protected decisions, notifications, and final workflow results. It does not claim or send a new message while Pi is busy or another message is pending. It checks these conditions again after the asynchronous host claim and immediately before the synchronous send. The coordinator remembers the stable delivery ID and exact claim expiry before that final check. If Pi became busy, a later poll can use that same claim while it remains live. Before it sends retained work, it revalidates the exact claim and durable resource through the host, then checks Pi and the lease again. Cancelled, paused, or replaced work is discarded. An expired unused claim is also discarded.
52
52
 
53
- The coordinator clears the queued ID only after it observes the custom message in the active branch and saves the public Pi session entry ID through the workflow host. If Pi becomes idle but does not expose that entry after the confirmation interval, the coordinator reports an ambiguous delivery and keeps it blocked. Reload and restart recovery first search the branch for that stable ID. An existing entry is adopted. A new message is sent only when no entry exists and the host grants a new claim.
53
+ Before the coordinator calls `pi.sendMessage()`, it records the delivery in a process-local queued map. The one-second poll can look for the matching session entry, but it cannot send that ID again. The coordinator clears the queued ID only after it observes the custom message in the active branch and saves the public Pi session entry ID through the workflow host. If Pi does not expose that entry after the confirmation interval, or if saving its receipt fails, the coordinator reports an ambiguous delivery and keeps it blocked.
54
54
 
55
- The host does not grant another live presentation claim to the same extension client. Claim expiry permits recovery by a new claimant; it is not permission for the current process to resend a queued message.
55
+ Reload and restart recovery first search the branch for that stable ID. An existing entry is adopted. A new message is sent only when no entry exists and the host grants a new claim. The host does not grant a second live presentation claim. Polling treats another live claim as unavailable work rather than a workflow error.
56
56
 
57
57
  ## Engine boundary
58
58
 
package/docs/workflows.md CHANGED
@@ -475,8 +475,18 @@ The model sees one `workflow` tool. Its `action` field supports:
475
475
  A direct user request to continue or resume the active workflow maps to
476
476
  `resume` immediately. The model does not call `status` instead of `resume` or
477
477
  use it as a prerequisite. An already active run adopts the resume request. A
478
- paused or parked run gets a new claim generation and worker. With no resumable
479
- run, the host rejects the request.
478
+ paused interaction remains the same durable request and resumes without a
479
+ worker. Other paused or parked work gets a new claim generation and worker.
480
+ With no resumable run, the host rejects the request.
481
+
482
+ The origin Pi session shows its active run in the workflow widget. `Shift+Up`
483
+ and `Shift+Down` scroll it. If Escape aborts the model turn started by a
484
+ presented workflow interaction, the extension detects the public context abort
485
+ signal and pauses that run through the host. It also accepts Pi's public
486
+ `aborted` stop reason. The matching `agent_end` event must contain the workflow
487
+ prompt, so an unrelated interrupted turn cannot pause old pending work. The
488
+ paused run does not accept updates, submissions, or decision answers until
489
+ `resume`.
480
490
 
481
491
  `status` reports the durable queue projection. A host command succeeds only
482
492
  after its transaction commits. The protocol stores request fingerprints and
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.15.2"
3
+ version = "0.15.3"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -191,6 +191,7 @@ type RunRow = {
191
191
  workflowName: string;
192
192
  workflowRef: string;
193
193
  runStatus: string;
194
+ paused: number;
194
195
  definitionDigest: Buffer;
195
196
  definitionHash: Buffer;
196
197
  inputHash: Buffer;
@@ -1561,6 +1562,104 @@ export class SqliteControllerStore implements ControllerStore {
1561
1562
  return this.releaseRunClaim(options.runId, options.claimToken, "parked", options.now);
1562
1563
  }
1563
1564
 
1565
+ pauseParkedWorkflowRun(options: { runId: string; now?: string }): boolean {
1566
+ const now = epoch(validTimestamp(options.now));
1567
+ return this.state.transaction(() => {
1568
+ const row = this.workflowRunRow(options.runId);
1569
+ if (row === undefined || row.status !== "parked") return false;
1570
+ const lease = this.requireLease(row.resourceId);
1571
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
1572
+ const pending = this.state.connection
1573
+ .prepare(
1574
+ `SELECT 1 FROM interactive_requests
1575
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`,
1576
+ )
1577
+ .get(options.runId);
1578
+ if (pending === undefined) return false;
1579
+ if (row.paused === 1) return true;
1580
+ if (!["running", "waiting"].includes(row.runStatus)) return false;
1581
+ const changed = this.state.connection
1582
+ .prepare(
1583
+ `UPDATE runs SET paused = 1, status_detail = 'paused', updated_at = ?
1584
+ WHERE run_id = ? AND paused = 0 AND status IN ('running', 'waiting')`,
1585
+ )
1586
+ .run(now, options.runId);
1587
+ if (changed.changes !== 1) return false;
1588
+ const revision = this.resourceRevision(row.resourceId);
1589
+ this.bumpResource(row.resourceId, revision, now);
1590
+ this.insertEvent(
1591
+ row.resourceId,
1592
+ revision + 1,
1593
+ "run.paused",
1594
+ "control",
1595
+ null,
1596
+ { status: "parked" },
1597
+ now,
1598
+ );
1599
+ recordViewerDeltas(
1600
+ this.state,
1601
+ options.runId,
1602
+ [{ targetType: "summary" }, { targetType: "replay" }],
1603
+ now,
1604
+ );
1605
+ return true;
1606
+ });
1607
+ }
1608
+
1609
+ resumePausedInteraction(options: { runId: string; now?: string }): boolean {
1610
+ const now = epoch(validTimestamp(options.now));
1611
+ return this.state.transaction(() => {
1612
+ const row = this.workflowRunRow(options.runId);
1613
+ if (
1614
+ row === undefined ||
1615
+ row.status !== "parked" ||
1616
+ row.runStatus !== "waiting" ||
1617
+ row.paused !== 1
1618
+ ) {
1619
+ return false;
1620
+ }
1621
+ const lease = this.requireLease(row.resourceId);
1622
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
1623
+ const pending = this.state.connection
1624
+ .prepare(
1625
+ `SELECT 1 FROM interactive_requests
1626
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`,
1627
+ )
1628
+ .get(options.runId);
1629
+ if (pending === undefined) return false;
1630
+ const changed = this.state.connection
1631
+ .prepare(
1632
+ `UPDATE runs
1633
+ SET paused = 0, status_detail = 'waiting for origin-session input', updated_at = ?
1634
+ WHERE run_id = ? AND status = 'waiting' AND paused = 1`,
1635
+ )
1636
+ .run(now, options.runId);
1637
+ if (changed.changes !== 1) return false;
1638
+ const revision = this.resourceRevision(row.resourceId);
1639
+ this.bumpResource(row.resourceId, revision, now);
1640
+ this.insertEvent(
1641
+ row.resourceId,
1642
+ revision + 1,
1643
+ "run.resumed",
1644
+ "control",
1645
+ null,
1646
+ { status: "waiting" },
1647
+ now,
1648
+ );
1649
+ recordViewerDeltas(
1650
+ this.state,
1651
+ options.runId,
1652
+ [{ targetType: "summary" }, { targetType: "replay" }],
1653
+ now,
1654
+ );
1655
+ return true;
1656
+ });
1657
+ }
1658
+
1659
+ isWorkflowRunPaused(runId: string): boolean {
1660
+ return this.workflowRunRow(runId)?.paused === 1;
1661
+ }
1662
+
1564
1663
  parkWorkflowRunForSourceChange(options: {
1565
1664
  runId: string;
1566
1665
  claimToken: string;
@@ -2038,6 +2137,21 @@ export class SqliteControllerStore implements ControllerStore {
2038
2137
  return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
2039
2138
  }
2040
2139
 
2140
+ isWorkflowTurnIntentClaimLive(options: {
2141
+ intentId: string;
2142
+ targetSessionId: string;
2143
+ claimToken: string;
2144
+ now?: string;
2145
+ }): boolean {
2146
+ const row = this.turnIntentRow(options.intentId);
2147
+ return (
2148
+ row !== undefined &&
2149
+ row.targetSessionId === options.targetSessionId &&
2150
+ row.resolvedAt === null &&
2151
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now)
2152
+ );
2153
+ }
2154
+
2041
2155
  claimWorkflowTurnIntent(options: {
2042
2156
  intentId: string;
2043
2157
  targetSessionId: string;
@@ -2279,6 +2393,20 @@ export class SqliteControllerStore implements ControllerStore {
2279
2393
  return result;
2280
2394
  }
2281
2395
 
2396
+ isWorkflowNotificationClaimLive(options: {
2397
+ notificationId: string;
2398
+ targetSessionId: string;
2399
+ claimToken: string;
2400
+ now?: string;
2401
+ }): boolean {
2402
+ const row = this.notificationRowById(options.notificationId);
2403
+ return (
2404
+ row !== undefined &&
2405
+ row.targetSessionId === options.targetSessionId &&
2406
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now)
2407
+ );
2408
+ }
2409
+
2282
2410
  markWorkflowNotificationDelivered(options: {
2283
2411
  notificationId: string;
2284
2412
  targetSessionId: string;
@@ -3452,7 +3580,7 @@ function workflowSelect(clause: string): string {
3452
3580
  function workflowRunSelect(clause: string): string {
3453
3581
  return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
3454
3582
  d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
3455
- r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
3583
+ r.paused, r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
3456
3584
  r.input_hash AS inputHash,
3457
3585
  r.launch_options_hash AS launchOptionsHash,
3458
3586
  q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,