@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +681 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +91 -63
  4. package/README.zh-CN.md +59 -39
  5. package/docs/cli.md +272 -83
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +395 -56
  8. package/docs/downstream.md +2 -2
  9. package/docs/goal.md +12 -3
  10. package/docs/hooks.md +6 -7
  11. package/docs/loops.md +288 -59
  12. package/docs/mcp.md +22 -14
  13. package/docs/recipes.md +6 -0
  14. package/docs/screenshot.png +0 -0
  15. package/docs/triggers.md +120 -26
  16. package/docs/troubleshooting.md +37 -26
  17. package/docs/web-ui-parity.md +691 -65
  18. package/package.json +4 -3
  19. package/recipes/autoresearch/recipe.toml +1 -0
  20. package/recipes/changelog-draft/recipe.toml +1 -0
  21. package/recipes/ci-sweeper/recipe.toml +1 -0
  22. package/recipes/daily-digest/recipe.toml +1 -0
  23. package/recipes/deps-sweeper/recipe.toml +1 -0
  24. package/recipes/ecosystem/recipe.toml +1 -0
  25. package/recipes/issue-loop/recipe.toml +1 -0
  26. package/recipes/pr-watch/recipe.toml +1 -0
  27. package/skills/pi-loops/SKILL.md +6 -4
  28. package/src/activity.ts +199 -0
  29. package/src/archive.ts +21 -9
  30. package/src/args.ts +6 -2
  31. package/src/cli-entry.mjs +12 -3
  32. package/src/cli.ts +746 -125
  33. package/src/config.ts +53 -5
  34. package/src/conversation-rows.ts +469 -0
  35. package/src/danger.ts +227 -6
  36. package/src/engine.ts +1240 -0
  37. package/src/goal.ts +71 -0
  38. package/src/held-read.ts +27 -0
  39. package/src/hooks.ts +49 -18
  40. package/src/host-control-channel.ts +455 -40
  41. package/src/host-control.ts +602 -111
  42. package/src/host-entry.mjs +5 -3
  43. package/src/host-runtime.ts +455 -41
  44. package/src/host.ts +659 -123
  45. package/src/inbox-card.ts +278 -0
  46. package/src/inbox.ts +139 -62
  47. package/src/job-edit.ts +7 -1
  48. package/src/job-health.ts +14 -2
  49. package/src/job-lines.ts +201 -13
  50. package/src/job-owner.ts +74 -13
  51. package/src/job-tune.ts +110 -0
  52. package/src/lease.ts +537 -0
  53. package/src/lock.ts +7 -2
  54. package/src/mcp-pool.ts +66 -14
  55. package/src/mcp.ts +1 -1
  56. package/src/open-file.ts +54 -0
  57. package/src/page-children.ts +1507 -0
  58. package/src/page-devices.ts +192 -0
  59. package/src/page-server.ts +3150 -0
  60. package/src/path-precheck.ts +215 -0
  61. package/src/paths.ts +24 -0
  62. package/src/pi-floor.ts +10 -0
  63. package/src/pi-loops.ts +1301 -450
  64. package/src/pi-resolver.d.mts +1 -0
  65. package/src/pi-resolver.mjs +110 -10
  66. package/src/presence.ts +12 -63
  67. package/src/protocol.ts +10 -1
  68. package/src/recipe.ts +43 -1
  69. package/src/requests.ts +354 -0
  70. package/src/rule-lines.ts +68 -0
  71. package/src/run-card.ts +105 -0
  72. package/src/runner.ts +18 -1
  73. package/src/schedule.ts +1 -2
  74. package/src/scheduler.ts +598 -245
  75. package/src/sdk-runner.ts +321 -27
  76. package/src/seen.ts +278 -0
  77. package/src/session-head.ts +21 -0
  78. package/src/share.ts +12 -2
  79. package/src/snapshot.ts +3 -2
  80. package/src/store.ts +81 -8
  81. package/src/tools.ts +196 -65
  82. package/src/trigger-runtime.ts +164 -127
  83. package/src/triggers.ts +39 -7
  84. package/src/trust.ts +46 -2
  85. package/src/ui-prefs.ts +16 -5
  86. package/src/watch-plan.ts +174 -0
  87. package/src/web.d.mts +3 -0
  88. package/src/web.mjs +2260 -2340
  89. package/src/window-executor.ts +459 -0
  90. package/src/window-guard.ts +158 -0
  91. package/docs/agents/issue-tracker.md +0 -34
package/docs/cli.md CHANGED
@@ -1,49 +1,99 @@
1
1
  # The `pi-loops` command line
2
2
 
3
- `pi-loops` starts a session. With no arguments it opens the browser front end when you are at a
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 # start a session, in whichever window makes sense here
12
+ pi-loops # open a conversation, in whichever window makes sense here
9
13
  pi-loops --tui # the terminal one
