@osolmaz/pi-workflows 0.2.0 → 0.3.0

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 (133) hide show
  1. package/README.md +71 -5
  2. package/dist/builtins/monitor.workflow.d.ts +71 -0
  3. package/dist/builtins/monitor.workflow.js +234 -0
  4. package/dist/builtins/monitor.workflow.js.map +1 -0
  5. package/dist/controllers/conditions.d.ts +6 -0
  6. package/dist/controllers/conditions.js +68 -0
  7. package/dist/controllers/conditions.js.map +1 -0
  8. package/dist/controllers/definition.d.ts +6 -0
  9. package/dist/controllers/definition.js +45 -0
  10. package/dist/controllers/definition.js.map +1 -0
  11. package/dist/controllers/effects.d.ts +14 -0
  12. package/dist/controllers/effects.js +104 -0
  13. package/dist/controllers/effects.js.map +1 -0
  14. package/dist/controllers/errors.d.ts +12 -0
  15. package/dist/controllers/errors.js +25 -0
  16. package/dist/controllers/errors.js.map +1 -0
  17. package/dist/controllers/index.d.ts +13 -0
  18. package/dist/controllers/index.js +13 -0
  19. package/dist/controllers/index.js.map +1 -0
  20. package/dist/controllers/json.d.ts +5 -0
  21. package/dist/controllers/json.js +57 -0
  22. package/dist/controllers/json.js.map +1 -0
  23. package/dist/controllers/loader.d.ts +23 -0
  24. package/dist/controllers/loader.js +74 -0
  25. package/dist/controllers/loader.js.map +1 -0
  26. package/dist/controllers/manager.d.ts +58 -0
  27. package/dist/controllers/manager.js +399 -0
  28. package/dist/controllers/manager.js.map +1 -0
  29. package/dist/controllers/results.d.ts +5 -0
  30. package/dist/controllers/results.js +32 -0
  31. package/dist/controllers/results.js.map +1 -0
  32. package/dist/controllers/sqlite.d.ts +212 -0
  33. package/dist/controllers/sqlite.js +1009 -0
  34. package/dist/controllers/sqlite.js.map +1 -0
  35. package/dist/controllers/store.d.ts +112 -0
  36. package/dist/controllers/store.js +32 -0
  37. package/dist/controllers/store.js.map +1 -0
  38. package/dist/controllers/types.d.ts +159 -0
  39. package/dist/controllers/types.js +2 -0
  40. package/dist/controllers/types.js.map +1 -0
  41. package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
  42. package/dist/controllers/workflow-engine-scheduler.js +93 -0
  43. package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
  44. package/dist/controllers/workflows.d.ts +27 -0
  45. package/dist/controllers/workflows.js +109 -0
  46. package/dist/controllers/workflows.js.map +1 -0
  47. package/dist/extension/controller-host.d.ts +47 -0
  48. package/dist/extension/controller-host.js +110 -0
  49. package/dist/extension/controller-host.js.map +1 -0
  50. package/dist/extension/executor.js +1 -1
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/index.d.ts +7 -0
  53. package/dist/extension/index.js +1053 -96
  54. package/dist/extension/index.js.map +1 -1
  55. package/dist/extension/recorder.d.ts +6 -0
  56. package/dist/extension/recorder.js +20 -8
  57. package/dist/extension/recorder.js.map +1 -1
  58. package/dist/extension/workflow-tool.d.ts +28 -0
  59. package/dist/extension/workflow-tool.js +33 -0
  60. package/dist/extension/workflow-tool.js.map +1 -0
  61. package/dist/host/processes.d.ts +24 -0
  62. package/dist/host/processes.js +114 -0
  63. package/dist/host/processes.js.map +1 -0
  64. package/dist/host/rpc-bridge.d.ts +9 -0
  65. package/dist/host/rpc-bridge.js +39 -0
  66. package/dist/host/rpc-bridge.js.map +1 -0
  67. package/dist/host/rpc-executor.d.ts +38 -0
  68. package/dist/host/rpc-executor.js +254 -0
  69. package/dist/host/rpc-executor.js.map +1 -0
  70. package/dist/host/runner.d.ts +49 -0
  71. package/dist/host/runner.js +350 -0
  72. package/dist/host/runner.js.map +1 -0
  73. package/dist/viewer/cli.d.ts +7 -3
  74. package/dist/viewer/cli.js +150 -19
  75. package/dist/viewer/cli.js.map +1 -1
  76. package/dist/workflows/engine.d.ts +36 -0
  77. package/dist/workflows/engine.js +244 -14
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/errors.d.ts +23 -0
  80. package/dist/workflows/errors.js +38 -0
  81. package/dist/workflows/errors.js.map +1 -1
  82. package/dist/workflows/graph.js +0 -5
  83. package/dist/workflows/graph.js.map +1 -1
  84. package/dist/workflows/loader.d.ts +5 -3
  85. package/dist/workflows/loader.js +10 -1
  86. package/dist/workflows/loader.js.map +1 -1
  87. package/dist/workflows/schema.js +1 -1
  88. package/dist/workflows/schema.js.map +1 -1
  89. package/dist/workflows/store.d.ts +50 -6
  90. package/dist/workflows/store.js +446 -51
  91. package/dist/workflows/store.js.map +1 -1
  92. package/dist/workflows/types.d.ts +10 -0
  93. package/docs/CONTROLLERS.md +215 -0
  94. package/docs/development.md +12 -9
  95. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  96. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  97. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  98. package/docs/run-bundles.md +66 -27
  99. package/docs/workflows.md +131 -11
  100. package/examples/controllers/pull-request.controller.ts +215 -0
  101. package/package.json +10 -2
  102. package/src/builtins/monitor.workflow.ts +278 -0
  103. package/src/controllers/conditions.ts +110 -0
  104. package/src/controllers/definition.ts +65 -0
  105. package/src/controllers/effects.ts +123 -0
  106. package/src/controllers/errors.ts +27 -0
  107. package/src/controllers/index.ts +90 -0
  108. package/src/controllers/json.ts +62 -0
  109. package/src/controllers/loader.ts +104 -0
  110. package/src/controllers/manager.ts +533 -0
  111. package/src/controllers/results.ts +46 -0
  112. package/src/controllers/sqlite.ts +1427 -0
  113. package/src/controllers/store.ts +160 -0
  114. package/src/controllers/types.ts +183 -0
  115. package/src/controllers/workflow-engine-scheduler.ts +145 -0
  116. package/src/controllers/workflows.ts +152 -0
  117. package/src/extension/controller-host.ts +163 -0
  118. package/src/extension/executor.ts +1 -1
  119. package/src/extension/index.ts +1243 -117
  120. package/src/extension/recorder.ts +65 -36
  121. package/src/extension/workflow-tool.ts +59 -0
  122. package/src/host/processes.ts +119 -0
  123. package/src/host/rpc-bridge.ts +44 -0
  124. package/src/host/rpc-executor.ts +299 -0
  125. package/src/host/runner.ts +406 -0
  126. package/src/viewer/cli.ts +167 -21
  127. package/src/workflows/engine.ts +327 -13
  128. package/src/workflows/errors.ts +45 -0
  129. package/src/workflows/graph.ts +0 -5
  130. package/src/workflows/loader.ts +13 -3
  131. package/src/workflows/schema.ts +1 -1
  132. package/src/workflows/store.ts +555 -46
  133. package/src/workflows/types.ts +10 -0
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: Add always-on workflow execution
3
+ author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
4
+ date: 2026-08-05
5
+ updated: 2026-08-05
6
+ status: implemented
7
+ ---
8
+
9
+ # Always-on workflows plan
10
+
11
+ Pi Workflows should feel the same whether the user watches a run or walks away from it. In the user's words: "I might start a workflow locally in Pi then I wait for it to complete. All the while I am looking at the screen and I'm not closing the Pi window. When the workflow ends I just want to be able to continue the same Pi session like normal with a session up to date with what happened in the workflow." And: "I just want to interact by starting a workflow, closing it, and then coming back and then still being able to continue it when I open it up. It's syncing continuously or something."
12
+
13
+ These are not two modes. The user asked for "both in a single unified system." This plan makes the Pi window irrelevant to execution: closing or opening the window is a change in observation, not in the run. The work stays on one machine, uses the merged controller runtime as its foundation, and does not modify Pi core.
14
+
15
+ This revision incorporates an external design review of the first draft. The review confirmed the three design rules and found five structural gaps in the details. The Decisions section resolves each one before implementation starts.
16
+
17
+ ## Design
18
+
19
+ Four rules define the system:
20
+
21
+ 1. **Durable state is the only source of truth.** Run bundles, the queue, and the event log live on disk. Nothing important lives in a session's memory.
22
+ 2. **Runners are interchangeable, but ownership is exclusive.** Any live process running the engine claims work from the durable queue. A run has exactly one owning runner at a time, proven by a claim token, and only that runner may write to the run.
23
+ 3. **The session is always a view.** It attaches to a run's event stream and renders it. An open window sees a live tail; a reopened window catches up from the same stream. Interaction such as approvals uses durable waiting states, never prompts tied to the window's lifetime.
24
+ 4. **Claims arbitrate every lifecycle decision.** Only the current claim holder may resume, interrupt, or write a terminal event for a run. Recovery code that finds an abandoned run goes through the queue instead of writing to the bundle directly.
25
+
26
+ The controller runtime already provides most of the machinery: a deduplicated queue with expiring claims, a structured event table, crash recovery through the trace tail, and guarded effect records. This plan extends that treatment to runs the user starts interactively and adds the view layer.
27
+
28
+ ## Decisions
29
+
30
+ These points were open in the first draft. External review showed each one is load-bearing, so they are decided here.
31
+
32
+ 1. **Write fencing on bundles.** Every claim records a runner ID, a token, and a lease expiry in the store. Every bundle write verifies the token inside the run lock before appending and fails the runner fast when the token no longer matches. Expiring claims alone do not stop a stalled runner from writing; fencing does.
33
+ 2. **Explicit resume protocol.** Resume is a named operation, not a restart. It truncates a torn trace tail to the last complete line, rebuilds run state from the trace, seeds the trace sequence from the tail, accounts for already-executed steps against the step limit, and records a resume boundary event before continuing.
34
+ 3. **Resume scope.** User-started runs get node-level resume. Controller child runs keep their current new-attempt semantics, because attempt immutability and parent-side retry already work and are tested. Both behaviors are explicit; nothing mixes silently.
35
+ 4. **A dedicated run queue.** Interactive runs do not fit the controller queue, which is keyed to controller resources. A new `workflow_run_queue` table shares the claim, lease, and fencing pattern and adds runner affinity fields. Runs stay out of `/controller list`.
36
+ 5. **Origin affinity.** An interactively started run is inserted and claimed in one transaction, so the session that started it owns it from birth. The standalone host takes over only when that claim is released or expires. This guarantees a watched run's conversation happens in the watching session.
37
+ 6. **Close-to-park shutdown.** `session_shutdown` stops writing a terminal cancel event for queued runs. It aborts in-flight work without a terminal event and releases the claim, leaving a resumable bundle. A clean close releases claims explicitly; a crash relies on lease expiry.
38
+ 7. **Per-attempt capture.** Session capture becomes segmented per attempt, keyed by attempt ID, so a run handed off to the host or a new session starts a fresh capture segment instead of failing integrity checks.
39
+ 8. **Continuation runs for waiting.** A run that needs human input ends at a checkpoint with status `waiting`, preserving bundle immutability. An answered checkpoint starts a continuation run with a new run ID chained to its parent, carrying forward prior outputs. The parent link makes the chain inspectable.
40
+ 9. **Source pinning.** The manifest stores a content hash of the workflow source at run start. Resume refuses to continue against changed source unless forced, and a forced resume records the mismatch in the trace.
41
+ 10. **Snapshot catch-up.** Notifications are idempotent snapshots of run state as of a store sequence number, recomputed from the store, not a stream of one-off messages. The watermark persists per session before sending. A skipped incremental notification is subsumed by the next snapshot.
42
+ 11. **A run-level event feed.** Run lifecycle transitions write rows into a store table, so one watermark covers both controller and run events. Tailing `trace.ndjson` is reserved for the single actively watched run.
43
+ 12. **Store-error backoff.** Worker loops treat store errors such as `SQLITE_BUSY` as transient and back off instead of letting a worker die silently. The host's advisory lock guards host-versus-host only; the embedded runner does not take it, and a second host refuses to start.
44
+ 13. **Orphan reaping.** The host spawns `pi --mode rpc` children in their own process group, records child PIDs in the bundle, and reaps known orphans on startup. Consequential actions stay behind guarded effects regardless.
45
+
46
+ ## Requirements
47
+
48
+ - Starting `/workflow run` in a Pi session creates a durable queued run claimed by that session, and shows its progress live.
49
+ - Closing Pi mid-run never loses the run and never writes a spurious terminal event. With the standalone host alive, the host reclaims and resumes the run. Without a host, the run waits and resumes when a runner returns.
50
+ - Reopening a session brings it up to date with an idempotent snapshot: what finished, what failed, what waits for input. No state is duplicated and no information is permanently lost.
51
+ - A run that needs human input ends at a checkpoint. The user answers with a command, and a continuation run carries the work forward. The wait survives any process lifetime.
52
+ - Killing any process at any point recovers without duplicate trace sequences, duplicate notifications of record, or duplicate external effects. A stalled runner that loses its claim can never write again.
53
+ - Everything uses documented Pi public APIs. No Pi core changes.
54
+
55
+ ## Work items
56
+
57
+ 1. **Run queue and fencing.** Add the `workflow_run_queue` table with claim tokens, lease expiry, and runner affinity. Gate every bundle write on the token inside the run lock. Insert-and-claim interactive starts in one transaction.
58
+ 2. **Node-level resume.** Implement the explicit resume protocol from Decision 2, with source pinning from Decision 9. User-started runs resume at the interrupted node; controller child runs are untouched.
59
+ 3. **Close-to-park and capture segments.** Change `session_shutdown` to abort-without-terminal plus claim release for queued runs. Split session capture into per-attempt segments the integrity checker understands.
60
+ 4. **Continuation runs.** Chain an answered checkpoint to a new run ID with a parent link and carried-forward outputs. Render the chain as one logical run in views.
61
+ 5. **Session sync.** Add the run-level event feed, per-session watermarks, snapshot catch-up on `session_start`, and noteworthy-event messages. Live watching tails `trace.ndjson` from a remembered byte offset with `fs.watch`, reusing the TUI viewer's file-tail path.
62
+ 6. **Standalone host.** A `pi-workflows` CLI subcommand loads controller definitions, opens the project store, and runs claiming in a loop with store-error backoff. Conversation child nodes run in spawned headless `pi --mode rpc` sessions with orphan reaping. The host takes an advisory lock against other hosts, drains on SIGTERM, and recovers on restart. It is a foreground process the user runs in a terminal; it is not a service.
63
+
64
+ ## Non-goals
65
+
66
+ - Pi core changes of any kind. Every integration uses public APIs: commands, session events, widgets, `sendUserMessage`, and `pi --mode rpc`.
67
+ - Multi-machine execution, a remote store, or leader election. SQLite and one machine are in scope; the store contracts leave room for a remote implementation later.
68
+ - Installing or configuring a system or user service. The host is a process the user starts and stops.
69
+ - A push channel from an external process into a live session. Polling the shared store is the mechanism, and it is fast enough.
70
+ - Exactly-once chat notifications. The guarantee is no duplicated state and no permanently lost information, delivered through idempotent snapshots.
71
+ - Exactly-once external side effects beyond the existing guarded effect records.
72
+
73
+ ## Assumptions
74
+
75
+ - One machine and one user, with the store and run bundles on the local filesystem.
76
+ - Spawning `pi --mode rpc` per conversation child run is acceptable at the expected cadence. If startup cost proves too high, the host keeps a small pool of persistent RPC sessions instead. Both options stay outside Pi core.
77
+ - A polling interval of a few seconds is responsive enough for the session view. File watching covers the live tail of a watched run.
78
+ - The user does not need machine-sleep or power-loss coverage beyond crash recovery. A stopped machine stops work until a runner returns.
79
+
80
+ ## Open questions
81
+
82
+ - Which events deserve a chat message and which belong only in the widget. The default should be quiet.
83
+ - The exact host command shape, for example `pi-workflows run --project <dir>` versus a subcommand under `controllers`.
84
+
85
+ ## Departures from the decisions
86
+
87
+ The implementation matches the decisions above with these refinements:
88
+
89
+ - Graph validation now allows outgoing edges from checkpoint nodes. The old
90
+ rejection encoded terminal-forever checkpoints; continuations make those
91
+ edges live. This is a deliberate contract change for workflow authors.
92
+ - The host command is `pi-workflows host`, chosen over `run` because the
93
+ viewer CLI's vocabulary already uses runs for bundles.
94
+ - The first capture stays flat at `session/`; only binds from the second
95
+ recorder onward write segments under `session/segments/`. This keeps the
96
+ layout of every existing single-session bundle and the viewer unchanged.
97
+ - Resume always starts unpaused; the operator can pause again.
98
+ - Headless conversation children keep the exact tool contract: a bridge
99
+ extension loaded with `-e` registers the `workflow` tool and reports
100
+ submissions to the host over stderr, so no engine prompt changes were
101
+ needed.
102
+ - The host accepts explicit `storeFile` and `runsDir` options. Its defaults
103
+ resolve from the project and environment like every other entry point.
104
+
105
+ ## Acceptance criteria
106
+
107
+ - Start a run in Pi, then close Pi while a conversation node is mid-response and the host is running. The host reclaims and resumes the run, and reopening Pi shows the catch-up snapshot and allows normal conversation about the result.
108
+ - The same flow without the host: the run resumes at the interrupted node when Pi reopens and completes.
109
+ - A stalled runner that loses its claim cannot write to the bundle afterward; fencing rejects its writes.
110
+ - An open session reflects host-driven progress within a few seconds. Two sessions on one project each get complete catch-up; neither starves the other.
111
+ - A waiting-for-input run survives closing and reopening. Answering it starts a continuation run that carries forward prior outputs.
112
+ - `kill -9` on the host mid-append, followed by a restart, repairs the torn trace tail and recovers without duplicate trace sequence numbers and without repeating an applied external effect. Orphaned RPC children are reaped.
113
+ - No changes to Pi core; the diff touches only this package.
114
+
115
+ ## Verification
116
+
117
+ - `npm run check` and `npm run test:e2e`, including new real-Pi E2E tests that start a run, kill the host, restart it, and assert continuation.
118
+ - A fencing test with both runners alive and a forced lease loss.
119
+ - A two-session watermark test on one project.
120
+ - A `kill -9` mid-append torn-tail resume test.
121
+ - A checkpoint answer-to-continuation round-trip test.
122
+ - Extension tests with fake timers for the polling loop, watermark, and snapshot catch-up.
123
+ - `npx slophammer-ts@latest dry .` and the dependency-boundary check.
124
+ - `npx -y @simpledoc/simpledoc check` for documentation changes.
125
+ - Manual pass through both usage patterns from the user's request with the host in a terminal.
@@ -0,0 +1,184 @@
1
+ ---
2
+ title: Let agents run and manage monitor workflows
3
+ author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
4
+ date: 2026-08-10
5
+ updated: 2026-08-10
6
+ status: implemented
7
+ ---
8
+
9
+ # Agent-managed monitor workflows plan
10
+
11
+ The user should be able to tell an agent, "Monitor this every 30 minutes," and have the agent start the right workflow. The user must not write controller records or JSON. The existing `workflow` model tool should manage workflows instead of serving only as a step-submission tool.
12
+
13
+ A monitor is one Pi Workflows graph. It checks the target, reports a meaningful change, sleeps for the requested interval with the existing shell node, and loops. This plan does not use controllers, Unified Exec, a new wait node, or a second scheduler.
14
+
15
+ ## Shipped design
16
+
17
+ The implementation follows this plan. The built-in monitor defaults to 1,000 checks, caps the interval at 24 hours, and uses a 5,010-step engine limit as a second guard. The normal Pi extension exposes all workflow tool actions. The headless RPC bridge exposes only `submit`. A real-Pi end-to-end test proves that a normal model turn can start the built-in monitor and complete its first check. The automated test inspects the exact 30-minute Node timer command and both timeout margins instead of making the test suite wait for 30 minutes. The Node timer keeps the shell action portable across Pi's supported platforms.
18
+
19
+ ## User experience
20
+
21
+ A normal request looks like this:
22
+
23
+ > Monitor PR 123 every 30 minutes. Tell me when checks fail. Stop when it is merged or closed.
24
+
25
+ The agent recognizes repeated monitoring from the `workflow` tool description and starts the built-in `monitor` workflow. The tool call contains the structured input, but the user does not write it.
26
+
27
+ The monitor checks immediately, then waits between later checks. It writes a normal assistant message only when the requested report condition is met. It stops when the requested stop condition is met, when it reaches its safety limit, or when the user or agent cancels it.
28
+
29
+ Existing controls remain available:
30
+
31
+ ```text
32
+ /workflow pause
33
+ /workflow resume
34
+ /workflow cancel
35
+ ```
36
+
37
+ The same operations become available to the model through the `workflow` tool. One active workflow per Pi session remains the rule. A long monitor therefore occupies that session's workflow slot.
38
+
39
+ ## Workflow tool
40
+
41
+ Replace the current submit-only schema with one discriminated tool schema. Use Pi's documented `registerTool` API and `StringEnum` helper.
42
+
43
+ The tool supports these actions:
44
+
45
+ - `list`: list discovered workflows and their source.
46
+ - `start`: start a workflow by name or path with structured input.
47
+ - `status`: return a bounded summary of the active or named run.
48
+ - `pause`: pause the active run at its next node boundary.
49
+ - `resume`: resume the active paused run.
50
+ - `cancel`: cancel the active run.
51
+ - `answer`: answer a waiting checkpoint, with an optional run ID.
52
+ - `submit`: submit the result required by the current workflow step contract.
53
+
54
+ The `submit` form becomes:
55
+
56
+ ```json
57
+ {
58
+ "action": "submit",
59
+ "step": "check",
60
+ "attempt": "attempt-id",
61
+ "output": {}
62
+ }
63
+ ```
64
+
65
+ Update workflow step prompts and reminders to include `action: "submit"`. Do not retain the old submit shape as a compatibility alias.
66
+
67
+ Slash commands and tool actions must call the same internal lifecycle functions. This avoids separate behavior for humans and models. Tool results must be structured and bounded. Errors must state whether the operation failed because no run exists, a run is already active, a checkpoint is not waiting, or the requested workflow cannot be found.
68
+
69
+ A tool call happens during an agent turn. The `start` action must validate and queue the launch, then deliver the first workflow prompt after the initiating turn settles. This prevents the new run from treating the initiating turn as a failed workflow step. It also prevents a workflow prompt from being injected in the middle of the tool call.
70
+
71
+ The headless RPC bridge keeps a submit-only version of the tool. Headless workflow children must not start or control other workflows. Its submit schema and parser still change to require `action: "submit"`.
72
+
73
+ ## Agent guidance
74
+
75
+ The tool description must tell the model when to use the built-in workflow:
76
+
77
+ - Use `start` with workflow `monitor` when the user asks to watch, monitor, poll, or check something repeatedly.
78
+ - Start directly when the task and interval are clear.
79
+ - Ask one short question when the target or interval is missing.
80
+ - Use observation-only behavior unless the user explicitly authorizes a mutation.
81
+ - Do not create repeated work without a user request or an existing workflow instruction that authorizes it.
82
+
83
+ The `list` result identifies `monitor` as a built-in workflow and gives a short description. This keeps discovery inside the tool instead of adding a skill or a system-prompt injection.
84
+
85
+ ## Built-in monitor workflow
86
+
87
+ Ship `monitor` as a built-in workflow in the Pi Workflows package. Built-ins have the lowest discovery precedence:
88
+
89
+ 1. Project workflows under `.pi/workflows/`
90
+ 2. Global workflows under `~/.pi/agent/workflows/`
91
+ 3. Workflows bundled with Pi Workflows
92
+
93
+ A project or global `monitor.workflow.ts` can therefore replace the default. The built-in remains a real workflow file so run bundles can record its path and source hash with the existing rules.
94
+
95
+ The workflow input contains:
96
+
97
+ - `task`: what to inspect.
98
+ - `everyMinutes`: the interval between checks.
99
+ - `reportWhen`: changes or states that deserve a user message.
100
+ - `stopWhen`: the condition that ends monitoring.
101
+ - `maxChecks`: a hard safety limit.
102
+
103
+ The model supplies these fields from the user's request. The workflow validates the input and applies documented bounds. The first release supports intervals from 1 minute through 24 hours. It defaults `maxChecks` to a documented finite value when the request gives no end limit.
104
+
105
+ The graph is:
106
+
107
+ ```text
108
+ prepare -> guard -> check
109
+ | continue quietly -> sleep -> guard
110
+ | continue and report -> report -> sleep -> guard
111
+ | stop quietly -> finish
112
+ | stop and report -> report-final -> finish
113
+ ```
114
+
115
+ `prepare` validates and normalizes the input. `guard` enforces `maxChecks`. `check` is an agent node that performs one observation and returns a validated route, a bounded observation, and an optional report. The next `check` can read the previous accepted `check` output, which Pi Workflows already keeps for looped nodes.
116
+
117
+ The report nodes write a normal assistant message and then submit an acknowledgement. Keeping reporting after accepted check output prevents the agent from showing a report before the structured result passes validation. The final presentation reports why the monitor stopped without repeating a report that the user already saw.
118
+
119
+ The sleep node uses the existing Pi Workflows shell action to launch the current Node executable with a timer. Set the node timeout above the largest supported interval because the engine default is 15 minutes. Set the shell execution timeout above the requested wait by a small fixed margin. Cancellation aborts the timer process immediately.
120
+
121
+ If the Pi TUI or standalone workflow host stops during sleep, Pi Workflows parks the run and kills the shell child. Resuming the run starts that sleep node again from the beginning. This is existing workflow behavior and is acceptable for this feature. No special timer persistence is added.
122
+
123
+ The workflow uses a high but finite `maxSteps` value as a second safety guard. Check and report values have explicit size limits so a long run cannot grow its bundle without bound.
124
+
125
+ ## Repository changes
126
+
127
+ Make the feature in `osolmaz/pi-workflows`:
128
+
129
+ - Refactor workflow lifecycle operations out of the slash-command handler.
130
+ - Expand the normal Pi `workflow` tool and update its tests.
131
+ - Update the RPC bridge submit contract.
132
+ - Add built-in workflow discovery with project and global override precedence.
133
+ - Add the built-in monitor workflow and focused tests.
134
+ - Update `README.md` and `docs/workflows.md`.
135
+
136
+ After the upstream change is complete, update the pinned Pi Workflows commit in OnurPi's thin `packages/workflows` wrapper. Do not add a new OnurPi extension or copy a monitor file into live global state.
137
+
138
+ ## State and API impact
139
+
140
+ - **Session state:** Normal workflow prompts, model replies, tool calls, and monitor reports are appended through Pi's normal session behavior.
141
+ - **Other persistent data:** No new data model. The feature uses existing run bundles and the existing workflow run queue.
142
+ - **Pi internals:** None.
143
+ - **Pi public API:** `registerTool`, `registerCommand`, `sendUserMessage`, and documented agent and session lifecycle events.
144
+ - **Pi Workflows API:** The workflow definition and run-state models do not change. The model-facing `workflow` tool contract changes, and discovery gains a lowest-priority built-in source.
145
+
146
+ ## Non-goals
147
+
148
+ This work does not add cron expressions, calendar schedules, a background service, controller resources, OS notifications, concurrent workflows in one session, resumable shell processes, or guaranteed wall-clock wake times across runner shutdowns. It does not let headless child agents recursively start workflows.
149
+
150
+ ## Acceptance criteria
151
+
152
+ - A user can ask for repeated monitoring in plain language, and the agent starts `monitor` through the existing `workflow` tool.
153
+ - The user does not type JSON or a slash command to start the monitor.
154
+ - The monitor checks immediately, sleeps for the requested interval, and checks again.
155
+ - An unchanged observation produces no normal assistant report.
156
+ - A matching report condition produces one concise assistant report.
157
+ - A matching stop condition reports as requested and ends the run.
158
+ - The safety limit ends a monitor that never reaches its stop condition.
159
+ - The model can list, start, inspect, pause, resume, cancel, answer, and submit through one tool.
160
+ - Slash commands and tool actions use the same lifecycle code.
161
+ - Starting from a model tool call does not cause an early nudge or record the initiating turn as the first workflow attempt.
162
+ - A 30-minute sleep is not stopped by the default 15-minute node timeout.
163
+ - Cancelling during sleep stops the shell child and ends the workflow.
164
+ - Project and global workflows override the built-in `monitor` name.
165
+ - Existing workflow, controller, host, viewer, and run-bundle tests continue to pass.
166
+ - OnurPi loads the updated wrapper and Pi starts successfully.
167
+
168
+ ## Verification
169
+
170
+ Add unit and integration coverage for tool action validation, lifecycle dispatch, deferred model-start launches, built-in discovery precedence, monitor routing, quiet checks, reports, stop conditions, safety limits, cancellation during sleep, and the headless submit bridge.
171
+
172
+ Run:
173
+
174
+ ```bash
175
+ npm run check
176
+ npm run test:e2e
177
+ npx slophammer-ts@latest dry .
178
+ npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
179
+ npx -y @simpledoc/simpledoc check
180
+ ```
181
+
182
+ Test the extension from the Pi Workflows checkout with `pi -e src/extension/index.ts`. Use a short test interval in a controlled fixture, then perform one manual 30-minute monitor run to confirm that the configured node timeout does not stop it. Test plain-language startup with the normal model, then test list, status, pause, resume, cancel, and checkpoint answer actions.
183
+
184
+ After updating OnurPi, run its full checks and start Pi with the installed OnurPi package. Confirm that the model sees one `workflow` tool, discovers `monitor`, and can start it from a plain-language request.
@@ -77,9 +77,9 @@ Consequences for readers:
77
77
 
