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