@alphacoder-v0/pi-loops 0.22.8 → 0.24.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 (91) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +91 -63
  4. package/README.zh-CN.md +59 -39
  5. package/docs/cli.md +272 -83
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +395 -56
  8. package/docs/downstream.md +2 -2
  9. package/docs/goal.md +12 -3
  10. package/docs/hooks.md +6 -7
  11. package/docs/loops.md +288 -59
  12. package/docs/mcp.md +22 -14
  13. package/docs/recipes.md +6 -0
  14. package/docs/screenshot.png +0 -0
  15. package/docs/triggers.md +120 -26
  16. package/docs/troubleshooting.md +37 -26
  17. package/docs/web-ui-parity.md +691 -65
  18. package/package.json +4 -3
  19. package/recipes/autoresearch/recipe.toml +1 -0
  20. package/recipes/changelog-draft/recipe.toml +1 -0
  21. package/recipes/ci-sweeper/recipe.toml +1 -0
  22. package/recipes/daily-digest/recipe.toml +1 -0
  23. package/recipes/deps-sweeper/recipe.toml +1 -0
  24. package/recipes/ecosystem/recipe.toml +1 -0
  25. package/recipes/issue-loop/recipe.toml +1 -0
  26. package/recipes/pr-watch/recipe.toml +1 -0
  27. package/skills/pi-loops/SKILL.md +6 -4
  28. package/src/activity.ts +199 -0
  29. package/src/archive.ts +21 -9
  30. package/src/args.ts +6 -2
  31. package/src/cli-entry.mjs +12 -3
  32. package/src/cli.ts +746 -125
  33. package/src/config.ts +53 -5
  34. package/src/conversation-rows.ts +469 -0
  35. package/src/danger.ts +227 -6
  36. package/src/engine.ts +1240 -0
  37. package/src/goal.ts +71 -0
  38. package/src/held-read.ts +27 -0
  39. package/src/hooks.ts +49 -18
  40. package/src/host-control-channel.ts +455 -40
  41. package/src/host-control.ts +602 -111
  42. package/src/host-entry.mjs +5 -3
  43. package/src/host-runtime.ts +455 -42
  44. package/src/host.ts +659 -123
  45. package/src/inbox-card.ts +278 -0
  46. package/src/inbox.ts +139 -62
  47. package/src/job-edit.ts +7 -1
  48. package/src/job-health.ts +14 -2
  49. package/src/job-lines.ts +201 -13
  50. package/src/job-owner.ts +74 -13
  51. package/src/job-tune.ts +110 -0
  52. package/src/lease.ts +537 -0
  53. package/src/lock.ts +7 -2
  54. package/src/mcp-pool.ts +66 -14
  55. package/src/mcp.ts +1 -1
  56. package/src/open-file.ts +54 -0
  57. package/src/page-children.ts +1507 -0
  58. package/src/page-devices.ts +192 -0
  59. package/src/page-server.ts +3150 -0
  60. package/src/path-precheck.ts +215 -0
  61. package/src/paths.ts +24 -0
  62. package/src/pi-floor.ts +10 -0
  63. package/src/pi-loops.ts +1301 -450
  64. package/src/pi-resolver.d.mts +1 -0
  65. package/src/pi-resolver.mjs +110 -10
  66. package/src/presence.ts +12 -63
  67. package/src/protocol.ts +10 -1
  68. package/src/recipe.ts +43 -1
  69. package/src/requests.ts +354 -0
  70. package/src/rule-lines.ts +68 -0
  71. package/src/run-card.ts +105 -0
  72. package/src/runner.ts +18 -1
  73. package/src/schedule.ts +1 -2
  74. package/src/scheduler.ts +595 -245
  75. package/src/sdk-runner.ts +321 -27
  76. package/src/seen.ts +278 -0
  77. package/src/session-head.ts +21 -0
  78. package/src/share.ts +12 -2
  79. package/src/snapshot.ts +3 -2
  80. package/src/store.ts +81 -8
  81. package/src/tools.ts +196 -65
  82. package/src/trigger-runtime.ts +164 -127
  83. package/src/triggers.ts +39 -7
  84. package/src/trust.ts +46 -2
  85. package/src/ui-prefs.ts +16 -5
  86. package/src/watch-plan.ts +174 -0
  87. package/src/web.d.mts +3 -0
  88. package/src/web.mjs +2260 -2340
  89. package/src/window-executor.ts +459 -0
  90. package/src/window-guard.ts +158 -0
  91. package/docs/agents/issue-tracker.md +0 -34