78
78
  Large payloads are stored once, content-addressed, under `artifacts/` and
79
79
  referenced from the documents that use them. This applies uniformly to every
80
- **persisted value position**: `input`, `outputs.*`, `results.*.output`,
81
- `steps[*].prompt`, `steps[*].output`, `finalOutput`, and trace event payload
82
- values.
80
+ **persisted value position**. These positions include `input`, `outputs.*`,
81
+ `results.*.output`, `steps[*].prompt`, `steps[*].output`, and `finalOutput`.
82
+ Trace event payload values follow the same rule.
83
83
 
84
84
  Encoding rule, applied recursively to a persisted value:
85
85
 
@@ -109,9 +109,9 @@ Artifact rules:
109
109
  - Artifacts are immutable once written and deduplicate by content hash.
110
110
  - Readers must tolerate unknown `mediaType` values.
111
111
 
112
- Because the same output can legitimately appear in `outputs`, `results`,
113
- `steps`, and the trace, externalization makes that duplication cheap: each
114
- copy is the same small reference.
112
+ The same output can legitimately appear in several places. Externalization
113
+ keeps each copy in `outputs`, `results`, `steps`, or the trace as the same small
114
+ reference.
115
115
 
116
116
  ## manifest.json
117
117
 
@@ -154,6 +154,29 @@ validators are not serialized. Each node keeps only its metadata (`nodeType`,
154
154
  and edges are copied verbatim. The snapshot is what lets viewers draw all
155
155
  nodes, including ones that have not run yet. It is immutable after run start.
156
156
 
157
+ ## Resume and repair
158
+
159
+ An interrupted run (status `running` with no terminal trace event) can resume
160
+ instead of failing. Resume is a named operation with strict rules:
161
+
162
+ 1. The caller must hold the run's queue claim. Only the current claim holder
163
+ may resume or interrupt a bundle, and every bundle write verifies the
164
+ claim token first (write fencing).
165
+ 2. A torn trace tail (a crash mid-append) is truncated to the last complete
166
+ line. Trace events the state projection never recorded are dropped, so
167
+ `state.traceSeq` and the trace agree again before any new event.
168
+ 3. Completed nodes replay from the projection. The in-flight node reruns with
169
+ a fresh attempt; a `run_resumed` trace event marks the boundary.
170
+ 4. `state.workflowHash` pins the workflow source from run start. Resume
171
+ refuses a hash mismatch unless forced, and a forced resume records the
172
+ mismatch in the `run_resumed` payload.
173
+
174
+ Continuation runs (answering a checkpoint) are new bundles, not resumed ones.
175
+ They link back through `state.parentRunId`, carry the parent's outputs,
176
+ results, and step records forward, and note `continuedFrom` in their
177
+ `run_started` payload. Bundles stay append-only; a continuation is the only
178
+ way work follows a terminal `waiting` state.
179
+
157
180
  ## state.json
158
181
 
159
182
  The full run projection (`WorkflowRunState` in
@@ -177,10 +200,16 @@ The full run projection (`WorkflowRunState` in
177
200
  ```
178
201
 
179
202
  - `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
180
- or `cancelled`.
181
- - While a node is executing, `currentNode`, `currentAttemptId`,
182
- `currentNodeStartedAt`, and `statusDetail` describe it; they disappear when
183
- the node finishes. The executing node's type comes from the definition
203
+ or `cancelled`. A controller host records an abandoned bundle as `failed`
204
+ with a final `run_interrupted` trace event. Before doing that, recovery checks
205
+ the trace tail and repairs a stale projection when the terminal event was
206
+ already appended. The controller store can then retry a genuinely abandoned
207
+ child attempt without changing this schema. If startup stopped before a
208
+ manifest existed, the scheduler preserves that incomplete directory as a
209
+ hidden sibling before creating the reserved run.
210
+ - While a node is executing, `currentNode` and `currentAttemptId` identify it.
211
+ `currentNodeStartedAt` and `statusDetail` add timing and display context.
212
+ These fields disappear when the node finishes. The executing node's type comes from the definition
184
213
  snapshot, not from the state.
185
214
  - While a pause request holds the run at a step boundary, `paused` is `true`
186
215
  (with matching `run_paused`/`run_resumed` trace events); it disappears when
@@ -218,10 +247,9 @@ The full run projection (`WorkflowRunState` in
218
247
  produced by this attempt, from prompt delivery through accepted submission.
219
248
  Viewers must use this explicit linkage and never infer it heuristically.
220
249
  - Action steps carry an `action` receipt with `actionType`
221
- (`shell`/`function`) and, for shell actions, `command`, `args`, `cwd`,
222
- `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the
223
- step output (the parsed or raw shell result) and are externalized when
224
- large.
250
+ (`shell`/`function`). Shell actions also record `command`, `args`, `cwd`,
251
+ `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the step
252
+ output (the parsed or raw shell result) and are externalized when large.
225
253
  - When a run pauses at a checkpoint, `waitingOn` names the checkpoint node.
226
254
  Terminal runs carry `finalOutput` on success and `error` on failure.
227
255
 
@@ -248,8 +276,8 @@ One event per line, monotonically sequenced per run, schema
248
276
  ignore unknown event types and unknown payload fields so new ones can be added
249
277
  within the same schema version.
250
278
 
251
- The trace alone is sufficient to reconstruct the run: outputs and receipts are
252
- part of the terminal node events, not only of `state.json`.
279
+ The trace alone is sufficient to reconstruct the run because terminal node
280
+ events carry outputs and receipts.
253
281
 
254
282
  Event catalog and payload contracts:
255
283
 
@@ -268,6 +296,7 @@ Event catalog and payload contracts:
268
296
  | `run_failed` | run | `status`, `error` |
269
297
  | `run_timed_out` | run | `status`, `error` |
270
298
  | `run_cancelled` | run | `status`, `error?` |
299
+ | `run_interrupted` | run | `error` |
271
300
 
272
301
  Invariants:
273
302
 
@@ -283,6 +312,15 @@ Present when the run executed inside a Pi conversation. The extension records
283
312
  the conversation into the bundle so replay never depends on Pi's global
284
313
  session store.
285
314
 
315
+ A run that outlives its first session (parked, then resumed by another
316
+ session or the host) gains a second capture under
317
+ `session/segments/<attemptId>/` with the same file layout (`binding.json`,
318
+ `entries.ndjson`, `events.ndjson`, `capture.json`). The first capture stays
319
+ flat at `session/`; only captures from the second bind onward become
320
+ segments, so readers that predate segments keep working on single-session
321
+ bundles. An interrupted run finalizes any segment still `recording` as
322
+ `failed` with the interruption reason.
323
+
286
324
  ### binding.json
287
325
 
288
326
  Written once when the run binds to the conversation
@@ -325,22 +363,23 @@ while the run was active, schema `pi-workflows.session-entry.v1`:
325
363
  - `entry` is the verbatim Pi session entry (Pi's own versioned format),
326
364
  including user messages, assistant messages, tool results, model changes,
327
365
  and compaction entries. Nothing is normalized or rewritten.
328
- - Entries include everything that happened in the conversation during the run:
329
- workflow prompts, nudges, and user interruptions are all part of the record.
366
+ - Entries include everything that happened in the conversation during the run.
367
+ This includes workflow prompts and nudges together with user interruptions.
330
368
  - `conversation` ranges in step records and `node_finished` events address
331
369
  entries by Pi entry id (`entry.id`).
332
370
 
333
371
  ### events.ndjson
334
372
 
335
- The temporal journal records documented Pi `turn_*`, `message_*`, and
373
+ The temporal journal records documented Pi `turn_*` and `message_*` hooks plus
336
374
  `tool_execution_*` hooks with schema `pi-workflows.session-event.v1`. Each
337
- record has a per-file `seq`, timestamp, `nodeId`, `attemptId`, optional
338
- `turnId`, `messageId`, and `toolCallId`, a normalized `type`, and `payload`.
375
+ record has a per-file `seq`, timestamp, `nodeId`, and `attemptId`. Optional
376
+ turn, message, and tool call IDs link related records. A normalized `type` and
377
+ `payload` carry the event data.
339
378
  The full contract and event catalog are in
340
379
  [session-event-journal.md](session-event-journal.md).
341
380
 
342
- Events preserve semantic deltas. Assistant `partial`, terminal `message`, and
343
- terminal `error` snapshots are never stored. Tool update records omit Pi's
381
+ Events preserve semantic deltas. Assistant `partial` snapshots are never
382
+ stored, and neither are terminal `message` or `error` snapshots. Tool update records omit Pi's
344
383
  cumulative `partialResult`. Final `message_finished` records link to settled
345
384
  Pi entries with `entryId`; after that linkage, `entries.ndjson` is the
346
385
  verbatim content authority.
@@ -364,10 +403,10 @@ complete lines, sequence gaps, and terminal torn tails are integrity failures.
364
403
  }
365
404
  ```
366
405
 
367
- `status` is `recording`, `complete`, or `failed`. Failed capture adds
368
- `failure` with `failedAt`, `code`, and `message`. Capture failure is visible to
369
- readers but does not fail the workflow. Terminal readers verify the counts,
370
- last sequence, schemas, and contiguous event sequence. Missing temporal files
406
+ `status` starts as `recording` and ends as `complete` or `failed`. Failed
407
+ capture adds `failure` with `failedAt` plus a code and message. Capture failure
408
+ is visible to readers but does not fail the workflow. Terminal readers verify
409
+ the counts and last sequence, then check schemas and contiguous event order. Missing temporal files
371
410
  in a session-bound bundle are invalid, not an older supported layout.
372
411
 
373
412
  ## Versioning