@alphacoder-v0/pi-loops 0.24.0 → 0.24.2
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 +136 -0
- package/README.md +37 -30
- package/README.zh-CN.md +4 -3
- package/docs/cli.md +104 -28
- package/docs/configuration.md +2 -2
- package/docs/design.md +12 -7
- package/docs/triggers.md +11 -7
- package/docs/troubleshooting.md +4 -2
- package/docs/web-ui-parity.md +18 -6
- package/package.json +1 -1
- package/skills/pi-loops/SKILL.md +2 -0
- package/src/activity.ts +4 -3
- package/src/browser-open.ts +169 -0
- package/src/cli.ts +300 -95
- package/src/engine.ts +4 -3
- package/src/host-control-channel.ts +2 -1
- package/src/host-control.ts +16 -1
- package/src/host-runtime.ts +11 -11
- package/src/inbox-card.ts +2 -2
- package/src/inbox.ts +7 -0
- package/src/job-lines.ts +1 -5
- package/src/job-signal.ts +2 -1
- package/src/mcp.ts +2 -0
- package/src/page-children.ts +3 -58
- package/src/page-server.ts +71 -24
- package/src/path-precheck.ts +2 -0
- package/src/paths.ts +15 -2
- package/src/pi-loops.ts +82 -35
- package/src/plural.ts +4 -0
- package/src/run-card.ts +3 -2
- package/src/schedule.ts +20 -0
- package/src/seen.ts +1 -1
- package/src/share.ts +1 -1
- package/src/store.ts +11 -2
- package/src/tools.ts +18 -19
- package/src/triggers.ts +2 -3
- package/src/trust.ts +1 -11
- package/src/ui-prefs.ts +6 -2
- package/src/web.mjs +70 -38
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,142 @@ All notable changes to pi-loops are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.24.2] - 2026-11-11
|
|
9
|
+
|
|
10
|
+
Fixes from the 0.24.2 milestone and a review of what merged since 0.24.1: a path rule that never
|
|
11
|
+
fired, faster `pi-loops` commands, page edges that showed what nobody could use, and fewer tools
|
|
12
|
+
offered to a sub-agent that could only fail.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **A path rule with an aside in parentheses is checked by the model.** The path precheck dropped
|
|
17
|
+
a trailing parenthetical, so `/tmp/a exists (or /tmp/b exists)` waited on `/tmp/a` alone and never
|
|
18
|
+
fired when only `/tmp/b` appeared. No word list can tell such an aside from a harmless one, so a
|
|
19
|
+
condition with one is model-checked again, and `new_trigger` asks the model to keep the person's
|
|
20
|
+
words with nothing added.
|
|
21
|
+
- **Every `pi-loops` command answers in a fraction of a second.** The command line loaded pi's SDK
|
|
22
|
+
on every call, about a second of each `status`, `stop` and `--help`; it now loads it only for
|
|
23
|
+
`import`, the one command that needs it. `pi-loops --version` went from about 1.2 s to 0.17 s here
|
|
24
|
+
(#168).
|
|
25
|
+
- **The page's inbox matches each project once per request.** `GET /inbox` resolved a finding's
|
|
26
|
+
directory on disk for every finding, in three passes; 2000 findings took about 24 ms a pass.
|
|
27
|
+
Each distinct directory is now resolved once per request (#169).
|
|
28
|
+
- **Logs name a conversation the way every screen does.** pi-loops' log lines cut a session id to
|
|
29
|
+
8 characters while the page, the terminal and the CLI show 13, so an id copied from the page was
|
|
30
|
+
not found in the log (#177).
|
|
31
|
+
- **Starting or resuming a conversation in the page says why, once.** The new session's events
|
|
32
|
+
arrived before the page moved to its id, so the page read the old id's history (which the server
|
|
33
|
+
opened in a pi of its own) and reloaded again on each event after, ending on "the session
|
|
34
|
+
restarted". The page now waits for the answer, moves to the id it names, and reloads once (#198).
|
|
35
|
+
- **A stopped MCP connection pushes nothing more.** A push read from the server just before the
|
|
36
|
+
connection stopped could still reach the host after it, and its trust check then read a project
|
|
37
|
+
directory that might be gone (#188).
|
|
38
|
+
- **An upgrade no longer holds a quit, or the page opening, in silence.** `pi-loops` opening the
|
|
39
|
+
page during an upgrade waited up to 180 s for the older process with nothing said; it now prints
|
|
40
|
+
`waiting up to 2 min for a turn in the browser page to finish…` when the wait passes 2 s. A pi
|
|
41
|
+
window that is quitting asks for the upgrade and leaves at once (#165).
|
|
42
|
+
- **Two page edges no longer show something nobody can use.** An unknown conversation address
|
|
43
|
+
answers with a page and a link back, not JSON. On a page only this machine can reach, Add a
|
|
44
|
+
device no longer shows a pairing code: it says how to make the page reachable, and retires the
|
|
45
|
+
code the server made (#157).
|
|
46
|
+
- **A conversation opened in the page loads pi-loops when the launch named its entry file.** A new
|
|
47
|
+
conversation in a project with no arguments of its own takes the `-e` that loads pi-loops from
|
|
48
|
+
another page pi. `-e <package>/src/extension-entry.ts` and `--extension=<path>` were not
|
|
49
|
+
recognised, so that conversation started without pi-loops (#167).
|
|
50
|
+
- **The page token is made once, even by two first starts at once.** Two windows that started
|
|
51
|
+
pi-loops together could each write a token, and one then exited. The token and its marker are
|
|
52
|
+
now written under a lock. A `web-token.v024` that is a symlink is refused before the token is
|
|
53
|
+
touched; before, it changed the token on every start and then failed (#144).
|
|
54
|
+
- **A loop's finding can no longer pass for an undelivered prompt.** A prompt that never reached its
|
|
55
|
+
conversation was told from a loop's finding by its text alone, so a loop that wrote
|
|
56
|
+
`could not be delivered (…): <text>` had its text carried out on Yes. The engine now marks a dead
|
|
57
|
+
letter with `"undelivered": true`, and only that field changes what a claim asks (#143).
|
|
58
|
+
- **A `$HOME` reached through a symlink is never one project.** Before, a session started in such a
|
|
59
|
+
`$HOME` lent its trust to loop runs in every project under it, and the create tools accepted any
|
|
60
|
+
directory under it as the session's own (#142).
|
|
61
|
+
- **Watch for… knows more time words.** "review deps quarterly", "check CI this afternoon",
|
|
62
|
+
"at nine", "in a few minutes", "next week", "biweekly" and "今晚" were told they read like
|
|
63
|
+
something to do now, and nothing was set up. They are now watches. A word that only starts like a
|
|
64
|
+
weekday or a time word ("montage", "sunset", "everyone") no longer reads as one (#146).
|
|
65
|
+
- **`pi-loops --tui --loops-dir <dir>` uses that directory.** The terminal's extension now gets it
|
|
66
|
+
as `PI_LOOPS_DIR`; before, it used the default. `--open` goes to pi like any flag pi-loops does
|
|
67
|
+
not read, and `--port`, `--host` and `--allow-host` with `--tui` are dropped, not passed to pi as
|
|
68
|
+
the 0.24.0 notes said (#145).
|
|
69
|
+
- **`pi-loops --version` prints the pi-loops version,** not pi's. `pi-loops --tui -- --version` asks pi
|
|
70
|
+
(#148).
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **The Loops pane says each thing once.** A named loop shows no prompt peek (the body has the
|
|
75
|
+
prompt), a failing loop no `quiet ×N` under its error, and the open row no id or cwd. Common cron
|
|
76
|
+
schedules read in words (`daily 09:00`, `weekdays 18:30`, `weekly Mon 09:00`, `every hour`). A
|
|
77
|
+
rule row is titled by its condition. The activity strip's "in chat" follows where a run's card
|
|
78
|
+
went: the conversation that asked for the run, not the one that made its loop (#154).
|
|
79
|
+
- **The page header and composer say less.** The header's clear button did what + New does in
|
|
80
|
+
this project, so it is gone (`/clear` and `/new` still work). The page says "conversation" where
|
|
81
|
+
it said "session". The cost badge appears once there is a cost. The composer says "Message pi".
|
|
82
|
+
The theme button's name is its visible state (#156).
|
|
83
|
+
- **`cron_remove` asks once.** It had a `confirm` parameter: the model asked the person in chat,
|
|
84
|
+
then the dialog asked again. The parameter is gone, and one call asks through the dialog.
|
|
85
|
+
- **A sub-agent is not offered `new_trigger`, `remove_trigger` or `cron_remove`.** Every call of
|
|
86
|
+
each was denied at hop 1, so the tools only invited a failed first try (#159, #212).
|
|
87
|
+
- **The agent is told more of what it can use.** `cron_create`'s schedule names `hourly`, `daily`
|
|
88
|
+
(09:00), `weekly` (Monday 09:00) and that `@daily` is midnight. SKILL.md names the inbox
|
|
89
|
+
commands a shell can run. The `catch_up` and `spec` descriptions are shorter (#160).
|
|
90
|
+
|
|
91
|
+
## [0.24.1] - 2026-11-10
|
|
92
|
+
|
|
93
|
+
Two first-run fixes: the `pi-loops` command exists after the first pi session with no question, and
|
|
94
|
+
the first browser gets into the page from one link on every platform, WSL included.
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
|
|
98
|
+
- **The first pi session after `pi install` puts `pi-loops` on your `PATH` without asking.** It
|
|
99
|
+
used to ask in a confirm dialog first. Now it writes the launcher into the first of
|
|
100
|
+
`~/.local/bin` and `/usr/local/bin` that is on your `PATH`, the way pi's own installer links
|
|
101
|
+
`pi`, and says so in one line: `[pi-loops] added pi-loops to ~/.local/bin; /pi-loops
|
|
102
|
+
uninstall-launcher removes it`. It writes only where nothing named `pi-loops` is there, and a
|
|
103
|
+
checkout never writes one. A link, even one that points nowhere, a directory, or a file it cannot
|
|
104
|
+
read counts as something there, and the file is created only if it does not exist, so the write
|
|
105
|
+
never goes through a link at that name or replaces a file; the directory itself may be a link to
|
|
106
|
+
a directory. A `pi-loops` counts as one that pi-loops wrote only when its first two lines are the
|
|
107
|
+
ones pi-loops writes, so `uninstall-launcher` never removes a script that only quotes them. It
|
|
108
|
+
writes only into a directory that is already there, and never makes `~/.local/bin`;
|
|
109
|
+
`install-launcher`, run by hand, still makes it. When the write fails, one warning line says why,
|
|
110
|
+
no half-written file is left, and no later session tries that directory again (#180).
|
|
111
|
+
|
|
112
|
+
Upgrade note: if you said no to the dialog before, that answer still holds and nothing is
|
|
113
|
+
written. If you never answered it, the next session writes the launcher.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- `pi-loops install-launcher --dir`, and `/pi-loops install-launcher --dir`, with no directory after
|
|
118
|
+
`--dir` is a usage error (exit 2). It used to write into the default directory, as if no `--dir`
|
|
119
|
+
had been given. `uninstall-launcher` reads `--dir` the same way.
|
|
120
|
+
- The first page visit works on every platform. On WSL the browser never opened, the terminal said
|
|
121
|
+
it had, and the printed address had no key, so Windows Chrome met a door that asked for a code
|
|
122
|
+
nobody could find. `pi-loops` now prints one link with a one-use key, opens the browser with a
|
|
123
|
+
per-platform opener (`rundll32.exe`, then `cmd.exe`, on WSL), and says `opened in your browser`
|
|
124
|
+
only once a browser has spent the key, because `rundll32` exits 0 even when nothing opened. When
|
|
125
|
+
the opener ran but no browser spent the key within 8 s, it says to open the link if no browser
|
|
126
|
+
opened. Otherwise, and on WSL when the loops directory's path holds a character Windows cannot
|
|
127
|
+
name, it says it could not open a browser and that the link works once, for 10 minutes (#179).
|
|
128
|
+
- The door names the ways in that a first browser can take: `pi-loops open` on this machine, and
|
|
129
|
+
`pi-loops pair` for another device.
|
|
130
|
+
|
|
131
|
+
### Added
|
|
132
|
+
|
|
133
|
+
- `pi-loops uninstall-launcher [--dir <dir>]` and `/pi-loops uninstall-launcher` remove the
|
|
134
|
+
launcher when pi-loops wrote it, and leave any other file named `pi-loops` as it is. No later
|
|
135
|
+
session writes the launcher again until `install-launcher` puts it back (#180).
|
|
136
|
+
- `pi-loops open [--loops-dir <dir>]` prints a fresh link that signs a browser in, for the
|
|
137
|
+
project's newest conversation, and opens the browser on it. With no terminal it still prints the
|
|
138
|
+
link with its key, and opens no browser.
|
|
139
|
+
- `/pi-loops web` opens the conversation you are in from inside pi (a new one in the project before
|
|
140
|
+
its first answer or under `--no-session`) and copies the link to the clipboard. It says
|
|
141
|
+
`copied to the clipboard` only when a clipboard command took the link. It needs no `pi-loops` on
|
|
142
|
+
your PATH.
|
|
143
|
+
|
|
8
144
|
## [0.24.0] - 2026-11-09
|
|
9
145
|
|
|
10
146
|
### Upgrade notes
|
package/README.md
CHANGED
|
@@ -46,11 +46,11 @@ one run with `pi -e .`, which installs nothing. Keep one copy, not two: two copi
|
|
|
46
46
|
same tools, and pi refuses to load the second — `Tool "cron_create" conflicts with …`, and it
|
|
47
47
|
exits.
|
|
48
48
|
|
|
49
|
-
Then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
Then start `pi` once, and that is the whole install. That first session puts the `pi-loops` command
|
|
50
|
+
on your `PATH` and says so in one line, so from then on `pi-loops` works from any shell (see
|
|
51
|
+
[The browser window, and the command line](#the-browser-window-and-the-command-line)). `/cron`,
|
|
52
|
+
`/inbox`, `/triggers` and `/goal` are registered by the extension itself, so they work with or
|
|
53
|
+
without that command. The package also ships a skill (`skills/pi-loops`), so the agent knows
|
|
54
54
|
when to reach for `cron_create`, `new_trigger` and the inbox on its own.
|
|
55
55
|
|
|
56
56
|
## Your first loop
|
|
@@ -178,24 +178,29 @@ you run `pi-loops start --foreground` from your own systemd unit ([docs/cli.md](
|
|
|
178
178
|
## The browser window, and the command line
|
|
179
179
|
|
|
180
180
|
`pi-loops` is the part that needs a launcher: `pi install` puts the package under pi's managed
|
|
181
|
-
directory rather than on your `PATH
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
directory rather than on your `PATH`. You do not fix that by hand. The first interactive pi session
|
|
182
|
+
after `pi install` writes `pi-loops` into the first of `~/.local/bin` and `/usr/local/bin` that is
|
|
183
|
+
already on your `PATH`, says so in one line, and keeps that launcher working afterwards if pi ever
|
|
184
|
+
moves the package:
|
|
185
185
|
|
|
186
186
|
```text
|
|
187
|
-
|
|
187
|
+
[pi-loops] added pi-loops to ~/.local/bin; /pi-loops uninstall-launcher removes it
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
It never replaces a `pi-loops` it did not write. When neither directory is on your `PATH`, or on a
|
|
191
|
+
machine where pi itself is never opened, write it yourself and pass `--dir <dir>` to say where:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
/pi-loops install-launcher --dir ~/bin # from inside pi, where the extension is already loaded
|
|
188
195
|
```
|
|
189
196
|
|
|
190
197
|
```bash
|
|
191
|
-
# or from a shell
|
|
198
|
+
# or from a shell — the same command, by absolute path
|
|
192
199
|
cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops # installed from npm
|
|
193
200
|
cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops # installed from GitHub
|
|
194
|
-
node src/cli-entry.mjs install-launcher
|
|
201
|
+
node src/cli-entry.mjs install-launcher --dir ~/bin
|
|
195
202
|
```
|
|
196
203
|
|
|
197
|
-
Any of them writes a launcher into the first of `~/.local/bin` and `/usr/local/bin` that is already
|
|
198
|
-
on your `PATH`, and refuses rather than guessing if neither is — pass `--dir <dir>` to say where.
|
|
199
204
|
After that, `pi-loops` works from anywhere:
|
|
200
205
|
|
|
201
206
|
```bash
|
|
@@ -227,8 +232,14 @@ deletes anything; the session you leave is a file that `resume` lists.
|
|
|
227
232
|
The page is served by pi-loops in the background on **`http://127.0.0.1:4173/`**, so `pi-loops`
|
|
228
233
|
opens it and exits, and the page stays open when that terminal closes. Its token lives in a file, so
|
|
229
234
|
the address is the same one tomorrow and is worth bookmarking (`[page] port` in `config.toml` moves
|
|
230
|
-
it if 4173 is spoken for).
|
|
231
|
-
|
|
235
|
+
it if 4173 is spoken for). `pi-loops` prints one link with a one-use key and opens your browser on
|
|
236
|
+
it, on macOS, Windows, WSL and Linux; when no browser opens, paste that link. The first visit leaves
|
|
237
|
+
a cookie and you never see the token again. There is no way to drop the token: the page is always
|
|
238
|
+
on, so it is always behind it.
|
|
239
|
+
|
|
240
|
+
A browser that shows "This browser has not been here before." needs a fresh link: run
|
|
241
|
+
`pi-loops open` in a terminal, or `/pi-loops web` inside pi, which also copies the link to the
|
|
242
|
+
clipboard and needs no `pi-loops` on your PATH.
|
|
232
243
|
|
|
233
244
|
From a phone, the best route is `tailscale serve --bg 4173`: this server stays on loopback and the
|
|
234
245
|
tailnet does TLS and identity. On the same wifi, `[page] host = "0.0.0.0"` in `config.toml` works
|
|
@@ -258,22 +269,18 @@ says what is in each one.
|
|
|
258
269
|
|
|
259
270
|
### Uninstall
|
|
260
271
|
|
|
261
|
-
Stop pi-loops
|
|
262
|
-
is removed, and nothing else stops it
|
|
272
|
+
Stop pi-loops and remove the `pi-loops` launcher before removing the extension. pi-loops keeps
|
|
273
|
+
running in the background after the extension is removed, and nothing else stops it; the launcher
|
|
274
|
+
needs the package to run. `uninstall-launcher` removes only a launcher that pi-loops wrote:
|
|
263
275
|
|
|
264
276
|
```bash
|
|
265
277
|
pi-loops stop
|
|
278
|
+
pi-loops uninstall-launcher # add --dir <dir> if install-launcher --dir <dir> wrote it
|
|
266
279
|
pi remove npm:@alphacoder-v0/pi-loops # or git:github.com/alphacoder-v0/pi-loops, or the checkout path — whatever you installed
|
|
267
280
|
```
|
|
268
281
|
|
|
269
|
-
State stays in `~/.pi/agent/loops` until you delete it.
|
|
270
|
-
command
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
rm ~/.local/bin/pi-loops # or /usr/local/bin/pi-loops, if ~/.local/bin was not on your PATH
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
If you gave `install-launcher --dir <dir>`, the launcher is `<dir>/pi-loops`.
|
|
282
|
+
State stays in `~/.pi/agent/loops` until you delete it. After a reinstall, `/pi-loops install-launcher`
|
|
283
|
+
in pi puts the command back, because a removed launcher is not written again by itself.
|
|
277
284
|
|
|
278
285
|
To go back to 0.22.8, stop pi-loops while this version is still installed, then install 0.22.8:
|
|
279
286
|
|
|
@@ -325,7 +332,8 @@ pi-loops stays stopped until `pi-loops start`, or until `pi-loops` opens the bro
|
|
|
325
332
|
| `/session-export [path]`, `/session-import <path>` | Portable `.pisession` archive: transcript + jobs + rules + loop state (in the browser front end, `/session export` and `/session import` too; `/sessions` lists) |
|
|
326
333
|
| `/session-share [--public]` | Upload a redacted transcript as a GitHub gist via `gh`, after showing you what it contains. (pi has its own `/share`, which sends the raw session elsewhere first — see [docs/session-archive.md](docs/session-archive.md)) |
|
|
327
334
|
| `/recipe [list\|show\|add\|update\|remove <name>]` | Install a packaged way of running this project on loops — the issue tracker as a state machine, or one research experiment per run — with one question and one confirmation ([docs/recipes.md](docs/recipes.md)) |
|
|
328
|
-
| `/pi-loops
|
|
335
|
+
| `/pi-loops web` | Open this conversation in the browser page, and copy its one-use link |
|
|
336
|
+
| `/pi-loops [install-launcher \| uninstall-launcher]` | Version and paths; `install-launcher` puts the `pi-loops` command on your `PATH`, `uninstall-launcher` removes it |
|
|
329
337
|
|
|
330
338
|
A schedule is a 5-field cron expression, or one of `hourly` / `daily` / `weekly` (also `每天`), or
|
|
331
339
|
`every 30m`, `in 10m`, `at <ISO>`. The `@hourly` / `@daily` / `@weekly` / `@monthly` spellings work
|
|
@@ -337,9 +345,8 @@ unambiguous prefix of it, or the `--name` you gave it — and `/crontab` and `/l
|
|
|
337
345
|
|
|
338
346
|
Tools for the model: `cron_create`, `cron_list`, `cron_remove`, `set_cron_job_state`,
|
|
339
347
|
`new_trigger`, `list_triggers`, `remove_trigger`, `set_trigger_state`, plus every tool of every
|
|
340
|
-
configured MCP server. Five of them stop and ask you: creating or removing a trigger,
|
|
341
|
-
cron job
|
|
342
|
-
and any state change that turns automation *on*, or touches another project's. They are the
|
|
348
|
+
configured MCP server. Five of them stop and ask you, once each: creating or removing a trigger,
|
|
349
|
+
removing a cron job, and any state change that turns automation *on*, or touches another project's. They are the
|
|
343
350
|
operations that decide what runs while nobody is watching.
|
|
344
351
|
|
|
345
352
|
## Documentation
|
package/README.zh-CN.md
CHANGED
|
@@ -51,7 +51,7 @@ pi install npm:@alphacoder-v0/pi-loops
|
|
|
51
51
|
|
|
52
52
|
重启 pi,装到这里就够了——那四个斜杠命令现在就能用。下面这段只跟 `pi-loops` 这个命令有关:浏览器窗口和几个 shell 子命令靠它,不要浏览器窗口可以先跳过。
|
|
53
53
|
|
|
54
|
-
`pi install` 把包放在 pi 自己的托管目录里、**不进 `PATH`**,所以此刻 `pi-loops` 这个命令还不存在。这一步不用你自己动手:`pi install`
|
|
54
|
+
`pi install` 把包放在 pi 自己的托管目录里、**不进 `PATH`**,所以此刻 `pi-loops` 这个命令还不存在。这一步不用你自己动手:`pi install` 之后第一次打开 pi,它会直接把 `pi-loops` 放进 `PATH` 上已有的目录,并用一行告诉你放在哪、怎么删;之后也一直替你维护着——包被 pi 挪到另一个位置,下一个会话会把启动器改回能用的样子,并用一行告诉你改了什么。不想要的话,`pi-loops uninstall-launcher` 删掉它,以后也不会再写;改了主意,`/pi-loops install-launcher` 随时给你写一个:
|
|
55
55
|
|
|
56
56
|
```text
|
|
57
57
|
/pi-loops install-launcher # 在 pi 里面,扩展本来就加载着
|
|
@@ -162,17 +162,18 @@ pi-loops upgrade --check # 只看看有没有新的
|
|
|
162
162
|
|
|
163
163
|
## 卸载
|
|
164
164
|
|
|
165
|
-
卸载扩展前先停止 pi-loops
|
|
165
|
+
卸载扩展前先停止 pi-loops,并删掉 `pi-loops` 启动器(`uninstall-launcher` 只删 pi-loops 自己写的那个):
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
168
|
pi-loops stop
|
|
169
|
+
pi-loops uninstall-launcher
|
|
169
170
|
pi remove npm:@alphacoder-v0/pi-loops # 当初怎么装的就怎么删:npm: 包名、git: ref 或检出路径
|
|
170
171
|
pi remove git:github.com/alphacoder-v0/pi-loops
|
|
171
172
|
pi -e /path/to/pi-loops # 或者:只在这次启动试用,什么都不装
|
|
172
173
|
pi update --extensions # 对齐已安装的包
|
|
173
174
|
```
|
|
174
175
|
|
|
175
|
-
数据留在 `~/.pi/agent/loops
|
|
176
|
+
数据留在 `~/.pi/agent/loops`,想清就删目录。以后重新安装时,在 pi 里运行 `/pi-loops install-launcher` 把命令放回来:删掉的启动器不会再自动写回。
|
|
176
177
|
|
|
177
178
|
要退回 0.22.8:趁 0.23 还装着,先停止 pi-loops,再安装 0.22.8:
|
|
178
179
|
|
package/docs/cli.md
CHANGED
|
@@ -18,31 +18,68 @@ pi-loops --model anthropic/claude-opus-5 -e . # anything it does not recognise
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
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
|
|
22
|
-
exits:
|
|
21
|
+
in the background if it is not running, asks it to open the conversation, prints a link, opens your
|
|
22
|
+
browser and exits:
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
$ pi-loops
|
|
26
26
|
started pi-loops in the background
|
|
27
|
-
pi-loops
|
|
28
|
-
|
|
27
|
+
pi-loops: http://127.0.0.1:4173/c/424aba43-…/?open=c0ff…
|
|
28
|
+
opened in your browser
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The first line appears only when this command started pi-loops. The
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
The first line appears only when this command started pi-loops. The link holds a one-use key that
|
|
32
|
+
works for 10 minutes, never the token, because what a terminal prints stays in its scrollback. It is
|
|
33
|
+
printed before the browser is tried, so a browser on this machine that did not open gets in with a
|
|
34
|
+
paste. The last line says what happened:
|
|
35
|
+
|
|
36
|
+
- ` opened in your browser`: a browser loaded the page and spent the launch key within 8 s,
|
|
37
|
+
watched while the opener runs. The opener is `open` on macOS,
|
|
38
|
+
`rundll32 url.dll,FileProtocolHandler` on Windows, `rundll32.exe url.dll,FileProtocolHandler` on
|
|
39
|
+
WSL (`WSL_DISTRO_NAME` set, or a kernel release that names Microsoft; then `cmd.exe /c start`
|
|
40
|
+
when `rundll32.exe` cannot start), and `xdg-open` on other Linux, which gets 10 s because it takes
|
|
41
|
+
about 5 s to give up when there is no browser. An opener's exit code is not enough: `rundll32`
|
|
42
|
+
exits 0 for a target it cannot open.
|
|
43
|
+
- ` if no browser opened, open the link above (it works once, for 10 minutes)`: the opener ran, but
|
|
44
|
+
no browser spent the key within 8 s.
|
|
45
|
+
- ` could not open a browser here; open the link above (it works once, for 10 minutes)`: every
|
|
46
|
+
opener failed, or Linux has no `DISPLAY` or `WAYLAND_DISPLAY`.
|
|
47
|
+
- ` open the link above in a browser on this machine`: over ssh or mosh, where nothing is opened.
|
|
48
|
+
- ` open the link above in a browser (it works once, for 10 minutes)`: with `--no-open`.
|
|
49
|
+
|
|
50
|
+
The browser is opened on a file that only you can read, under the loops directory's `pages/open/`,
|
|
51
|
+
which sends it to the conversation with a second one-use key; that key is in no command line, and
|
|
52
|
+
the file is removed once the browser has used it, or after a minute. On WSL the Windows browser
|
|
53
|
+
reads that file as `file://wsl.localhost/<distro>/…`, which it cannot do when the loops directory's
|
|
54
|
+
path holds a character Windows does not allow in a name (`\ : * ? " < > |`); then the last line says
|
|
55
|
+
it could not open a browser. A browser confined by snap or flatpak
|
|
56
|
+
(Ubuntu's default Firefox is one) cannot read a hidden directory such as `~/.pi`, so it shows an
|
|
57
|
+
error instead: use the printed link. With no terminal (stdout piped or redirected), no key is
|
|
58
|
+
printed, because a file would keep it: the link is the page's address, and the next line is
|
|
59
|
+
` pi-loops open prints a link that signs a browser in`. A conversation the page already has open
|
|
60
|
+
is not started again, so pi arguments given for it are not applied, and the last line says which:
|
|
42
61
|
` this conversation was already open; --model was not applied`. Opening can wait up to 15 s for
|
|
43
62
|
an older pi in that conversation to stop; after 3 s the command says
|
|
44
63
|
`waiting for pi-loops to open the conversation…` on stderr, and it waits up to 30 s in all.
|
|
45
64
|
|
|
65
|
+
### A fresh link: `pi-loops open` and `/pi-loops web`
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
pi-loops open [--cwd <dir>] [--loops-dir <dir>]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
opens the page on the project's newest conversation (a new one when the project has none), with
|
|
72
|
+
the same output as `pi-loops`, except that it prints the link with its key with no terminal too,
|
|
73
|
+
because that link is what it is for; it opens no browser then. `--loops-dir` names the loops
|
|
74
|
+
directory, as it does for `pi-loops` and as `PI_LOOPS_DIR` does. Run it when a browser shows the door ("This browser has not been
|
|
75
|
+
here before."), or when the printed link has been used or is older than 10 minutes. Inside pi,
|
|
76
|
+
`/pi-loops web` does the same for the conversation you are in (a new one in the project when pi
|
|
77
|
+
has saved nothing of it yet, before its first answer or under `--no-session`), shows the same
|
|
78
|
+
lines, and copies the link to the clipboard (on WSL, the Windows clipboard). It adds
|
|
79
|
+
` copied to the clipboard` only when `pbcopy`, `clip`, `clip.exe`, `wl-copy`, `xclip` or `xsel`
|
|
80
|
+
said it took the link; a copy through the terminal (OSC 52), which no terminal confirms, is tried
|
|
81
|
+
without a line. It needs no `pi-loops` on your PATH.
|
|
82
|
+
|
|
46
83
|
Both windows are complete pi sessions: each conversation in the page is a `pi --mode rpc` that
|
|
47
84
|
pi-loops runs for it, so the session file, your models, tools and extensions are the same either
|
|
48
85
|
way. What the browser cannot do is pi's own built-in slash commands, which do not exist in that
|
|
@@ -219,16 +256,49 @@ by the id in its header.
|
|
|
219
256
|
|
|
220
257
|
`pi install` puts this package under pi's managed directory rather than on your `PATH`, so the
|
|
221
258
|
command that is meant to start your sessions is at first reachable only by absolute path. The
|
|
222
|
-
extension does that step for you: the first pi session
|
|
223
|
-
`
|
|
224
|
-
|
|
259
|
+
extension does that step for you, with no question: the first interactive pi session after
|
|
260
|
+
`pi install` writes `pi-loops` into the first of `~/.local/bin` and `/usr/local/bin` that is on your
|
|
261
|
+
`PATH`, and says so in one line:
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
[pi-loops] added pi-loops to ~/.local/bin; /pi-loops uninstall-launcher removes it
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
It writes only where nothing named `pi-loops` is there yet. A link, even one that points nowhere, a
|
|
268
|
+
directory, or a file it cannot read counts as something there, and the file is created only if it
|
|
269
|
+
does not exist, so the write never goes through a link at that name. The directory itself may be a
|
|
270
|
+
link to a directory, as a dotfiles `~/.local/bin` often is. A `pi-loops` this project did not write,
|
|
271
|
+
such as your own wrapper, is left alone, and so is one that runs another copy of this package still
|
|
272
|
+
on disk, which is that copy's to keep. With neither directory on your `PATH`, or with
|
|
273
|
+
the one on your `PATH` not made yet, nothing is written and nothing is said: a session start never
|
|
274
|
+
makes a directory. When the write fails, for example because `/usr/local/bin` belongs
|
|
275
|
+
to root, one warning line says why, and no later session tries that directory again; a `PATH` that
|
|
276
|
+
names another directory is a new try. A checkout never writes a launcher that is not there.
|
|
277
|
+
|
|
278
|
+
Afterwards the extension keeps that launcher current: if the copy it runs is not there any more
|
|
225
279
|
(pi installed the package by the other route), or that copy is the one now running and the node has
|
|
226
|
-
changed, the next session rewrites it and says so in one line.
|
|
227
|
-
write — your own wrapper — is left alone, and so is one that runs another copy of this package still
|
|
228
|
-
on disk, which is that copy's to keep. A checkout is never asked to write a launcher that is not
|
|
229
|
-
there, though one written from a checkout is still kept working.
|
|
280
|
+
changed, the next session rewrites it and says so in one line.
|
|
230
281
|
|
|
231
|
-
To
|
|
282
|
+
To remove it, and stop every later session from writing it again:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
/pi-loops uninstall-launcher
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
pi-loops uninstall-launcher
|
|
290
|
+
pi-loops uninstall-launcher --dir ~/bin
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`--dir` needs a directory after it, here and on `install-launcher`: a `--dir` with nothing after it,
|
|
294
|
+
or with a flag after it, is a usage error (exit 2), not the default directory.
|
|
295
|
+
|
|
296
|
+
It removes only a launcher of ours that this copy keeps. For any other file named `pi-loops` it
|
|
297
|
+
says whose file it is, leaves it as it is, and exits 1. Either way it remembers the choice in
|
|
298
|
+
`ui.json`.
|
|
299
|
+
|
|
300
|
+
To write one yourself, after `uninstall-launcher` or into another directory, run it from inside pi,
|
|
301
|
+
where the extension is already loaded:
|
|
232
302
|
|
|
233
303
|
```text
|
|
234
304
|
/pi-loops install-launcher
|
|
@@ -243,6 +313,8 @@ node src/cli-entry.mjs install-launcher
|
|
|
243
313
|
node src/cli-entry.mjs install-launcher --dir ~/bin
|
|
244
314
|
```
|
|
245
315
|
|
|
316
|
+
`install-launcher` clears the choice `uninstall-launcher` remembered.
|
|
317
|
+
|
|
246
318
|
It writes a small `sh` script that names the node you ran it with and the package it lives in — a
|
|
247
319
|
launcher rather than a symlink. Neither path is trusted for ever: if the package is not where it
|
|
248
320
|
was, the launcher looks in both of the directories above (the ones pi installs into, under the agent
|
|
@@ -283,7 +355,7 @@ The rest of the command line does not need a pi session and has no build step; i
|
|
|
283
355
|
packages the way pi-loops does. `PI_LOOPS_DIR` selects the loops directory, as everywhere.
|
|
284
356
|
|
|
285
357
|
```
|
|
286
|
-
pi-loops sessions [--all] [--limit <n>]
|
|
358
|
+
pi-loops sessions [--all] [--limit <n>] [--cwd <dir>]
|
|
287
359
|
pi-loops inspect <file>
|
|
288
360
|
pi-loops export [--session <id>] [--cwd <dir>] [--output <file>] [--exclude-triggers]
|
|
289
361
|
pi-loops import <file> [--cwd <dir>] [--activate-triggers=off|ask|on]
|
|
@@ -293,7 +365,9 @@ pi-loops inbox list [--all] [--cwd <dir>] | claim <id> | dismiss <id> [--reason
|
|
|
293
365
|
```
|
|
294
366
|
|
|
295
367
|
`-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`
|
|
368
|
+
and exits 0 without doing anything else; `pi-loops -h`, `pi-loops help` and `pi-loops --help` are the same. After `--` they are pi's.
|
|
369
|
+
`pi-loops --version` (or `-v`) alone prints the pi-loops version, such as `pi-loops v0.24.2`;
|
|
370
|
+
`pi-loops --tui -- --version` prints pi's.
|
|
297
371
|
A command typed wrong, such as a missing archive path, a flag the command does not read
|
|
298
372
|
(`status does not take --json`) or a word it does not read (`export: unexpected argument "out.pisession"`),
|
|
299
373
|
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
|
|
@@ -306,7 +380,7 @@ in the session's control-plane audit.
|
|
|
306
380
|
|
|
307
381
|
## sessions, inspect
|
|
308
382
|
|
|
309
|
-
`sessions` lists this project's sessions newest first, one line each: the first sixteen
|
|
383
|
+
`sessions` lists this project's sessions (`--cwd <dir>` names another project) newest first, one line each: the first sixteen
|
|
310
384
|
characters of the id, when the session started, the automation it
|
|
311
385
|
has (`[2 cron, 1 trigger]`, or `[automation off]` when all of it is disabled), and what was first
|
|
312
386
|
said in it — the session's name if it was given one, otherwise the first message, cut at eighty
|
|
@@ -350,7 +424,7 @@ spellings `pi-loops host status|abort|stop|start` and `/cron host` still work.
|
|
|
350
424
|
$ pi-loops status
|
|
351
425
|
pi-loops
|
|
352
426
|
pid 2949521 on box, started 2026-09-09 13:33, model openai-codex/gpt-5.5
|
|
353
|
-
pi-loops 0.24.
|
|
427
|
+
pi-loops 0.24.2 · 1/1 loops, 0/0 rules enabled · inbox: 0 new
|
|
354
428
|
started by the shell that ran pi-loops
|
|
355
429
|
conversation 424aba43-0c1f "fix the flaky login test" open in pid 939915 (tui)
|
|
356
430
|
running nightly (run-97ffb250) since 2026-09-09 13:34: check the repo issues
|
|
@@ -375,7 +449,9 @@ start` starts it now, even with nothing to run yet. `pi-loops restart` stops it,
|
|
|
375
449
|
process has exited (SIGKILL after 180 seconds), and starts it. A restart, and an upgrade that a pi
|
|
376
450
|
window with a newer pi-loops asks for, first wait up to two minutes for a turn in the browser page to
|
|
377
451
|
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
|
|
452
|
+
to 2 min for a turn in the browser page to finish…`, and `pi-loops` opening the page prints the same
|
|
453
|
+
line when the older process has not left after 2 seconds. A pi window that is quitting asks for the
|
|
454
|
+
upgrade and does not wait: the next window or `pi-loops` starts the new version. A plain `pi-loops stop` ends it at once, and one
|
|
379
455
|
run during that wait ends the wait. A restart or an upgrade asked for while it already waits is
|
|
380
456
|
answered `already leaving` and does not start the wait again. With `--help`, each of these prints its
|
|
381
457
|
usage and does nothing else.
|
package/docs/configuration.md
CHANGED
|
@@ -8,7 +8,7 @@ Everything lives under `~/.pi/agent/loops/` (override: `PI_LOOPS_DIR`).
|
|
|
8
8
|
| `logs/pi-<pid>.log` | what each pi process diagnosed: jobs disabled, writes that failed, sub-agent warnings. Rotated at 2 MB, newest 5 processes kept |
|
|
9
9
|
| `mcp.toml` | MCP servers — see [mcp.md](mcp.md) |
|
|
10
10
|
| `hooks.toml` | lifecycle hooks — see [hooks.md](hooks.md) |
|
|
11
|
-
| `ui.json` | preferences that outlive a session: `panel` (written by `/cron panel on\|off`); `model` / `thinking` — the last ones you chose in the browser page, applied to the next session that does not say otherwise; `host` (`"stopped"` after a manual stop); `launcher` (
|
|
11
|
+
| `ui.json` | preferences that outlive a session: `panel` (written by `/cron panel on\|off`); `model` / `thinking` — the last ones you chose in the browser page, applied to the next session that does not say otherwise; `host` (`"stopped"` after a manual stop); `launcher` (`"declined"` after `uninstall-launcher`, so the launcher is not written again; `"failed"`, with `launcherFailedIn`, the directory a write failed in, so it is retried only when that changes); `heldJobsNotice` (the one-time notice about plain jobs of closed conversations); `lastPage` (the conversation `GET /` opens) |
|
|
12
12
|
| `jobs.json`, `triggers.json` | cron jobs and trigger rules (machine-global, each with a `cwd`) |
|
|
13
13
|
| `jobs.json.bak` | the last `jobs.json` that read cleanly, kept so a damaged store can be restored |
|
|
14
14
|
| `state/<id>.md` | loop notes; plain Markdown, editable |
|
|
@@ -30,7 +30,7 @@ Everything lives under `~/.pi/agent/loops/` (override: `PI_LOOPS_DIR`).
|
|
|
30
30
|
| `*.lock` | short-lived locks around writes |
|
|
31
31
|
| `polls.json` | last dynamic check per project (shared with 0.22 windows during an upgrade, so a project is not checked twice in one interval) |
|
|
32
32
|
| `dedup.json` | machine-wide trigger dedup window (5 minutes) |
|
|
33
|
-
| `web-token` | the browser page's token, mode 0600. It lives in a file rather than being made per start so the address stays the same one and a signed-in device stays signed in across restarts and upgrades. Delete it and run `pi-loops restart` to sign every device out. The first 0.24 start replaces a token from an earlier version once, and writes `web-token.v024` beside it to record that; a token you write into `web-token` after that is kept |
|
|
33
|
+
| `web-token` | the browser page's token, mode 0600. It lives in a file rather than being made per start so the address stays the same one and a signed-in device stays signed in across restarts and upgrades. Delete it and run `pi-loops restart` to sign every device out. The first 0.24 start replaces a token from an earlier version once, and writes `web-token.v024` beside it to record that; a token you write into `web-token` after that is kept. A `web-token.v024` that is not a regular file (a symlink, even a dangling one) stops pi-loops from starting, as a `web-token` symlink does |
|
|
34
34
|
|
|
35
35
|
Project-level: `<project>/.pi/mcp.toml` (trusted projects only) and `<project>/.pi/hooks.toml`
|
|
36
36
|
(when allowed).
|
package/docs/design.md
CHANGED
|
@@ -153,8 +153,9 @@ machine. Everything here follows from that, and each one has a price.
|
|
|
153
153
|
three is recorded in the control-plane audit, the self-removals included.
|
|
154
154
|
|
|
155
155
|
12. **Cycle suppression by hop count.** Sub-agents run at hop 1 and keep the cron and trigger tools
|
|
156
|
-
(`cron_create`,
|
|
157
|
-
|
|
156
|
+
(`cron_create`, listing, disabling), so a loop can manage automation; the tools whose every
|
|
157
|
+
call needs a human's yes are not offered there, the other operations that need one are
|
|
158
|
+
denied, and sub-sessions never run the
|
|
158
159
|
trigger runtime, so nothing nests.
|
|
159
160
|
|
|
160
161
|
13. **Stdio MCP servers reconnect** with backoff (20 attempts by default), each distinct error
|
|
@@ -236,11 +237,15 @@ machine. Everything here follows from that, and each one has a price.
|
|
|
236
237
|
the copy it names is this one and the node or the shape of the script has changed. Either way
|
|
237
238
|
one line says that it was rewritten and why. One naming another copy that is still there is that
|
|
238
239
|
copy's to keep current, so an install and a checkout both being opened never trade the file back
|
|
239
|
-
and forth. With no launcher anywhere, the
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
and forth. With no launcher anywhere, the first interactive session writes one into a directory
|
|
241
|
+
already on the PATH without asking, and says so in one line with the command that removes it,
|
|
242
|
+
because a confirm dialog and a second start cost more than the file does (0.24.1, #180).
|
|
243
|
+
`uninstall-launcher` removes only a launcher of ours and records `declined` in `ui.json`, so it
|
|
244
|
+
is not written again; `/pi-loops install-launcher` still writes one whenever you want it, and
|
|
245
|
+
clears that mark. A write that failed is recorded with its directory and not retried until the
|
|
246
|
+
target changes. A `pi-loops` carrying no marker of ours is somebody's own wrapper and is left
|
|
247
|
+
alone, and a checkout is never given one that is not there — nobody ran `pi install`. The
|
|
248
|
+
decision is `launcherAction`, `launcherState` and `refreshLauncher` in
|
|
244
249
|
[../src/cli.ts](../src/cli.ts), beside the one function that renders the script, so it is
|
|
245
250
|
testable without pi; the extension is the caller.
|
|
246
251
|
|
package/docs/triggers.md
CHANGED
|
@@ -54,6 +54,8 @@ noun only when a space sets it apart from the path: `~/备份文件 存在` wait
|
|
|
54
54
|
`once`, `当`, `如果` or `一旦` and an optional trailing `的时候`, `时` or `后`. `~` and `$HOME` are
|
|
55
55
|
resolved in the process that creates the rule, so they are your home even when pi-loops in the
|
|
56
56
|
background was started with another one; a relative path is resolved against the rule's project.
|
|
57
|
+
An aside in parentheses is a second clause: `/tmp/a exists (or /tmp/b exists)` is true while
|
|
58
|
+
`/tmp/a` is absent, so a condition with one is checked by the model.
|
|
57
59
|
|
|
58
60
|
Anything else is refused as path-only and checked by the model, as before: a glob, a second clause
|
|
59
61
|
(`~/x appears and it contains ERROR`, `~/a exists or ~/b exists`), another verb, a bare word with no
|
|
@@ -138,7 +140,7 @@ that instant (someone wrote it in the last two minutes), the row waits and pi-lo
|
|
|
138
140
|
later tick. A row whose delivery fails waits a minute, then five, and blocks nothing behind it; at
|
|
139
141
|
the third failure, or when the conversation's file is gone, it is a dead letter: a finding in the
|
|
140
142
|
inbox marked for the conversation, then a `request` audit row (`dead`), then the row is removed, in
|
|
141
|
-
that order so a crash in between loses nothing. A rule's result has its finding already (every match is one), so its dead letter files no second one; any other row's finding reads `could not be delivered (<why>): <prompt
|
|
143
|
+
that order so a crash in between loses nothing. A rule's result has its finding already (every match is one), so its dead letter files no second one; any other row's finding reads `could not be delivered (<why>): <prompt>` and carries `"undelivered": true`. Only that field makes a claim carry the prompt out; a loop's finding in the same words is claimed as a finding. A rule with no
|
|
142
144
|
creating conversation (made from a shell, imported, or made by a loop's run) sends results to the
|
|
143
145
|
project's open window as above, and to the inbox (`redirected`, which finds the match's finding and adds nothing) when none is open. One check evaluates every
|
|
144
146
|
rule of the project at once, so it runs under **one** model: the first rule that has one recorded
|
|
@@ -207,12 +209,14 @@ poll interval is unaffected: the next scheduled check happens when it would have
|
|
|
207
209
|
|
|
208
210
|
## Cycle safety
|
|
209
211
|
|
|
210
|
-
Cycles are bounded by a hop count: sub-agents run at hop 1 and still have
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
Cycles are bounded by a hop count: sub-agents run at hop 1 and still have
|
|
213
|
+
`cron_create`, `cron_list`, `set_cron_job_state`, `list_triggers` and `set_trigger_state`, so a
|
|
214
|
+
trigger action can schedule a job; they never run the trigger runtime, so nothing nests.
|
|
215
|
+
`new_trigger`, `remove_trigger` and `cron_remove` are not offered at hop 1: every call of each
|
|
216
|
+
needs a person, and a sub-agent has nobody to ask. The other
|
|
217
|
+
prompt-class operations — re-enabling a trigger or a cron job — are denied fail-closed in
|
|
218
|
+
sub-agents (no control-plane prompt channel there). `cron_create` works there, and so does disabling a job of the sub-agent's own
|
|
219
|
+
project; the control-plane audit records `actor: sub-agent`. Re-enabling a job and
|
|
216
220
|
touching another project's job are denied there like the other prompt-class operations. Sub-agents never handle triggers themselves: they ignore MCP pushes, and the
|
|
217
221
|
runtime audits anything reaching hop ≥ 1 as `cycle_suppressed` (hops are counted per trace, but the
|
|
218
222
|
bound that matters is structural: a sub-agent session never acts on a trigger at all).
|