@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.
- package/dist/controllers/sqlite.d.ts +21 -0
- package/dist/controllers/sqlite.js +82 -1
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.js +137 -4
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +16 -0
- package/dist/extension/session-view.js +119 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.js +2 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +92 -26
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +2 -0
- package/dist/host/state.js +7 -1
- package/dist/host/state.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/WORKFLOW_HOST.md +8 -4
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/workflows.md +12 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/controllers/sqlite.ts +129 -1
- package/src/extension/index.ts +174 -4
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +131 -0
- package/src/extension/widget.ts +3 -2
- package/src/host/runner.ts +103 -29
- package/src/host/state.ts +11 -1
|
@@ -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.
|
package/docs/WORKFLOW_HOST.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
479
|
-
|
|
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
package/package.json
CHANGED
|
@@ -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,
|