package/CHANGELOG.md CHANGED
@@ -5,6 +5,670 @@ All notable changes to pi-loops are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.24.0] - 2026-11-09
9
+
10
+ ### Upgrade notes
11
+
12
+ Read these before you upgrade from 0.22.x. 0.23.0 was tagged but never published to npm, so read its
13
+ notes below too: everything in it ships in 0.24.0.
14
+
15
+ - **Pair each browser again, once.** The first start of 0.24 replaces the page token that 0.22.8
16
+ wrote: 0.22.8 kept it in a cookie that every port on 127.0.0.1 receives. Old `?token=` bookmarks
17
+ stop working. Open the page with `pi-loops`, or enter a pairing code, once on each browser. If
18
+ you go back to 0.22.8 afterwards, it puts the token in that cookie again; delete `web-token` and
19
+ `web-token.v024` before you upgrade again to get a fresh one.
20
+ - **One page, served by pi-loops in the background, at `127.0.0.1:4173`.** It replaces the page that
21
+ `pi-loops --web` served and the status page on 4174, whose content is now the page's Background
22
+ view. `pi-loops` opens the page and exits; Ctrl-C no longer ends anything. `--no-auth` is gone.
23
+ When `pi-loops` opens the page, `--port`, `--host`, `--allow-host` and `--resume` exit 2 with the
24
+ `[page]` lines in `config.toml` that replace them; with `--tui` they go to pi as before.
25
+ `PI_LOOPS_PAGE_PORT` overrides the port.
26
+ - **Each conversation open in the page is one `pi --mode rpc` that pi-loops in the background runs.**
27
+ It stops when idle or at the cap, and uses about 116 MB while idle. `pi-loops restart` and an
28
+ upgrade wait up to two minutes for a turn in the page to finish; `pi-loops stop`, or pi-loops in
29
+ the background crashing, ends it at once. `[page] port` now applies to any loops directory, not
30
+ only the default one.
31
+ - **A loop's runs no longer write into the conversation.** A loop's first three runs and every failed
32
+ run still put a one-line card in the conversation that made it; the others are in the terminal's
33
+ Runs panel, the page's activity strip and `/cron runs`. A loop carried over from 0.22.8 keeps its
34
+ run count, so one that has already run three times shows cards for failures only. A run you ask
35
+ for (`/cron run <loop>`, or the run button in the page) always puts a card, with the first line of
36
+ its reply, in the conversation you asked from.
37
+ - **Every matched rule's result is a finding in the inbox, except a repeat.** In 0.22.8 a result kept
38
+ out of the conversation (`promote_to_chat = false`) went only to the audit log. Now every match is
39
+ one finding, and a result shown in the conversation is a finding too until you dismiss it. A result
40
+ with the same text as the newest finding from the same rule that is still new is not filed again. The choice
41
+ "Keep it out of this conversation" is now called "Put it in my inbox".
42
+ - **Stop pi-loops before you uninstall.** Run `pi-loops stop` before `pi remove`. pi-loops runs in
43
+ the background, and removing the extension does not stop it: its loops keep running, and each run
44
+ costs a model turn. The README's Uninstall section has the steps.
45
+ - **A project's `hooks.toml` runs for loops only where pi has saved trust.** Loops run in the
46
+ background, and there a project's own hooks load only for a directory pi trusts (`/trust` in a pi
47
+ opened there), as its settings, extensions and `.pi/mcp.toml` already do.
48
+ - **New files in the loops directory:** `pages/<session>.json`, `pages/open/` (0600), and
49
+ `web-token.v024`. `seen.json` moves to one cursor per project; its old cursors are folded in once.
50
+ A job installed before its conversation was saved carries `owner: "project"` in `jobs.json`.
51
+ - **An unattended run that really runs `pi-loops stop`, `abort` or `restart` is refused.** That
52
+ includes running it through `sh -c`, `ssh` or `docker exec`. A quoted message that only mentions
53
+ it, such as a `git commit -m`, is allowed.
54
+ - **`PI_WEB_TOKEN` is gone.** pi-loops in the background serves the page and reads its token from
55
+ `web-token` in the loops directory only; the variable is ignored. To keep a token you chose, write
56
+ it into that file (letters, digits, `-` and `_`, at least 8 of them) and run `pi-loops restart`;
57
+ a new token signs every paired device out.
58
+
59
+ - **A scheduled prompt in an open window asks before a dangerous command or an undeclared tool.**
60
+ Until now a plain job's prompt ran under the job's `--tools` and the dangerous-command policy only
61
+ in the background; in an open window it ran as your own message would. Now the same turn in a
62
+ window asks, with a 60-second countdown, before a command the policy refuses (`rm -rf` on an
63
+ absolute path, `sudo`, `git push --force` on main, and the rest of the corpus) or a tool outside
64
+ the job's `--tools`. The question has two answers, `No, refuse it` focused and `Yes, run it once`
65
+ under it, so Enter refuses: pi's Yes/No confirm starts on Yes, and a reflexive Enter ran the
66
+ command. The yes answer runs it this once; anything else, Escape or no answer refuses it with the
67
+ wording the background uses, and the model is told to do the safe part, and whether you refused
68
+ or nobody answered. pi-loops' own tools are asked about once at most: `cron_create` in such a turn
69
+ makes a job capped to the turn's tools, as a job a background run makes is capped to that run's,
70
+ and `new_trigger`, the trigger tools, `cron_remove` and `set_cron_job_state` ask through their own
71
+ confirm, with the same two answers and the same minute, where the background would refuse. A
72
+ message you send into that turn makes the rest of it yours. The turn an MCP server's
73
+ `inject_and_run` push starts is capped the same way, under the dangerous-command policy alone: in
74
+ 0.22.8 it ran as your own message would. `[engine] window_guard = false` in `config.toml` restores
75
+ the uncapped window.
76
+ - **`cron_create` and `new_trigger` ask you first, in a terminal too.** With you present, in your own
77
+ turn, both tools show one question before they create anything: pi's selector in a terminal, a
78
+ card in the browser page. It says what would run and offers where the result goes, and, for a
79
+ rule, whether it repeats; for a loop, whether a checker reviews each finding. The tool returns
80
+ `created …` or `cancelled: …` in the same call. In 0.22.8 `cron_create` created at once and
81
+ `new_trigger` asked a `Control-plane approval required` confirm; that confirm is replaced, not
82
+ added to. The turn waits for your answer up to thirty minutes, then nothing is created. A
83
+ scheduled turn, a sub-agent and pi-loops in the background ask nobody, as before. A `cwd` outside
84
+ the project is refused before the question; `/cron add --cwd` still sets one up elsewhere.
85
+ - **`pi-loops host stop` now pauses everything until `pi-loops start`.** In 0.22.8 it stopped the
86
+ running process and recorded nothing, and with nothing running it exited 1. Since 0.23 it is
87
+ `pi-loops stop`: it exits 0, records the stop, and every loop, every rule and the plain jobs of
88
+ closed conversations wait until `pi-loops start`. The command now says so when you use this
89
+ spelling.
90
+ - **`PI_LOOPS_HOST=1` no longer makes a `pi -p` run the timer.** In 0.22.8 it let a `pi -p` run
91
+ host the timer for as long as it lived. Loops now run in pi-loops in the background; with the
92
+ variable set, a `pi -p` run only starts pi-loops in the background when it is not running, and that
93
+ process outlives the run. The `pi -p` run's own clock still runs its conversation's plain jobs'
94
+ slots, but it delivers no scheduled prompt itself: the prompts wait in the queue for a window or
95
+ pi-loops in the background.
96
+ - **`--tools` naming a built-in tool this chat does not have says so.** 0.22 stored the list as
97
+ typed; a loop made in a chat without `bash` then ran with `bash` in the background. Since 0.23
98
+ the undeclared tool was dropped without a word, and `/cron add` could print `the tools ` with
99
+ nothing after it. The tool is still dropped, so a job never gets more than its chat has and a
100
+ recipe's install does not fail on it, and `/cron add` and `/recipe add` now print
101
+ `dropped from --tools: bash (this chat does not have it)`.
102
+
103
+ ### Fixed
104
+
105
+ - **`pi-loops` finds a pi that pi's own installer manages.** pi 1.5 installs itself under
106
+ `~/.pi/agent/install/`; `pi-loops` and pi-loops in the background looked only beside node and in
107
+ npm's global directory, and stopped with "no pi package is there at all".
108
+ - **`--help` and `-h` never run a command.** `pi-loops <command> --help` prints that command's
109
+ usage and changes nothing: `upgrade --help` used to run the upgrade, and `install-launcher --help`
110
+ wrote the launcher. `pi-loops -h` is `pi-loops --help`.
111
+ - **`[page] port` applies to any loops directory,** and `pi-loops` no longer names port 4173 for a
112
+ directory whose page is on another.
113
+ - **Set it up or Cancel closes the Watch for… box,** so the composer is free again, and the page
114
+ shows the new loop or rule at once.
115
+ - **A finished run is carded once.** Run records with mixed time-zone offsets were carded again on
116
+ every restart.
117
+ - **Two claims of one finding at the same moment no longer both succeed.**
118
+ - **A reason you are typing on a decision card keeps its text across the page's redraw.**
119
+ Found by a review of this release against 0.22.8 and 0.23.0; none of these had shipped.
120
+
121
+ - **A question in the browser closes when its countdown ends.** pi refuses a timed dialog on its own
122
+ and tells the page nothing, so the page kept the question open, held the next one back, and
123
+ printed "approved" for an answer nobody read. Now the dialog closes at the end of its time with
124
+ `… → timed out with no answer: refused`, a page that attaches later is not offered it, one offered
125
+ again carries only the time it has left, and an approve after the time is up is neither sent nor
126
+ reported as approved.
127
+ - **"queued N ✕" in the browser clears all N.** It cancelled only your own messages and pi's queue,
128
+ so with two scheduled prompts queued the count stayed 2. Each row's ✕ is unchanged.
129
+ - **A dead letter in the inbox says why it was not delivered.** A plain job's prompt, a claim, a
130
+ goal continuation or a push that could not be delivered became a finding with the prompt alone,
131
+ and `/inbox claim` ran it again with nobody told it had failed. Now it reads
132
+ `could not be delivered (<why>): <prompt>`. A rule's result is the finding it always was.
133
+ - **A window standing aside no longer names its own pid as another window.** `/cron` marked its jobs
134
+ `open in pid <this pid>` and `/cron run` said to run the job there. Both now say that this window
135
+ shows an older copy of the conversation and to `/resume` it.
136
+ - **`cron_create` tells the agent what a plain job's turns may do.** Its result, and the skill, said
137
+ only that the job runs in the background. They now say that the turns run with the chat's
138
+ built-in tools under the dangerous-command guard, ask in an open window and are refused in the
139
+ background, and run there without the window's `-e` extensions or flags.
140
+ - **A hand-over pi refused no longer stops a plain job until your next turn.** pi swallows a
141
+ refused `sendUserMessage` (no credentials, no model, another extension's input handler taking
142
+ the text) and starts no turn, so the queued prompt stayed "handed to pi" for ever and every later
143
+ slot was skipped onto it. Now a prompt handed to an idle pi that starts no run within ten seconds
144
+ of pi's last input event counts a failed attempt, backs off a minute, then five, and is a finding
145
+ in the inbox at the third refusal, like any failed row; a slot due while a refused prompt waits
146
+ replaces it rather than being skipped onto it, so one refusal costs one slot, as it did in 0.22.8.
147
+ `/cron` shows the reason (`pi did not start a turn for it; backing off until …`). The same holds
148
+ for a `/goal` continuation, a claim and an `inject_and_run` push. A message you sent from the
149
+ browser keeps its behaviour: judged once, handed back to the composer.
150
+ - **A `/cron run` left queued when you quit no longer stops the job's background schedule.** The
151
+ background runs no attended row, and it used to wait for that one instead of running the clock's
152
+ slots behind it. Now it runs them as if the row were not there; your `/cron run` waits for a
153
+ window.
154
+ - **A damaged `jobs.json` costs only the plain-job rows, not the whole queue.** A `jobs.json` that
155
+ cannot be read (empty after a torn write, written by a newer pi-loops) made every drain of the
156
+ conversation throw, so your browser messages and rule results were not delivered either. Now the
157
+ rows that name a job wait, with `cannot read the jobs: …` as the reason, and the rest drain.
158
+ - **`/cron run` is your own turn.** Its row was capped as a scheduled turn, so the window asked
159
+ before `git push --force` or `sudo` in a prompt you typed the command for, and after Esc it
160
+ waited out the minute like the clock's rows. Now the row is recorded as this window's, runs
161
+ attended, and goes before the clock's rows, as a typed message does.
162
+ - **A disabled job's queued prompt no longer runs when a window opens the conversation.** The
163
+ background clock already skipped a disabled job; the window handed its waiting prompt over anyway,
164
+ including a job parked after three interrupted held turns. Now a `cron:` row of a disabled job
165
+ waits (`its job is disabled`) until the job is enabled again; a `/cron run` row of a job disabled
166
+ since still runs, because you asked for it.
167
+ - **A recipe's merge-conflict hand-off is no longer dropped while its tracker description waits.**
168
+ Both collapsed on one key per recipe, so `/recipe update` said "handed to the agent to resolve
169
+ with you" and nothing was queued. Each kind of hand-off now has its own key, and when one does
170
+ collapse onto an earlier one of the same kind the line says the agent is already queued for it.
171
+ - **A prompt an input extension rewrites runs at most three times.** An `input` handler that
172
+ prepends text to extension-sourced messages removed the `[Trigger <id>] ` proof, so the row was
173
+ never done and ran again at every settle, uncapped, for ever. Each re-offer of a row pi dropped
174
+ unwritten now counts an attempt (the third is a dead letter), and a prefix-less user message that
175
+ starts right after this window handed a pi-loops row over is that row's turn, capped as the row;
176
+ your own pending input still wins.
177
+ - **A send pi refused comes back to the browser composer when the page rings, not up to 30 s
178
+ later.** The drain the page's follow-up ring causes may now judge a refused send, as the tick may.
179
+ - **A refused judgement counts idle time from pi's last `input` or `message_start`, not from the
180
+ hand-over, and the grace is ten seconds.** pi's pre-run phase (other extensions' input handlers,
181
+ the credential check, which can go to the network) reports nothing to an extension; five seconds
182
+ from the hand-over could fail a send pi then ran, so the person saw an error and an answer.
183
+ - **A window no longer remembers every row it ever produced.** The record is pruned with the request
184
+ file; a 1-minute job in a window open for a month kept 43k entries.
185
+ - **The background process never files a browser message in the inbox.** A person row for a
186
+ conversation it cannot find is removed and audited `stale`; design §5 says a person row never
187
+ goes to the inbox, and this one did.
188
+ - **The window guard judges self-scheduling playbooks as the background does.** `cron_create`
189
+ inside a scheduled turn is not asked about; the job it makes is capped to the turn's tools.
190
+ `new_trigger`, `remove_trigger`, `set_trigger_state`, `cron_remove` and `set_cron_job_state`
191
+ are not asked a second time by the guard: they confirm themselves, once. The model is told
192
+ "nobody answered" only when nobody did; your No says you refused.
193
+ - **The upgrade notice counts the plain jobs `/cron` says run in the background.** It counted
194
+ every plain job with a conversation, while `/cron` marked those of never-reopened conversations
195
+ `resume it to run`.
196
+ - **A browser message is answered when the pi holding the session cannot answer it.** A terminal
197
+ that held the session but stood aside (its copy of the conversation behind the file), or a one-shot
198
+ `pi -p` run, kept the lease and handed nothing over; the page said "answered there" and the message
199
+ waited for ever. Such a holder now says so on its lease, the page's own pi runs the page's message
200
+ in its place, and the page does not call it answered elsewhere. Whatever the cause (a terminal
201
+ suspended with Ctrl-Z, a pi that is not draining), a message of yours nobody took in fifteen
202
+ seconds while pi is idle is listed as `not taken by pi N (why)` with `send it here instead`, which
203
+ takes it out of the queue and sends it to the page's pi over rpc, as 0.22 did. When the page's own
204
+ pi stands aside, the page says so and `/resume`, and offers nothing that would branch the
205
+ conversation. A terminal that holds the session and delivers, but has not taken the message, is not
206
+ called unanswering: the row says `pi N holds this conversation and has not taken it yet (it may be
207
+ busy)`, and its one offer reads `answer here instead (a branch pi N will not see)`. Send-here takes
208
+ the row out under the queue's lock only while no holder has stamped it handed over, and a drain
209
+ whose stamp finds no row hands nothing over, so a message is never answered twice; a row pi
210
+ already has answers `already handed to pi`.
211
+ - **A page whose pi has no pi-loops in it chats as before.** With the extension not loaded (a pi
212
+ older than the floor, a load error, extensions off) the page wrote a row nobody would drain and
213
+ sent `/pi-loops ring <id> <fingerprint>`, which pi handed to the model as your message. Chat text
214
+ now goes to pi over rpc, no row is written, and the queue the page shows is pi's own.
215
+ - **One request lock per conversation.** One `requests.lock` served every session, and a process
216
+ killed inside a rewrite left it for the ten-second stale wait, during which every writer of every
217
+ session (each window, the page, the background process) spun with its event loop blocked. Each
218
+ session's queue has its own lock now, so a lock left behind costs that one conversation's writers,
219
+ and nobody else. A `requests.lock` an older version left is ignored.
220
+ - **Image bytes stay out of the queue file.** A browser message's images were written into the
221
+ row, so three screenshots made a 90 MB queue file, parsed at every listing and rewritten and synced
222
+ under the lock at every update (3 s on a fast disk). The bytes are a file beside the queue now, one
223
+ per row, removed with the row; the row carries each image's type and digest, the fingerprint
224
+ covers the digests, and bytes that do not match are refused rather than sent. An append that fails
225
+ takes its image file back, and pi-loops sweeps image files no row names once they are a minute old.
226
+ - **A page whose pi reloaded its extensions routes on the command list as it is now.** `/reload` in
227
+ the page can unload pi-loops; the page routed chat on the list it fetched at startup. The list is
228
+ asked for again after every slash command, and when the cached one is older than thirty seconds.
229
+ - **A queued row the background cannot run no longer costs a read of the conversation every tick.**
230
+ An attended row waiting for a window made pi-loops parse the whole session file (58 ms for 20 MB,
231
+ event loop blocked) on every tick to decide again that nothing had changed. It remembers the
232
+ file's size and mtime, jobs.json's, and the queue's content at the last such decision, and reads
233
+ again only when one of them changes or a backoff ends. A window drains once per burst of request
234
+ file events instead of once per event.
235
+ - **A plain job's slot lost to another process leaves no row behind.** The row is written before
236
+ the stamps' compare-and-swap; when the swap lost (a pi-loops 0.22.8 window firing the same slot
237
+ during an upgrade), the row stayed and the prompt ran twice. The row this call appended is removed.
238
+
239
+ Found by a four-angle review of this release's user journeys against 0.22.8 and 0.23.0 (owner
240
+ decisions of 2026-11-02); none of these had shipped either.
241
+
242
+ - **A rule's result reaches the window you are working in, as in 0.22.8.** 0.23 appended it to the
243
+ closed conversation that created the rule, so in a new conversation of the same project the agent
244
+ never saw it. Now it goes to the creating conversation while a window holds it; else to the
245
+ conversation a window holds in the rule's project (lowest pid first, as 0.22.8 chose its owner);
246
+ else to the closed creating conversation; and to the inbox only when no conversation can take it.
247
+ A rule with no creating conversation (imported, or made by a loop's run) starts at the project's
248
+ open window.
249
+ - **`[host] auto = false` means what it meant in 0.22.8.** Loops run only while a pi window is open:
250
+ the first window starts pi-loops, `/cron run <loop>` and rule checks work in that window, and
251
+ pi-loops leaves once the last window has closed, unless you started it by name (`pi-loops start`,
252
+ `/pi-loops start`), which keeps it until `pi-loops stop`. 0.23 read the setting as "never start", which
253
+ paused every loop until `/pi-loops start` ([configuration.md](docs/configuration.md#host-auto)).
254
+ - **A slot collapsed onto a queued prompt sets no `last error`.** `skipped overlaps` counts it; the
255
+ error outlived the prompt it was about and made `cron_list` read a healthy job as failing.
256
+ - **A plain job's turn in a closed conversation is the conversation's own.** It runs on the
257
+ conversation's model (a `model` a 0.22 chat stored on the job was never used, and is ignored), its
258
+ tools act for the conversation (`cron_create stateful:false` binds to it, `cron_list all_projects`
259
+ lists the machine), and a control-plane confirm there is refused with a sentence that says the turn
260
+ ran in the background with nobody to ask, not "sub-agent".
261
+ - **A 0.22 plain job's `--tools` can be cleared.** The list was inert then and caps the job's turns
262
+ now; `/cron set <job> --tools -` clears it (`--tools a,b` sets one), and the first session start
263
+ names such jobs once, with that command.
264
+ - **The first-run notice counts every conversation's plain jobs, and leaving a conversation says
265
+ what goes on running.** The notice counted this conversation's jobs alone and was then spent;
266
+ `/new` and `/resume` said nothing, where in 0.22.8 leaving a conversation was how you stopped its
267
+ jobs. The next session start now says what the conversation you left has running, and how to stop one.
268
+
269
+ ### Added
270
+
271
+ - **A conversation list on the page.** A project switcher, `All / Needs you / Unread`, who holds each
272
+ conversation (`terminal`, `open`, `pi-loops`), a loops rail and a status bar; on a phone, Inbox,
273
+ Chat and Loops tabs. "+ New" starts a conversation in the project the list shows.
274
+ - **Inbox cards with actions.** A decision has `Yes, do it`, `No…` with reasons, and `See the run`; a
275
+ piece of news has `Look into it`, `Dismiss…` and `See the run`. A decision from another project
276
+ runs in a new conversation in that project. The terminal's `/inbox` and `pi-loops inbox list` show
277
+ the same split.
278
+ - **Watch for something from one sentence.** The agent's `cron_create` and `new_trigger` ask you
279
+ first, as a card in the page and pi's selector in a terminal, and only your answer creates the loop
280
+ or rule. The card asks where the result goes and, for a rule, whether it repeats. A question
281
+ nobody answers is cancelled after 30 minutes.
282
+ - **An away card.** "Since … · N runs (M failed) · K findings · $X", with `Show what needs me` and
283
+ `Mark all seen`, counted per project; the terminal's session-start line uses the same numbers.
284
+ - **Starters in an empty project.** daily-digest, pr-watch and changelog-draft, each one click, each
285
+ installed at `--level report`.
286
+ - **A loop activity strip, counts in the tab title, and notifications you opt into.** Notifications
287
+ come only while the tab is hidden: a new decision, a third failure in a row, the daily budget.
288
+ - **Stop a running loop from the page, and one tuning suggestion per loop** ("Run hourly instead" or
289
+ "Pause", or keep it as it is).
290
+ - **Rules that only wait for a path cost nothing until the path exists.** A rule made in 0.24 that
291
+ waits for a file checks the file first and calls the model only when it is there; the rule's row
292
+ says so. Rules made before keep the model check.
293
+ - **The page from a phone.** `pi-loops` prints a one-use address valid for 10 minutes, never the
294
+ token; a phone on your tailnet opens the same page.
295
+ - **`/cron queue [cancel <n|id>|clear]`, and `queued N` on the status line.** What waits for this
296
+ conversation (a plain job's prompt, a claim, a rule's result), numbered, with its source, age and
297
+ text; cancel one by number or id, or clear them all, each cancel audited as a `request` row.
298
+
299
+ - **`/cron` and `cron_list` say when a plain job's prompt is waiting in its conversation's queue.**
300
+ A job whose prompt sits in `requests/<session>.jsonl` (pi busy, after Esc, behind another turn)
301
+ looked exactly like one that is not queued. `/cron` now marks it `[queued <how long>]`, with the
302
+ reason when the queue knows one (`backing off until <when>` after a failed hand-over), and
303
+ `cron_list` adds a `queued` field (`since`, and `reason` when there is one) to such a job and a
304
+ `queued:` line to its text. Every other line and field is as it was.
305
+ - **`pi-loops status` and the status page list the request queues.** After its usual lines, `status`
306
+ prints `requests: N queued in M session(s)` and one line per conversation: the session id, the
307
+ count, how long the oldest row has waited, `dead: K` for rows at the dead-letter limit, and who
308
+ would run them (`in pi <pid>`, `in the background`, `nobody holds it`). It reads the files, so it
309
+ says this with pi-loops down too, where it used to say only that nothing was running. The status
310
+ page shows the same lines under "Requests". No row text is printed.
311
+
312
+ ### Changed
313
+
314
+ - **Text the layout already says is gone.**
315
+ - The page: the empty conversation's tagline, the hint row under the composer (the conversation's
316
+ id is the last line of the ⋯ sheet, and ⋯ shows at every width), a starter's meta line and "or
317
+ describe a loop" (a starter is its name, a `≈ $N/day` chip, the sentence its recipe's new `line`
318
+ key gives, whole, and Add), the Watch for…
319
+ box's label and caption (title *Watch for…*, three examples), the plan card's "Not right?" line
320
+ (its notes are rows), and on an inbox card the `not said` rows, a greyed See the run, the loop's
321
+ name when only one loop is listed, and where a claim runs unless that is not here.
322
+ - The terminal: `/inbox` says `inbox: empty` and has no footer; the widget drops `Runtime` and
323
+ shows `Hooks` only when there are hooks.
324
+ - The command line: `pi-loops --help` is one line and one sentence per command (545 words to under
325
+ 200); the launch line "the page stays open when this terminal closes" is gone; `pi-loops status`
326
+ says `started by …` and drops its two parentheticals.
327
+ - **Words a person was never meant to meet are gone.** The Background view says conversation, where
328
+ it is open (a terminal, this page, the background), loops, rules and waiting, in the page's own
329
+ time format, with no pid, "spawned", "held by" or "mailbox"; `background.json` is unchanged. A
330
+ message or a Watch for… sentence that a terminal answers, or that waits for this page to read the
331
+ conversation again, says so with no pid. The
332
+ status bar says "1 conversation open in a terminal". `/cron` says a plain job's conversation is
333
+ "open in a terminal", "in the page" or "in the background", with no pid, and `/cron scheduler`,
334
+ `/cron queue` and a claim's card no longer say lease, held by pid, request or page slot.
335
+ `pi-loops status` says "3 messages waiting for 2 conversations, 1 could not be delivered".
336
+ `/cron host` still works and is no longer listed. The README says it in plain words. The
337
+ terminal's notices and `/cron run`'s refusal say where a conversation is open in the same words
338
+ and name no pid; only `/cron scheduler`, `/pi-loops status` and `pi-loops status` do. They count
339
+ "1 loop", "2 loops" or "2 plain jobs", never "job(s)", and so does the badge ("2 loops failing").
340
+ - **One fact in one place.** The status bar is the one place that says pi-loops runs (or why loops
341
+ are paused) and the one link to the Background view, under the Loops pane on a phone; the Loops
342
+ pane's pi-loops line and link and the rail's Background icon are gone. The activity strip is the
343
+ one place for runs: the Loops pane's "last run" line and the away card's run count are gone, and
344
+ the away card shows when something was found. It, and the terminal's line at session start, say
345
+ "N from this conversation" only when that is not every finding. The Loops pane drops its summary line and says
346
+ "loops", not "job(s)". A loop row keeps ▸, its name and run on its first line, says `runs N` only
347
+ once it has run, and shows its 30-day numbers only when one is not zero, and then only those
348
+ (`3 findings in 30 days · 1 claimed`); `/cron` keeps its `30d:` text. A project with no loop
349
+ has an empty Loops pane and no loop count in the status bar. The away card's Show what needs me is gone (the Needs you tab and heading
350
+ do it), and the list is called Inbox on the desktop rail as on the phone. Every time on the page
351
+ is in one format, the away card's 24-hour words. A loop is labelled the same everywhere but
352
+ `/cron`, `cron_list` and `inbox --json` (its name, else `cron-` and 8 hex digits), the terminal's
353
+ `/inbox` included; a run card
354
+ drops its leading `cron`. The page says a schedule as the terminal does (`every 1d`). The page
355
+ server asks `src/paths.ts`, `src/schedule.ts` and `src/job-owner.ts` instead of keeping copies.
356
+ - **The README's picture is the 0.24 page:** a decision and two findings beside a conversation.
357
+ - **A stop you make spends the run's slot.** A stopped one-shot loop is turned off. Stops for quit,
358
+ budget or preemption still give the slot back.
359
+ - **"Since you looked" is counted per project,** and includes findings from loops a recipe made.
360
+ - **"Last run" shows 24-hour time.**
361
+ - **A chat message from the browser page is a request, in one queue with what pi-loops addressed to
362
+ the conversation.** Sending from the page writes a `person` row to `requests/<session>.jsonl` and
363
+ rings the pi behind the page (`/pi-loops ring <id> <fingerprint>`, over its own rpc stdin, which is
364
+ how the row is believed to be yours and runs uncapped); the pi holding the session hands it over
365
+ when it is idle. While pi is busy the page lists everything queued, in the order it runs, each row
366
+ with its source ("you", "cron nightly", "rule 3f2a…") and a ✕, then what pi itself still holds (a
367
+ command typed while busy) as "pi"; "queued N ✕" counts both, and clear cancels every row it
368
+ counts, pi-loops' included, and what pi holds, as 0.22.8's clear emptied pi's whole queue. A
369
+ message queued when pi exits is kept and offered by the next page as "not sent before pi stopped",
370
+ with send and discard, where it used to be lost. After Stop what is queued runs once pi is idle, as
371
+ Esc leaves a terminal's queue (owner decision 2026-11-02); each row keeps its ✕, and nothing waits
372
+ on a clock or for your next send. A send pi refuses (compaction in progress, no
373
+ credentials) comes back to the composer with the error. When the same session is open in a
374
+ terminal pi that holds it, that terminal answers the page's message and the page says "open in a
375
+ terminal (pid N): answered there; /resume to see it", where the page used to fork a branch the
376
+ terminal never saw; such a row, and any person row nobody rang for, runs capped as unattended, so
377
+ the terminal asks before a dangerous command. Slash commands, steers and Stop still go over rpc.
378
+ - **A request runs as its source, nowhere wider.** The process that runs a row recomputes its
379
+ `mode` from its source, and a row that says otherwise is removed and audited `refused`
380
+ (`/triggers audit`). pi-loops in the background runs no attended row (what you typed, a `/goal`
381
+ continuation, a claim): it waits for a window. In a window, a plain job's turn runs under the job's
382
+ declared tools and the dangerous-command policy and asks you first (see the upgrade notes); a
383
+ control-plane approval (`new_trigger`, the trigger tools) asked for in such a turn waits a minute at
384
+ most.
385
+ - **A rule's result is a request addressed to its conversation.** It is one row in
386
+ `requests/<session>.jsonl` (`deliveries/` and the mailbox are gone), kept until
387
+ `[Trigger <id>]` is in the conversation, whoever delivers it: the window holding the conversation,
388
+ or pi-loops in the background for a closed one. A row whose delivery fails waits a minute, then
389
+ five, and no longer holds up the rows behind it; at the third failure, or when the conversation's
390
+ file is gone, it becomes a finding in the inbox, marked for the conversation, with a `request`
391
+ audit row (`dead`), and only then leaves the queue, so a crash in between loses nothing.
392
+ `/triggers audit` shows the promotion as `to: "chat"` or `to: "queued"`.
393
+ - **A plain job's prompt is a request too, written before its slot is spent.** The window's clock
394
+ writes a `turn` row to `requests/<session>.jsonl` (its id derived from the job and the slot, so a
395
+ crash between the row and the stamps costs nothing) and the window hands it to pi only while pi
396
+ is idle: Esc has nothing of pi-loops' to drop, a window that quits leaves the prompt for the next
397
+ one, and after a turn you stopped it runs once pi is idle, as a message you queued would; while it
398
+ waits the window says `queued: cron <name> runs after the current turn`, the status line shows
399
+ `queued N`, and `/cron queue` lists and cancels it. A
400
+ one-shot is removed once its prompt is in the conversation, not when it fires. While a job's
401
+ prompt is still queued a later slot is spent without a second one (`skipped overlaps`). pi-loops
402
+ in the background runs a row a window left as a held turn under the row's id, and a held turn
403
+ handed back after its prompt landed leaves a fresh row for the window to run.
404
+ - **Every prompt pi-loops has for your chat is a request now.** A `/goal` continuation, `/inbox
405
+ claim`, a recipe's tracker or merge-conflict hand-off, `/new-trigger` and an `inject_and_run` push
406
+ are rows in `requests/<session>.jsonl` like a plain job's prompt, handed to pi only while it is
407
+ idle (a plain job's prompt and a push keep their `[Trigger <id>] ` prefix; what you hand over
408
+ yourself, and a goal continuation, go in their own words, as in 0.22.8, and are proved by their
409
+ text after the stamp they were handed over at): Esc and a window that quits lose none of them, except a goal
410
+ continuation, which Esc drops with the goal it pauses. A claim queues
411
+ its hand-off before the finding is marked claimed, so a crash between leaves it `new` with the
412
+ hand-off waiting, not claimed with nobody told. While a goal
413
+ continuation is queued or on its way the evaluator does not run, so the budget is charged once per
414
+ continuation; Esc pauses the goal and drops the continuation queued for it. What you type runs in
415
+ the window you typed it in, lease or not, and before any turn the clock or a server queued; a
416
+ window that is busy says `queued: /inbox claim runs after the current turn` (a recipe's, `/new-trigger`'s and a plain job's lines name their source the same way). `/goal clear` and a new `/goal` drop a continuation still queued for the goal they replace.
417
+ - **A stop is a request too, and `pi-loops abort <session-id>` stops a conversation's turn from
418
+ outside.** `pi-loops abort` takes a session id (or a unique prefix of one pi-loops knows) and stops
419
+ the turn running in that conversation in a terminal's pi, in the browser's pi or in pi-loops in the
420
+ background (not in a `pi -p` or `--mode json` run, which reads no requests); it waits up to 5
421
+ seconds and says `stopped the turn in session <id>`, `nothing was running in session <id>` (exit 1)
422
+ or `stop queued for session <id>`. A held turn a person stops is done: no fresh prompt is queued
423
+ for it and its slot is spent. The stop is a `stop` row in
424
+ `requests/<session>.jsonl`, read by whichever process holds the conversation, so it works with the
425
+ socket closed, and `pi-loops abort <run-id>` of a turn in a closed conversation goes the same way
426
+ when pi-loops does not answer. `/triggers abort <run-id>` of such a turn writes the row instead of
427
+ asking the socket and says `requested stop for <run-id> (session <id>)`, where it used to answer
428
+ `pi-loops did not abort …`. A stop names the turn it was asked about, so one that arrives after that
429
+ turn ended never stops the next one; a stop for a prompt pi has queued but not started aborts it as
430
+ it starts, and a `cancel` row takes a queued prompt out before it runs. Stops and cancels end in
431
+ `/triggers audit` as `request` rows, `done` or `stale`.
432
+ - **A row for a closed conversation lands within moments, not on the next tick.** pi-loops in the
433
+ background watches `requests/` and `leases/`: a rule's result written for a closed conversation,
434
+ or a row a window left when it quit or gave the conversation back, is appended as soon as the file
435
+ changes, unless the conversation was written in the last two minutes: then it waits for the first
436
+ tick after that, as before. A window that writes a row for a conversation it does not hold also rings pi-loops over
437
+ the control socket (`ring`, `request:<session>` or `lease:<session>`). The 30-second tick stays as
438
+ the floor: a ring that is lost delays a row by at most one tick and never loses or repeats one.
439
+ - **The agent's tools say each thing once, and say it truly.**
440
+ - Only `cron_create` explains a `Watch for:` message; `new_trigger` and the skill no longer repeat it.
441
+ - `promote_to_chat` and `stateful` state their real defaults: with a person present the question
442
+ starts at this conversation (a rule) or the inbox (a schedule); with nobody to ask, both are false.
443
+ - Each refusal and each id a tool cannot find ends with the next step: `cron_list shows the ids and
444
+ names`, `list_triggers shows the ids`, `report what you would have done, so a person can do it`
445
+ (a sub-agent) and `a person can do it with the slash command` (no UI). `set_cron_job_state` no
446
+ longer ends every refusal, a disable's included, with `; use /cron enable <id>`.
447
+ - `cron_list` counts this project's new findings (`inbox: 1 new finding`); `all_projects` counts the
448
+ machine's.
449
+ - **The page says each thing in one place.**
450
+ - The list has no All / Needs you / Unread tabs: the cards are what needs you, and each row keeps
451
+ its `⚑ N` and unread dot. The `Today` heading shows only when an `Earlier` group does too.
452
+ - The away card no longer says `· N needs you`; the terminal's sentence still does.
453
+ - The status bar says only whether pi-loops runs: no loop count, no terminal count.
454
+ - The status chip is empty while idle (no `ready`).
455
+ - A loop row has no `runs N` and no ` · loop`; a plain job's schedule ends ` · in its conversation`.
456
+ - The project switcher names a project by its folder, with the whole path as its title; the
457
+ header has no path chip.
458
+ - The Loops pane shows Goal only while a goal is set, and has no This conversation section.
459
+ - Controls whose title repeated their label lose the title; the theme and notifications are
460
+ `theme: system|light|dark` and `notifications: on|off`. A row's holder tag has no title, with
461
+ one exception: a `terminal` tag whose terminal answers nothing sent to it keeps a title that
462
+ says so.
463
+ - The door is one heading, one line and the code field.
464
+ - A card whose conversation is gone says `its conversation is gone; runs here` (or `runs in a new
465
+ conversation in <project>`), not which button to press.
466
+ - Other projects' loops, rules and findings are counted (`+ 2 loops in other projects`) with no
467
+ terminal command; more new findings than the cards show is `+ N more`.
468
+ - The Watch card has no command box, no `Model` row and no `WATCH` tag; the terminal's question
469
+ keeps its `Command` line.
470
+ - An untitled conversation is named by its id's first 13 characters (`01a1c183-7467`), as
471
+ elsewhere.
472
+ - **The terminal and the command line say each thing once, in one form.**
473
+ - A conversation is named by its id's first 13 characters on every surface; the first 8 were
474
+ shared by conversations started about a minute apart. `pi-loops sessions` keeps 16.
475
+ - `pi-loops status` and the Background view list a loop as failing only after three failures in
476
+ a row, as `/cron` and the badge do, and name it by its label, never its whole id.
477
+ - The widget lists Rules (`condition → action`, marked only `[disabled]` or `[every time]`),
478
+ Loops, Runs and MCP; the inbox count is the footer's, and a check is shown only when it went
479
+ wrong.
480
+ - `/cron` says `disabled` but not `enabled`, marks catch-up only where it differs from the
481
+ default, prints the last fired time in local time, and the path only in `/cron all`.
482
+ - Bare `/triggers` lists the rules; `/triggers status` says in plain words what checks them.
483
+ A rule row has no `fire_once` or `promote_to_chat`.
484
+ - `pi-loops inbox list` names the loop by its label in local time, and a dismiss tells the loop
485
+ by its label. `pi-loops status` prints the start once, and every time in local time.
486
+ - `/pi-loops`, `/recipe` and `/goal` drop their taglines; both recipe lists show each recipe's
487
+ one line, with no `—` column.
488
+ - `/cron runs` says `stopped` for a run that a quit or restart ended, not `FAILED: ?`.
489
+ - An error prints `pi-loops: <message>`, and the usage of that command only when it was typed
490
+ wrong (exit 2); an unknown command points at `pi-loops --help`. A flag or a word a command
491
+ does not read is refused (`status does not take --json`) instead of being ignored.
492
+ - With HOME `/home/al`, `/home/alice/x` is no longer written `~ice/x`.
493
+
494
+ ## [0.23.0] - 2026-10-30
495
+
496
+ Tagged, never published to npm. Everything below ships in 0.24.0.
497
+
498
+ ### Upgrade notes
499
+
500
+ Read these before you upgrade from 0.22.x. Each one changes what runs, what it costs, or what a run
501
+ can see.
502
+
503
+ - **The commands that start and stop it are new.** `pi-loops status|stop|abort` and
504
+ `/pi-loops status|start|stop` replace `pi-loops host status|stop|abort` and `/cron host`. The old
505
+ spellings still work, but `/cron host stop` means something new: in 0.22 it only kept the last
506
+ window from handing loops on as it quit; now it stops pi-loops in the background, and every loop
507
+ pauses until `/pi-loops start`.
508
+ - **Plain jobs of closed conversations now run, and each run costs a turn.** In 0.22 they waited
509
+ until you resumed the conversation. Now pi-loops runs them inside the closed conversation. The
510
+ daily budget has no cap unless you set one: `[limits] daily_budget_usd` in `config.toml`. The
511
+ first start of 0.23 says how many of your plain jobs this affects; `/cron all` lists them.
512
+ - **Loops and rule checks run in the background.** A run there does not get these from
513
+ the window:
514
+ - extensions given with `-e`, `--skill`, `--prompt-template`, `--system-prompt` and
515
+ `--append-system-prompt` (creating a loop says so);
516
+ - a key given with `--api-key`. When a service manager runs pi-loops, keys that are only
517
+ exported in your shell do not reach it either. Keys saved with pi's `/login` always do;
518
+ - the window's current model. A loop whose model you cleared with `/cron set --model -` uses
519
+ pi's default model;
520
+ - the window's trust. A loop or rule check loads a project's own settings, extensions and
521
+ `.pi/mcp.toml` only when pi has saved trust for that exact directory, read at each run; `/trust`
522
+ in a pi opened there saves it, and the window says so when a loop needs it. A plain job's run
523
+ in its closed conversation uses pi's saved trust for that directory or a parent of it, as the
524
+ conversation's owner would get on resume; "this session only" trust is not carried;
525
+ - the window's live MCP connections. A run connects its own from `mcp.toml` and the project.
526
+ - **`--trigger-poll-secs` no longer has an effect.** Rule checks run in the background, and pi-loops
527
+ reads `[triggers] poll_interval_secs` from `config.toml` there. Passing the flag warns at session start.
528
+ - **A loop keeps the built-in tools of the chat it was made in.** A loop made in `pi --tools read,ls`
529
+ runs with `read` and `ls`, not with pi's default `bash`, `edit` and `write`. Loops made before this
530
+ version run with pi's defaults; to restrict one, remove it and add it again in such a chat, or
531
+ give it `--tools`.
532
+ - **`[host] auto = false` now means that loops do not run until `/pi-loops start`.** In 0.22 it
533
+ only stopped the hand-off when the last window quit, and loops still ran while a window was open.
534
+ The session-start line says when loops are paused for this reason.
535
+ - **pi-loops keeps running after the last window closes.** To stop it, run `/pi-loops stop` or
536
+ `pi-loops stop`. It stays stopped until `/pi-loops start` or `pi-loops start`.
537
+ - **pi-loops needs node.** Under bun, pi-loops runs on the `node` on your PATH. If there is none,
538
+ loops pause and the status line says why.
539
+ - **A pi-loops 0.22 window can report that pi-loops failed to start when you quit it.** pi-loops 0.23
540
+ is already running, so the one the old window starts exits at once. Nothing is wrong;
541
+ restart your 0.22 windows after you upgrade.
542
+ - **To go back to 0.22.8,** follow the steps under "Uninstall" in the README before you install it.
543
+
544
+ ### Added
545
+ - **`pi-loops start`, `stop`, `restart`, `status` and `abort`, and `/pi-loops start|stop|restart|status`
546
+ in pi.** They control the part of pi-loops that runs loops and rule checks in the background.
547
+ `stop` keeps it stopped until `start`; `restart` waits for the old process to exit before it starts
548
+ the new one. `pi-loops start --foreground` runs it in the terminal, for a systemd unit or a
549
+ LaunchAgent that you write: `docs/cli.md` has a systemd example. pi-loops itself installs no unit.
550
+ - **A loop's results come back to the conversation that created it.** A loop's findings went to
551
+ the machine-wide inbox and nothing told the conversation that started the loop. Each finding now
552
+ names that conversation and its run's transcript, and a run record names the conversation too.
553
+ When the conversation starts, one line says how many runs (and how many failed) and how many new
554
+ findings its loops produced since `/inbox` was last listed there; listing `/inbox` clears it.
555
+ - **`/inbox open <n>` starts a new conversation from the run that filed a finding.** pi forks the
556
+ run's transcript into a new session file and switches to it; the run's own transcript is not
557
+ written. A transcript that a new finding points at is no longer pruned with the oldest runs
558
+ (at most 100 such transcripts per loop).
559
+
560
+ ### Changed
561
+ - **Loops run in the background.** Windows used to elect one of themselves to run the loops,
562
+ and the last to quit handed them to a background process that went away again when a window
563
+ opened. Now a window starts pi-loops in the background when there is a loop, a rule or a push
564
+ server to run, it never yields to a window, and windows run only the plain jobs of the
565
+ conversations they hold. `/pi-loops stop` stops it until `/pi-loops start`. A loop no longer shares
566
+ a window's live MCP connections: it connects its own from `mcp.toml` and the project, as it always
567
+ did in the background.
568
+ - **A background process that stops answering is replaced, one that keeps crashing is paused, and
569
+ one that cannot start says so.** A window whose three ticks in a row get no answer from its socket,
570
+ and whose heartbeat has stopped, kills it and starts another; one still ticking with a broken socket
571
+ is reported instead. An uncaught error is logged first, then the process exits; three crashes
572
+ within five minutes pause it for half an hour (`loops paused: pi-loops keeps crashing`), twice as
573
+ long each time it crashes again as soon as it is back, at most a day; `/pi-loops start` ends a
574
+ pause at once. A loop whose runs die with it three times in a row, each time the only run in
575
+ flight, is parked. One that dies at startup is retried
576
+ after 1, 5 and 30 minutes, and the status line says `loops paused: pi-loops could not start`. Under bun, pi-loops
577
+ runs on the `node` on your PATH.
578
+ - **A rule's result goes to the conversation that created it, also when two conversations in one
579
+ repository match in the same check.** One check covers a project's rules, and its result used to
580
+ land in the inbox when the matched rules came from two conversations. Now each of those
581
+ conversations gets the result. The result is the check's whole summary, not only the part about
582
+ that conversation's rules.
583
+ - **A closed conversation's plain jobs run in it.** They used to sleep until the conversation was
584
+ resumed. pi-loops now takes the conversation for the one turn and gives it back, so the answer is
585
+ there on resume; `/cron` marks the job `[session … — runs in the background]`. It leaves
586
+ alone a file written by someone else in the last two minutes, and a window that opens the conversation mid-turn
587
+ takes it back at once and runs the job itself, however late. Each turn is a run in `/cron runs`
588
+ with its own cost, counted by the daily budget; a failed turn is a failed run (error, backoff, one
589
+ retry for a one-shot), and a turn pi-loops stops itself runs again on its next tick. A rule's
590
+ result for a closed conversation is appended to it too, instead of going to the inbox.
591
+ - **A rule's result reaches a closed conversation however busy it is, and you are told.** A result
592
+ for a conversation pi-loops cannot take that instant waits in its queue and is appended on a later
593
+ tick, instead of landing in the inbox; a queued result is kept until it is in the conversation,
594
+ so a window quitting before it read one loses nothing. The `[inbox] since you last looked` line
595
+ counts rule results appended while you were away, and findings in other projects apart.
596
+ - **A project's own push servers trigger its rules again.** pi-loops connects the push servers of
597
+ trusted projects that have rules (a regression in the earlier releases of this redesign).
598
+ - **When pi-loops is stopped or disabled, every surface says its work is paused.** With pi-loops stopped by hand,
599
+ `[host] auto = false` or `PI_LOOPS_HOST=off`, no loop runs. The status line, `/cron`, the
600
+ session-start line and `pi-loops status` now say so and why, instead of promising next runs.
601
+ - **pi-loops picks up edits without a restart.** It rereads `mcp.toml` every minute and starts or
602
+ stops only the servers that changed, and a project's AGENTS.md, skills, extensions and settings
603
+ reach the next run after they are edited. `pi-loops status` names the process whose environment
604
+ the background process has.
605
+ - **A plain job runs on its conversation's model.** It is no longer pinned to the model of the chat
606
+ it was made in, so it uses the same model whether its conversation is open or closed.
607
+ - **The window tells you what happens in the background.** The status line, the panel and the run
608
+ cards follow the background process's runs as they start and finish, not a tick later. A rule
609
+ check that matched or failed shows its card as in 0.22, and the browser panel's "last check" line
610
+ is back. When a rule's result is added to one of this project's closed conversations, the open
611
+ windows of the project say which one. A loop disabled or parked there, a state write that failed, an untrusted project, an
612
+ extension that did not load and the budget pause are said once in the windows of that project, as
613
+ they were when loops ran in a window. Quitting says whether loops keep running in the background,
614
+ or why they are paused. The since-you-looked line and the session-start lines come after a
615
+ resumed conversation's history, where a long one no longer pushes them off the screen.
616
+ - **A window shows and stops what pi-loops runs.** The running badge, `/triggers running`,
617
+ `/triggers status`, the panel and the browser panel include pi-loops' runs and checks, and
618
+ `/triggers abort` can stop a loop running in the background again. The browser panel reads
619
+ whether pi-loops is running on every refresh, so it no longer says `pi-loops is not running`
620
+ while it runs, and the last rule check it shows is this project's.
621
+ - **pi-loops has a read-only status page** on `http://127.0.0.1:4174/`: which process holds each
622
+ conversation, what is running, budget, failing jobs and MCP servers. `pi-loops status --url`
623
+ prints its address with the token. It takes no action; abort, run and stop stay on the socket.
624
+ The token survives a restart; `docs/cli.md` says how to retire one.
625
+ - **During an upgrade, pi-loops 0.22 windows stand by for loops.** pi-loops writes the file those
626
+ windows elect through, so they do not run loops beside it. They still fire their own
627
+ conversations' plain jobs; restart them.
628
+
629
+ ### Fixed
630
+ - **The browser window follows `/inbox open`.** Opening a finding forked the run into a new
631
+ conversation and switched pi to it, but the page kept showing the old conversation until it was
632
+ reloaded by hand. It now reloads onto the new one, as `/session import --resume` does too.
633
+ - **A conversation open in two windows no longer fires its plain jobs in both.** One window holds
634
+ the conversation; the other is a viewer and `/cron` there says `[session … — open in pid N]`.
635
+ The holder changes only when its process is gone, so a window that is quiet for hours keeps it.
636
+ A window that shares its conversation with a pi-loops 0.22 window says so once, because that
637
+ window still fires the jobs on its own. A viewer no longer writes pi-loops' views into the
638
+ conversation (`/cron`, `/inbox`, run cards). A window whose copy is older than the file (the
639
+ other window added turns, or pi-loops appended a rule result) fires nothing into it: it says to
640
+ `/resume` the conversation, and its plain jobs and rule results wait until then. Before, its turns
641
+ forked the conversation and hid the other window's answers on a dead branch.
642
+ - **A project's `.pi/mcp.toml` no longer starts its servers before you trust the project.** pi
643
+ does not ask about a directory that has none of its own settings, skills or extensions, and
644
+ treats it as trusted; pi-loops took that answer, so opening a cloned repository started the
645
+ command its `.pi/mcp.toml` names. There, the file now loads only once `/trust` has saved trust
646
+ (pi applies it at its next start) or with `pi --approve`, and the window says it ignored the file
647
+ until then.
648
+ - **The lines a session starts with are all on the screen.** pi shows back-to-back notices on one
649
+ line, so only the last one stayed: "since you last looked", the viewer line and the one-time
650
+ notice that plain jobs now cost turns were replaced before anyone saw them. They are now one
651
+ notice, and the one-time notice counts as said only once it is shown.
652
+ - **A rule's result lands in the conversation that created it.** A check that ran in a different
653
+ window than the rule's conversation now queues the result for the window holding that
654
+ conversation; a closed conversation gets it appended by pi-loops (see "Changed").
655
+ - **Removing a loop stops its run.** A loop removed while it ran in the background went on to its
656
+ deadline; pi-loops now stops that run on its next tick, wherever the removal was made.
657
+ - **A damaged `triggers.json` costs its rules, not every loop.** The window counted no work and
658
+ started nothing, and pi-loops itself exited at start. The store's error now names its file.
659
+ - **The background process starts where pi's packages are hoisted.** It looked for pi's
660
+ dependencies only inside pi's own `node_modules`; a hoisted npm install or pnpm put them beside pi.
661
+ - **Smaller fixes for a long-running background process.** A loop's extension set that a newer
662
+ one replaces (any `/model` rewrites `settings.json`) shuts down when its runs end, not under them,
663
+ and an unused one after an hour. A rule result that pi dropped from its queue (Esc while it
664
+ answered) is offered again. A clock set back no longer holds every plain job of a closed
665
+ conversation. A lease left held by a failed release is given back. One failed `ps` no longer
666
+ counts a healthy process as gone. Several windows replacing one wedged process count one crash.
667
+ A `pi-loops stop` that lands while a window starts pi-loops is kept. `cron_create` and
668
+ `new_trigger` say when pi-loops is paused. `pi-loops start|stop|restart --help` prints the usage
669
+ instead of acting. The browser front end answers a malformed request line with 400 instead of
670
+ exiting, and a relative `PI_LOOPS_DIR` reaches the background process as an absolute path.
671
+
8
672
  ## [0.22.8] - 2026-10-28
9
673
 
10
674
  ### Fixed