@alphacoder-v0/pi-loops 0.22.7 → 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.
- package/CHANGELOG.md +681 -0
- package/CONTRACT.md +7 -7
- package/README.md +91 -63
- package/README.zh-CN.md +59 -39
- package/docs/cli.md +272 -83
- package/docs/configuration.md +86 -24
- package/docs/design.md +395 -56
- 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 +37 -26
- package/docs/web-ui-parity.md +691 -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/cli-entry.mjs +12 -3
- package/src/cli.ts +746 -125
- 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 -41
- 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 +1301 -450
- 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 +598 -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 +16 -5
- package/src/watch-plan.ts +174 -0
- package/src/web.d.mts +3 -0
- package/src/web.mjs +2260 -2340
- package/src/window-executor.ts +459 -0
- package/src/window-guard.ts +158 -0
- package/docs/agents/issue-tracker.md +0 -34
package/docs/cli.md
CHANGED
|
@@ -1,49 +1,99 @@
|
|
|
1
1
|
# The `pi-loops` command line
|
|
2
2
|
|
|
3
|
-
`pi-loops`
|
|
3
|
+
`pi-loops` opens a conversation. With no arguments it opens the browser page when you are at a
|
|
4
4
|
local terminal, and pi itself when you are not — over ssh, or with no terminal at all, where a
|
|
5
5
|
browser on this machine would help nobody. `--web` and `--tui` say which when the guess is wrong.
|
|
6
|
+
With no terminal at all (stdin or stdout piped) and nothing that tells pi to run without one (`-p`,
|
|
7
|
+
`--mode`, `--help` and the like after `--`), there is nowhere for pi to draw: `pi-loops` exits 1 and
|
|
8
|
+
says so, with `pi-loops: there is no terminal here to open pi in; run pi-loops --web to open the
|
|
9
|
+
page, or pi-loops -- -p "<prompt>" for one answer`.
|
|
6
10
|
|
|
7
11
|
```
|
|
8
|
-
pi-loops #
|
|
12
|
+
pi-loops # open a conversation, in whichever window makes sense here
|
|
9
13
|
pi-loops --tui # the terminal one
|
|
10
|
-
pi-loops --web
|
|
14
|
+
pi-loops --web # the browser one, also over ssh (it prints the address)
|
|
15
|
+
pi-loops tui | pi-loops web # the same two, as words
|
|
16
|
+
pi-loops --loops-dir <dir> # the page uses that loops directory, as PI_LOOPS_DIR does
|
|
11
17
|
pi-loops --model anthropic/claude-opus-5 -e . # anything it does not recognise goes to pi
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`/login`, whose OAuth flow has no equivalent — log in once with `pi` and the rest follows.
|
|
20
|
+
The page is served by pi-loops in the background, not by this command. `pi-loops` starts pi-loops
|
|
21
|
+
in the background if it is not running, asks it to open the conversation, opens your browser and
|
|
22
|
+
exits:
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
```
|
|
25
|
+
$ pi-loops
|
|
26
|
+
started pi-loops in the background
|
|
27
|
+
pi-loops page: http://127.0.0.1:4173/c/424aba43-…/ (opened in your browser)
|
|
28
|
+
if it did not open, or from another browser: http://127.0.0.1:4173/c/424aba43-…/?open=c0ff… (works once, for 10 minutes)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The first line appears only when this command started pi-loops. The browser is opened on a file
|
|
32
|
+
that only you can read, under the loops directory's `pages/open/`, which sends it to the
|
|
33
|
+
conversation with a one-use key; the key is in no command line, and the file is removed once the
|
|
34
|
+
browser has used it, or after a minute. "Opened in your browser" means an opener started; it cannot
|
|
35
|
+
tell whether the browser could read the file. A browser confined by snap or flatpak (Ubuntu's
|
|
36
|
+
default Firefox is one) cannot read a hidden directory such as `~/.pi`, so it shows an error
|
|
37
|
+
instead: use the address on the next line. That address, printed only on a terminal, holds a second
|
|
38
|
+
one-use key that works for 10 minutes, never the token, because what a terminal prints stays in its
|
|
39
|
+
scrollback. Over ssh, with `--no-open`, or with no terminal, nothing is opened, no file is written,
|
|
40
|
+
and the line ends `(open it in a browser)`. A conversation the page already has open is not started
|
|
41
|
+
again, so pi arguments given for it are not applied, and the last line says which:
|
|
42
|
+
` this conversation was already open; --model was not applied`. Opening can wait up to 15 s for
|
|
43
|
+
an older pi in that conversation to stop; after 3 s the command says
|
|
44
|
+
`waiting for pi-loops to open the conversation…` on stderr, and it waits up to 30 s in all.
|
|
45
|
+
|
|
46
|
+
Both windows are complete pi sessions: each conversation in the page is a `pi --mode rpc` that
|
|
47
|
+
pi-loops runs for it, so the session file, your models, tools and extensions are the same either
|
|
48
|
+
way. What the browser cannot do is pi's own built-in slash commands, which do not exist in that
|
|
49
|
+
mode, and `/login`, whose OAuth flow has no equivalent — log in once with `pi` and the rest follows.
|
|
50
|
+
|
|
51
|
+
The page outlives the terminal. Closing the terminal, or Ctrl-C in it, ends nothing: the command
|
|
52
|
+
has already exited. A conversation nobody looks at stops after 15 minutes with no tab open and no
|
|
53
|
+
turn running, and the page keeps at most six open at once (the one used least recently and idle
|
|
54
|
+
goes first). Opening it again starts it again, with the same transcript. `pi-loops stop` closes the
|
|
55
|
+
page and every conversation in it.
|
|
26
56
|
|
|
27
57
|
### The address
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
`http://127.0.0.1:4173/` for the default loops directory. Bookmark it. A loops directory moved with
|
|
60
|
+
`PI_LOOPS_DIR` takes a free port, and `pi-loops status` names it. The token that guards the page
|
|
61
|
+
lives in `~/.pi/agent/loops/web-token` (mode 0600) and is kept between runs, so the address is the
|
|
62
|
+
same one tomorrow, and the first visit pairs the browser, so you never see the token again.
|
|
63
|
+
`GET /` opens the conversation you looked at last; `/c/<session id>/` opens a particular one. When
|
|
64
|
+
the conversation you looked at last was empty and pi never saved it, `GET /` opens a new conversation
|
|
65
|
+
in its project instead.
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
The address belongs to pi-loops in the background, so it is set in `config.toml`, not on the command
|
|
68
|
+
line ([configuration.md](configuration.md#page)):
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[page]
|
|
72
|
+
port = 4200
|
|
73
|
+
```
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
then `pi-loops restart`. `pi-loops --port`, `--host`, `--allow-host` and `--no-auth` exit 2 and print
|
|
76
|
+
the lines to write.
|
|
77
|
+
|
|
78
|
+
There is a token at all because anything that reaches this server gets every conversation in it,
|
|
79
|
+
and "anything" includes a website you have open in another tab: it cannot read the answers, but
|
|
80
|
+
without a check it could still tell your agent what to do. The page sends the token in a header
|
|
81
|
+
that no other site can set, never in a cookie: a cookie goes to every port on this machine. What the
|
|
82
|
+
first visit leaves is a device cookie (`SameSite=Strict`, which is a browser's promise not to send
|
|
83
|
+
it on anything another site started) and a secret the page keeps for this address; together they
|
|
84
|
+
get a reloaded page its token, and the cookie alone opens nothing. So the cost of this to you is one
|
|
85
|
+
visit and then nothing.
|
|
86
|
+
|
|
87
|
+
When 4173 is held, `pi-loops` opens nothing and exits 1. An older pi-loops page (0.23 or before,
|
|
88
|
+
which ran in its terminal) gets
|
|
89
|
+
`pi-loops: an older pi-loops page is still open on 127.0.0.1:4173; quit it (Ctrl-C in its terminal) and this page takes its place within 30 seconds`.
|
|
90
|
+
Any other program gets
|
|
91
|
+
`pi-loops: 127.0.0.1:4173 is used by another program; set [page] port in config.toml, then pi-loops restart`.
|
|
92
|
+
Loops and rule checks run either way.
|
|
42
93
|
|
|
43
94
|
### From a phone
|
|
44
95
|
|
|
45
|
-
The
|
|
46
|
-
better:
|
|
96
|
+
The page binds loopback, which a phone cannot reach. Two ways round it, and the first is better:
|
|
47
97
|
|
|
48
98
|
**Tailscale.** Leave the server where it is and put Tailscale in front:
|
|
49
99
|
|
|
@@ -55,19 +105,28 @@ Your phone then opens `https://<machine>.<tailnet>.ts.net/` — TLS terminated o
|
|
|
55
105
|
your own devices, and this server still bound to 127.0.0.1. It arrives here looking local and
|
|
56
106
|
carrying the tailnet name as its `Host`, which is accepted for that reason.
|
|
57
107
|
|
|
58
|
-
**The local network.** `
|
|
59
|
-
phone on the same wifi can use. Anything that can
|
|
60
|
-
the
|
|
108
|
+
**The local network.** `[page] host = "0.0.0.0"` in `config.toml`, then `pi-loops restart`, binds
|
|
109
|
+
every interface; `host.log` names the address a phone on the same wifi can use. Anything that can
|
|
110
|
+
open the port is then talking to your conversations, which is why the token is never optional.
|
|
61
111
|
|
|
62
112
|
`tailscale funnel` is the same shape and puts it on the open internet; the token is what stands
|
|
63
|
-
between that and anyone
|
|
64
|
-
route a request took to arrive.
|
|
113
|
+
between that and anyone.
|
|
65
114
|
|
|
66
115
|
Either way the phone has to get in once, and the token is 32 hex characters, which is fine to click
|
|
67
116
|
and miserable to type. So: press **add device** in the browser you are already signed in on. It
|
|
68
117
|
shows a **QR code** — point the phone at it and it is in, having typed nothing — and the same six
|
|
69
|
-
digits underneath for when a camera is not the thing you want to use.
|
|
70
|
-
|
|
118
|
+
digits underneath for when a camera is not the thing you want to use. From a terminal, `pi-loops pair`
|
|
119
|
+
makes the same code and names the addresses that device can open:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
$ pi-loops pair
|
|
123
|
+
pairing code for another device: 482913 (10 minutes)
|
|
124
|
+
on that device, open http://192.168.1.20:4173/ and type it
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A code exists only when you ask for one, with the button or with `pi-loops pair`. Opening a
|
|
128
|
+
conversation does not make one, so no other program on the machine can spend its twenty guesses
|
|
129
|
+
before you need it.
|
|
71
130
|
|
|
72
131
|
The QR encodes the address *this browser reached the server on*, which under `tailscale serve` is
|
|
73
132
|
the tailnet name and is the one that works from anywhere on your tailnet. If the window you press it
|
|
@@ -77,44 +136,51 @@ so instead of showing you one that does not.
|
|
|
77
136
|
A code is good for one use and lasts ten minutes; twenty wrong guesses disable it until you ask
|
|
78
137
|
for another.
|
|
79
138
|
|
|
80
|
-
On a network you do not own, remember that
|
|
81
|
-
a token that outlives the process, and anyone on the wire can read it. That is the
|
|
82
|
-
`tailscale serve` exists for
|
|
139
|
+
On a network you do not own, remember that `[page] host` is plain http: every request the page
|
|
140
|
+
makes carries a token that outlives the process, and anyone on the wire can read it. That is the
|
|
141
|
+
case `tailscale serve` exists for.
|
|
142
|
+
|
|
143
|
+
The phone has a page only while pi-loops runs in the background. Until something runs `pi-loops`
|
|
144
|
+
(or a pi window, or the service unit in
|
|
145
|
+
[Running pi-loops under your own service manager](#running-pi-loops-under-your-own-service-manager)),
|
|
146
|
+
there is nothing on 4173 to answer it.
|
|
83
147
|
|
|
84
148
|
Add it to the home screen and it opens without browser chrome: there is a web app manifest, and no
|
|
85
149
|
service worker — nothing here is worth caching, and a stale copy of a live front end is worse than
|
|
86
150
|
no copy.
|
|
87
151
|
|
|
88
|
-
### Dropping the token
|
|
89
|
-
|
|
90
|
-
`pi-loops --no-auth` drops the token entirely: no cookie, no query string, nothing to carry, and
|
|
91
|
-
anything on this machine that can open port 4173 has your session. What is left is the check that
|
|
92
|
-
the request did not come from another site — a browser tells the truth about that, and it is what
|
|
93
|
-
keeps a page on `http://localhost:5173` from posting into your agent — but any *program* running as
|
|
94
|
-
any user on this machine is then in. It is the right trade on a machine only you use, and the wrong
|
|
95
|
-
one on a shared host.
|
|
96
|
-
|
|
97
152
|
A browser that has never been here (a different one, or after clearing cookies) gets a page saying
|
|
98
|
-
so.
|
|
99
|
-
|
|
153
|
+
so. Run `pi-loops` from a terminal on this machine and it opens a window that works from then on,
|
|
154
|
+
or use the `?token=…` address it printed.
|
|
155
|
+
|
|
156
|
+
### No token, no longer
|
|
100
157
|
|
|
101
|
-
|
|
158
|
+
`--no-auth` is gone in 0.24: the page is always on and always behind its token. A page that runs
|
|
159
|
+
whether or not a terminal is open, with a shell behind every conversation, is not one to leave open
|
|
160
|
+
to every program on the machine.
|
|
102
161
|
|
|
103
|
-
|
|
104
|
-
in a terminal — two front ends driving one agent is not something pi offers, and pretending
|
|
105
|
-
otherwise would mean two windows disagreeing about whose turn it is.
|
|
162
|
+
### Opening a conversation you already have
|
|
106
163
|
|
|
107
|
-
|
|
108
|
-
|
|
164
|
+
The page starts a `pi --mode rpc` for each conversation it opens. Picking up where you left off
|
|
165
|
+
uses pi's own words for it:
|
|
109
166
|
|
|
110
167
|
```
|
|
111
|
-
pi-loops --continue # the newest
|
|
112
|
-
pi-loops --
|
|
113
|
-
pi-loops --session <id-or-path> # a particular one
|
|
168
|
+
pi-loops --continue # the newest conversation in this directory, in the page
|
|
169
|
+
pi-loops --session <id-or-prefix> # a particular one
|
|
114
170
|
```
|
|
115
171
|
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
`pi-loops` finds the conversation the way `pi-loops export` does and asks the page for it. An id it
|
|
173
|
+
cannot find is refused, with exit 1. `--resume` is pi's picker, which the page has as **resume**
|
|
174
|
+
(`/resume`), so `pi-loops --resume` exits 2 and says so.
|
|
175
|
+
|
|
176
|
+
A flag for pi goes after `--` when there is one: `pi-loops --web -- --session <id>`. A flag that is
|
|
177
|
+
not this command's placed before `--` is refused with the command line to use, rather than dropped.
|
|
178
|
+
The page keeps a conversation's pi arguments under `pages/`, so it reopens the same way after an
|
|
179
|
+
idle stop or a restart; `--api-key` is refused for that reason (exit 1), and the line says to set the
|
|
180
|
+
provider's environment variable instead.
|
|
181
|
+
|
|
182
|
+
A conversation a terminal holds can be open in the page at the same time. A message sent from the
|
|
183
|
+
page is then answered in the terminal, and the page says so.
|
|
118
184
|
|
|
119
185
|
### Starting over, without leaving the window
|
|
120
186
|
|
|
@@ -134,16 +200,15 @@ filename. Compaction reports what the context was, what it became, and what the
|
|
|
134
200
|
Underneath, **clear** and **resume** are pi's `switch_session`: the session is replaced inside the
|
|
135
201
|
process that is already running, so the model, the MCP connections and the extensions are the same
|
|
136
202
|
ones, rebuilt against the new session. That is what `/new` and `/resume` do in the terminal, and
|
|
137
|
-
automation follows it the same way
|
|
138
|
-
|
|
139
|
-
and fires again on the next tick.
|
|
203
|
+
automation follows it the same way: pi-loops keeps running through the swap, and the conversation's
|
|
204
|
+
plain jobs move with it to whichever window now holds it.
|
|
140
205
|
|
|
141
206
|
One thing the front end adds on top of the swap: **resume** puts the session back on the model it
|
|
142
207
|
was last using. pi re-resolves the launch command line's `--model` on every swap — and the launcher
|
|
143
208
|
supplies one from the model you last chose ([configuration.md](configuration.md#what-is-remembered-between-sessions))
|
|
144
209
|
— so without this, going back to a conversation had with one model landed it on a different one. If
|
|
145
210
|
the model it used is not available any more (credentials gone), the session stays on the one it has
|
|
146
|
-
and
|
|
211
|
+
and `host.log` says which model it could not go back to.
|
|
147
212
|
|
|
148
213
|
Both are refused while a turn is running, because the swap would abort it: stop the turn first. A
|
|
149
214
|
session started this way is named by the front end rather than by pi, which is invisible everywhere
|
|
@@ -215,18 +280,25 @@ the newest release, and `upgrade` and it then agree.
|
|
|
215
280
|
## The tools
|
|
216
281
|
|
|
217
282
|
The rest of the command line does not need a pi session and has no build step; it resolves pi's
|
|
218
|
-
packages the way
|
|
283
|
+
packages the way pi-loops does. `PI_LOOPS_DIR` selects the loops directory, as everywhere.
|
|
219
284
|
|
|
220
285
|
```
|
|
221
286
|
pi-loops sessions [--all] [--limit <n>]
|
|
222
287
|
pi-loops inspect <file>
|
|
223
288
|
pi-loops export [--session <id>] [--cwd <dir>] [--output <file>] [--exclude-triggers]
|
|
224
289
|
pi-loops import <file> [--cwd <dir>] [--activate-triggers=off|ask|on]
|
|
225
|
-
pi-loops
|
|
290
|
+
pi-loops start [--foreground] | stop | restart | status [--url] | abort <run-id|trace-id|session-id>
|
|
226
291
|
pi-loops recipe list | show <name|path> | add <name|path> [--cwd <dir>] [--level report|propose|act] [--overwrite]
|
|
227
292
|
pi-loops inbox list [--all] [--cwd <dir>] | claim <id> | dismiss <id> [--reason <text>] [--json]
|
|
228
293
|
```
|
|
229
294
|
|
|
295
|
+
`-h` or `--help` anywhere in a subcommand's arguments before a `--` prints that subcommand's usage
|
|
296
|
+
and exits 0 without doing anything else; `pi-loops -h` is `pi-loops --help`. After `--` they are pi's.
|
|
297
|
+
A command typed wrong, such as a missing archive path, a flag the command does not read
|
|
298
|
+
(`status does not take --json`) or a word it does not read (`export: unexpected argument "out.pisession"`),
|
|
299
|
+
prints `pi-loops: <what is wrong>` and that command's usage, and exits 2. Any other error prints only its line and exits 1. An unknown command
|
|
300
|
+
names itself and the version you run, and exits 2.
|
|
301
|
+
|
|
230
302
|
The command line reads, triages one finding at a time and moves sessions. It never creates or
|
|
231
303
|
removes: there is no `cron remove`, no `recipe remove` and no `inbox clear`. Creating and removing
|
|
232
304
|
jobs, rules and recipes are slash commands, because each asks a person first and records what it did
|
|
@@ -254,7 +326,7 @@ taken. `--session` accepts a full id or a unique prefix. `--exclude-triggers` dr
|
|
|
254
326
|
sidecar.
|
|
255
327
|
|
|
256
328
|
```
|
|
257
|
-
pi-loops export --session 01a084a0 --output ~/backups/api.pisession
|
|
329
|
+
pi-loops export --session 01a084a0-1c2d --output ~/backups/api.pisession
|
|
258
330
|
```
|
|
259
331
|
|
|
260
332
|
## import
|
|
@@ -268,28 +340,142 @@ disabled and then asks once, where there is a terminal or a UI to ask in, whethe
|
|
|
268
340
|
was enabled in the source. `off` never asks; `on` activates it as part of the import.
|
|
269
341
|
Importing the same archive twice adds nothing the second time.
|
|
270
342
|
|
|
271
|
-
##
|
|
343
|
+
## pi-loops in the background
|
|
272
344
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
345
|
+
`pi-loops status` prints the process status, including its pid, version, start time, current work,
|
|
346
|
+
conversations held by windows, and log path. If it is stopped, the command says why. The older
|
|
347
|
+
spellings `pi-loops host status|abort|stop|start` and `/cron host` still work.
|
|
276
348
|
|
|
277
349
|
```
|
|
278
|
-
$ pi-loops
|
|
279
|
-
|
|
280
|
-
pid 2949521 on box, started 2026-09-
|
|
281
|
-
|
|
282
|
-
|
|
350
|
+
$ pi-loops status
|
|
351
|
+
pi-loops
|
|
352
|
+
pid 2949521 on box, started 2026-09-09 13:33, model openai-codex/gpt-5.5
|
|
353
|
+
pi-loops 0.24.0 · 1/1 loops, 0/0 rules enabled · inbox: 0 new
|
|
354
|
+
started by the shell that ran pi-loops
|
|
355
|
+
conversation 424aba43-0c1f "fix the flaky login test" open in pid 939915 (tui)
|
|
356
|
+
running nightly (run-97ffb250) since 2026-09-09 13:34: check the repo issues
|
|
357
|
+
3 messages waiting for 2 conversations, 1 could not be delivered
|
|
358
|
+
424aba43-0c1f: 2 waiting · oldest 5m · 1 could not be delivered · open in pid 939915
|
|
359
|
+
9e1f3b07-77a0: 1 waiting · oldest 30s · closed
|
|
283
360
|
```
|
|
284
361
|
|
|
285
|
-
The
|
|
286
|
-
|
|
362
|
+
The waiting lines appear when something waits to be delivered to a conversation (in
|
|
363
|
+
`requests/<session>.jsonl`): how many, how long the oldest has waited, how many failed three times
|
|
364
|
+
and go to the inbox next, and where the conversation is (`open in pid <pid>`, `in the background`,
|
|
365
|
+
or `closed`, one pi-loops is not running a turn in). They are read from the files, so
|
|
366
|
+
`pi-loops status` prints them with pi-loops down too, after the line that says it is not running.
|
|
367
|
+
|
|
368
|
+
`pi-loops stop` stops it until `pi-loops start`, `/pi-loops start` or `pi-loops` opening the page
|
|
369
|
+
(which prints `pi-loops was stopped by hand; it runs again now (loops and rules resume)` before the
|
|
370
|
+
address), and waits up to 15 seconds for
|
|
371
|
+
it to exit; a process that has not exited by then (its event loop blocked) gets SIGKILL. The page
|
|
372
|
+
stops with it, and so does every conversation open in it:
|
|
373
|
+
` the page and its 2 open conversations closed with it; pi-loops start opens it again`. `pi-loops
|
|
374
|
+
start` starts it now, even with nothing to run yet. `pi-loops restart` stops it, waits until the old
|
|
375
|
+
process has exited (SIGKILL after 180 seconds), and starts it. A restart, and an upgrade that a pi
|
|
376
|
+
window with a newer pi-loops asks for, first wait up to two minutes for a turn in the browser page to
|
|
377
|
+
finish, so a conversation open there is not cut off mid-answer; a restart first prints `waiting up
|
|
378
|
+
to 2 min for a turn in the browser page to finish…`. A plain `pi-loops stop` ends it at once, and one
|
|
379
|
+
run during that wait ends the wait. A restart or an upgrade asked for while it already waits is
|
|
380
|
+
answered `already leaving` and does not start the wait again. With `--help`, each of these prints its
|
|
381
|
+
usage and does nothing else.
|
|
382
|
+
|
|
383
|
+
`pi-loops abort` takes three kinds of id. `abort <run-id>` interrupts one run and `abort <trace-id>`
|
|
384
|
+
one rule check, both through pi-loops in the background (short trace ids are accepted). `abort
|
|
385
|
+
<session-id>`, a full id or a unique prefix of one pi-loops knows (a conversation with a lease or a
|
|
386
|
+
queue), stops the turn running in that conversation in a terminal's pi, in the browser's pi, or in
|
|
387
|
+
pi-loops in the background. It does not reach a `pi -p` or `--mode json` run: such a run reads no
|
|
388
|
+
requests, and nothing else may stop a turn in a conversation a live process holds. That stop is a
|
|
389
|
+
request in `requests/<session>.jsonl`, read by whichever process holds the conversation, so it needs
|
|
390
|
+
no socket; a run id whose process does not answer goes the same way when a lease names it. It stops
|
|
391
|
+
only a turn that began before you asked, so a stop that arrives late never stops the next turn, and a
|
|
392
|
+
held turn it stops is done: pi-loops does not ask that prompt again. `abort` waits up to 5 seconds
|
|
393
|
+
and ends with one of three lines: `stopped the turn in session <id>`; `nothing was running in session
|
|
394
|
+
<id>` (exit code 1); or `stop queued for session <id>; it takes effect when that conversation's pi or
|
|
395
|
+
pi-loops reads it`. A prefix shorter than 8 characters is read as a trace id. A prefix that fits more
|
|
396
|
+
than one session, or a run id prefix that fits more than one running turn, lists them and exits 1.
|
|
397
|
+
|
|
398
|
+
The times are the local time of the machine pi-loops runs on, with no offset
|
|
399
|
+
([loops.md](loops.md#time-and-which-clock-it-is)).
|
|
400
|
+
|
|
401
|
+
### The Background view
|
|
402
|
+
|
|
403
|
+
What pi-loops in the background is doing is a view of the page, at `<page address>background`
|
|
404
|
+
(`http://127.0.0.1:4173/background` for the default loops directory). With no conversation to open
|
|
405
|
+
and no project to start a new one in, `GET /` shows it too. `pi-loops status --url` prints the page's address with its token, for a
|
|
406
|
+
browser that has not been there yet:
|
|
287
407
|
|
|
288
|
-
|
|
289
|
-
|
|
408
|
+
```
|
|
409
|
+
$ pi-loops status --url
|
|
410
|
+
http://127.0.0.1:4173/?token=3f9c…
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The view lists every conversation that is open somewhere or has a plain job, a rule or something
|
|
414
|
+
waiting, with where it is (`a terminal`, `this page`, `the background`, `a pi-loops 0.22 window`,
|
|
415
|
+
`closed`), then what is running, what waits in the words `pi-loops status` uses, the budget, failing
|
|
416
|
+
loops (three failures in a row, as `/cron` marks them), recent runs and MCP servers, every time in the browser's own format and no pid. It refreshes
|
|
417
|
+
every 15 seconds. `/background.json` is the same as JSON, every field as before.
|
|
418
|
+
It is behind the page's token, like every other address of the page.
|
|
419
|
+
|
|
420
|
+
`pi-loops status` has a `page:` line for it: ` page: http://127.0.0.1:4173/ · 2 open conversations`,
|
|
421
|
+
or why there is none (` page: waiting for 127.0.0.1:4173 (an older pi-loops page holds it)`). A
|
|
422
|
+
conversation open in the page reads ` conversation 424aba43-0c1f open in the page (pid 939915, 1 tab,
|
|
423
|
+
idle 3m)`. A conversation is named by the first 13 characters of its id, which only conversations
|
|
424
|
+
started in the same millisecond share, followed by its name or first message (at most 40 characters)
|
|
425
|
+
when its session file has one. The terminal, the page and the Background view name it the same way,
|
|
426
|
+
and `--session` takes it.
|
|
427
|
+
|
|
428
|
+
The token is kept in `web-token` (0600) in the loops directory, so a bookmarked address and a paired
|
|
429
|
+
phone keep working after pi-loops restarts. A stolen token reaches every conversation for as long as
|
|
430
|
+
pi-loops runs. To retire one that was shared, remove `web-token` and run `pi-loops restart`: the new
|
|
431
|
+
process makes a new token, and every browser signs in again. A token file other users can read or
|
|
432
|
+
write is set back to 0600 when pi-loops starts, and one that another user owns is refused.
|
|
433
|
+
|
|
434
|
+
### Running pi-loops under your own service manager
|
|
435
|
+
|
|
436
|
+
`pi-loops start --foreground` runs pi-loops in the terminal until it is stopped, for a service
|
|
437
|
+
manager that you set up. pi-loops writes no unit itself. A systemd user unit, with the path to
|
|
438
|
+
`pi-loops` filled in, at `~/.config/systemd/user/pi-loops.service`:
|
|
439
|
+
|
|
440
|
+
```ini
|
|
441
|
+
[Unit]
|
|
442
|
+
Description=pi-loops
|
|
443
|
+
|
|
444
|
+
[Service]
|
|
445
|
+
ExecStart=<path to pi-loops> start --foreground
|
|
446
|
+
Restart=on-failure
|
|
447
|
+
RestartPreventExitStatus=78
|
|
448
|
+
|
|
449
|
+
[Install]
|
|
450
|
+
WantedBy=default.target
|
|
451
|
+
```
|
|
290
452
|
|
|
291
|
-
|
|
292
|
-
|
|
453
|
+
Then `systemctl --user enable --now pi-loops`. Keys saved with pi's `/login` reach the process; keys
|
|
454
|
+
exported only in a shell do not. The unit is also what keeps the page up: until something runs
|
|
455
|
+
`pi-loops`, a pi window or the unit, a phone has no page to open.
|
|
456
|
+
|
|
457
|
+
The exit status tells systemd what happened:
|
|
458
|
+
|
|
459
|
+
- 0: stopped by hand (`pi-loops stop`, SIGTERM, SIGHUP), or asked to make way for a newer
|
|
460
|
+
pi-loops. `Restart=on-failure` leaves it stopped.
|
|
461
|
+
- 78: it cannot run here (the package or pi has moved). `RestartPreventExitStatus=78` leaves it
|
|
462
|
+
stopped.
|
|
463
|
+
- Anything else: a crash, which systemd restarts.
|
|
464
|
+
|
|
465
|
+
After `pi-loops stop`, `systemctl --user start pi-loops` starts it again, and so does the next login,
|
|
466
|
+
as with any systemd service. Started during a crash pause, the unit's process waits it out (half an
|
|
467
|
+
hour, longer for each pause in a row) and then runs; `/pi-loops start` ends the pause at once. `pi-loops start` and
|
|
468
|
+
`pi-loops restart` start a process of their own that the unit does not manage, and so does a pi
|
|
469
|
+
window when it upgrades pi-loops. While one of those runs, the unit's process finds it and exits 0.
|
|
470
|
+
To hand it back to the unit, run `pi-loops stop`, then `systemctl --user start pi-loops`. A LaunchAgent restarts every non-zero exit when `KeepAlive`
|
|
471
|
+
has `SuccessfulExit` false, so under launchd an exit of 78 is restarted too.
|
|
472
|
+
|
|
473
|
+
When pi-loops is stopped, `status` says why: stopped by hand, `PI_LOOPS_HOST=off`, `[host] auto =
|
|
474
|
+
false`, paused after repeated crashes, or a start that failed and when it is retried. The channel is a
|
|
475
|
+
unix socket at `host-service.sock` in the loops directory, created 0600, and it is read-mostly on
|
|
476
|
+
purpose: a background process you could prompt would be a second chat. Besides status, abort and stop
|
|
477
|
+
it accepts only `run` (a window's `/cron run` of a loop that already exists) and `upgrade` (a window
|
|
478
|
+
running a newer pi-loops asking the old process to make way).
|
|
293
479
|
|
|
294
480
|
## recipe
|
|
295
481
|
|
|
@@ -316,6 +502,9 @@ pi-loops inbox dismiss inb-3f2a… --reason "that file is generated" --json
|
|
|
316
502
|
|
|
317
503
|
`/inbox` with no pi open ([loops.md](loops.md#the-inbox)). `claim` and `dismiss` take an id or a
|
|
318
504
|
unique prefix of one and change the entry the way the slash command does; a claim here cannot start
|
|
319
|
-
a turn, so it marks the finding and leaves the acting to whoever asked.
|
|
320
|
-
|
|
505
|
+
a turn, so it marks the finding and leaves the acting to whoever asked. A finding of a loop that
|
|
506
|
+
belongs to its project, not a conversation ([loops.md](loops.md)), is marked claimed the same way,
|
|
507
|
+
and no conversation acts on it: the caller handles it. Without `--json` the same,
|
|
508
|
+
one line per finding, for a person: the full id first, then the finding, with a decision's clauses
|
|
509
|
+
on the same line as `· waiting on: …` and `· if you don't: …` (the words the page uses). The JSON is fixed in [CONTRACT.md](../CONTRACT.md) §2.2 and shown in [downstream.md](downstream.md), which is
|
|
321
510
|
the page a program should be written against.
|