10
- pi-loops --web --port 4200 # the browser one, on a port you chose
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
- Both are complete pi sessions: the browser one runs `pi --mode rpc` behind a page ([src/web.mjs](../src/web.mjs)),
15
- so the session file, `--resume`, your models, tools and extensions are the same either way. What
16
- the browser cannot do is pi's own built-in slash commands, which do not exist in that mode, and
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
- Ctrl-C in the terminal you started it from ends the session the way `/quit` does: pi is asked to
20
- shut down rather than killed, so it still hands the clock to a headless host when there are loops,
21
- rules or MCP pushes to keep running, and the line saying so is printed before the window closes. That
22
- line is pi's own when it arrives in time — pi addresses it to the window rather than to the terminal,
23
- so when it does not, the front end reads `host.json` itself and says either `automation handed to a
24
- background host (pid N); pi-loops host status | stop` or `no background host started; automation is
25
- not running — see <loops dir>/host.log`.
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
- Always `http://127.0.0.1:4173/`. Bookmark it. The port is fixed rather than "whatever was free",
30
- and the token that guards it lives in `~/.pi/agent/loops/web-token` (mode 0600) rather than being
31
- made fresh every launch — so the address is the same one tomorrow, and the first visit leaves a
32
- cookie that means you never see the token again. `--port <n>` moves it if 4173 is spoken for.
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
- There is a token at all because anything that reaches this server gets your whole session, and
35
- "anything" includes a website you have open in another tab: it cannot read the answers, but
36
- without a check it could still tell your agent what to do. The cookie is `SameSite=Strict`, which
37
- is a browser's promise not to send it on anything another site started, so the cost of this to you
38
- is one visit and then nothing.
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
- Running `pi-loops` a second time while one is up does not fail on the busy port — it opens the
41
- window that is already there and leaves.
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 front end binds loopback, which a phone cannot reach. Two ways round it, and the first is
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.** `pi-loops --host 0.0.0.0` binds every interface and prints the addresses a
59
- phone on the same wifi can use. Anything that can open the port is then talking to your session, so
60
- the token is not optional here — `--no-auth` is refused outright in this mode.
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, so `--no-auth` refuses to serve anything but loopback no matter which
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. The terminal prints a code at
70
- startup too.
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 `--host` is plain http: the cookie it hands out carries
81
- a token that outlives the process, and anyone on the wire can read it. That is the case
82
- `tailscale serve` exists for, and the startup output says so.
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. Start a session from a terminal on this machine and it will open a window that works from then
99
- on, or use the `?token=…` address the terminal printed.
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
- ### It starts a session; it does not attach to one
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
- The browser front end starts its own `pi --mode rpc`. It does not join a pi you already have open
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
- Picking up where you left off is a different thing, and pi already has it. The flags reach pi
108
- unchanged, so:
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 session in this directory, in the browser
112
- pi-loops --resume # pick one
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
- That also means you can hand a session between windows: quit the terminal one, `pi-loops --continue`,
117
- and carry on in the browser with the same transcript.
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 — the scheduler stops and starts with the session rather than
138
- handing the clock to the headless host, and a scheduled run the swap interrupts gives its slot back
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 the terminal says which model it could not go back to.
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 the headless host does. `PI_LOOPS_DIR` selects the loops directory, as everywhere.
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 host status | abort <run-id|trace-id> | stop
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
- ## host
343
+ ## pi-loops in the background
272
344
 
273
- The headless host has no chat, so `host status` is how you see what it is doing while no pi is
274
- open: the loop runs and trigger checks in flight, what is enabled, the inbox count, and each MCP
275
- server's state.
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 host status
279
- background host
280
- pid 2949521 on box, started 2026-09-09T13:33:16.652+08:00, model openai-codex/gpt-5.5
281
- owns the clock · 1/1 loop(s), 0/0 rule(s) enabled · inbox: 0 new
282
- running nightly (run-97ffb250) since 2026-09-09T13:34:16.676+08:00: check the repo issues
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 stamps carry the offset of the machine the host runs on ([loops.md](loops.md#time-and-which-clock-it-is));
286
- these are a `+08:00` machine's.
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
- `host abort <id>` interrupts one run or trigger check (the shortened ids the status prints are
289
- accepted). `host stop` ends the host; the next pi to open would have taken the clock back anyway.
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
- The channel is a unix socket at `host.sock` in the loops directory, created 0600, and it is
292
- read-mostly on purpose: a host you could prompt would be a second chat.
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. Without `--json` the same,
320
- one line per finding, for a person. The JSON is fixed in [CONTRACT.md](../CONTRACT.md) §2.2 and shown in [downstream.md](downstream.md), which is
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.