@alphacoder-v0/pi-loops 0.22.8 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +717 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +114 -78
  4. package/README.zh-CN.md +62 -41
  5. package/docs/cli.md +352 -91
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +404 -61
  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 +41 -28
  17. package/docs/web-ui-parity.md +702 -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/browser-open.ts +169 -0
  32. package/src/cli-entry.mjs +12 -3
  33. package/src/cli.ts +960 -143
  34. package/src/config.ts +53 -5
  35. package/src/conversation-rows.ts +469 -0
  36. package/src/danger.ts +227 -6
  37. package/src/engine.ts +1240 -0
  38. package/src/goal.ts +71 -0
  39. package/src/held-read.ts +27 -0
  40. package/src/hooks.ts +49 -18
  41. package/src/host-control-channel.ts +455 -40
  42. package/src/host-control.ts +602 -111
  43. package/src/host-entry.mjs +5 -3
  44. package/src/host-runtime.ts +455 -42
  45. package/src/host.ts +659 -123
  46. package/src/inbox-card.ts +278 -0
  47. package/src/inbox.ts +139 -62
  48. package/src/job-edit.ts +7 -1
  49. package/src/job-health.ts +14 -2
  50. package/src/job-lines.ts +201 -13
  51. package/src/job-owner.ts +74 -13
  52. package/src/job-tune.ts +110 -0
  53. package/src/lease.ts +537 -0
  54. package/src/lock.ts +7 -2
  55. package/src/mcp-pool.ts +66 -14
  56. package/src/mcp.ts +1 -1
  57. package/src/open-file.ts +54 -0
  58. package/src/page-children.ts +1507 -0
  59. package/src/page-devices.ts +192 -0
  60. package/src/page-server.ts +3150 -0
  61. package/src/path-precheck.ts +215 -0
  62. package/src/paths.ts +24 -0
  63. package/src/pi-floor.ts +10 -0
  64. package/src/pi-loops.ts +1370 -473
  65. package/src/pi-resolver.d.mts +1 -0
  66. package/src/pi-resolver.mjs +110 -10
  67. package/src/presence.ts +12 -63
  68. package/src/protocol.ts +10 -1
  69. package/src/recipe.ts +43 -1
  70. package/src/requests.ts +354 -0
  71. package/src/rule-lines.ts +68 -0
  72. package/src/run-card.ts +105 -0
  73. package/src/runner.ts +18 -1
  74. package/src/schedule.ts +1 -2
  75. package/src/scheduler.ts +595 -245
  76. package/src/sdk-runner.ts +321 -27
  77. package/src/seen.ts +278 -0
  78. package/src/session-head.ts +21 -0
  79. package/src/share.ts +12 -2
  80. package/src/snapshot.ts +3 -2
  81. package/src/store.ts +81 -8
  82. package/src/tools.ts +196 -65
  83. package/src/trigger-runtime.ts +164 -127
  84. package/src/triggers.ts +39 -7
  85. package/src/trust.ts +46 -2
  86. package/src/ui-prefs.ts +22 -7
  87. package/src/watch-plan.ts +174 -0
  88. package/src/web.d.mts +3 -0
  89. package/src/web.mjs +2261 -2340
  90. package/src/window-executor.ts +459 -0
  91. package/src/window-guard.ts +158 -0
  92. package/docs/agents/issue-tracker.md +0 -34
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  给 [pi](https://github.com/earendil-works/pi) 的自动化层,做成一个 extension:cron 和有记忆的 loop、一个分诊用的 inbox、动态 trigger 与 MCP 推送通知、生命周期 hooks。pi 的代码一行没动。
6
6
 
7
- ![浏览器窗口里同一个 loop 的两次运行:第一次「0 findings,没什么可报的」,第二次报出一条新增的 TODO;右边是等着分诊的 inbox](docs/screenshot.png)
7
+ ![pi-loops 页面:项目里的 loop 报来的一条待决定和两条发现,旁边是一段对话,右边是这两个 loop](docs/screenshot.png)
8
8
 
9
9
  一个 loop 醒来时带着上次留下的笔记,在干净上下文的子代理里干完活,把 finding 归档;你想看的时候去看,值得认真处理的那条再 claim 成真正的一轮。加上 `--verify`,第二个对抗式子代理会先把每条 finding 挑一遍,留下的才到你面前。关掉 pi 之后,这些照样跑。
10
10
 
@@ -28,7 +28,7 @@ pi-loops --continue # 接着这个目录里最新的那个会
28
28
  pi-loops sessions [--all] # 这个目录(或整台机器)记下的会话
29
29
  pi-loops inspect <file> # 别人发来的 .pisession,导入前先看清里面是什么
30
30
  pi-loops upgrade # 装最新的发布版,当初从哪装就从哪装
31
- pi-loops host status # 看一眼没有 pi 开着时在跑的自动化
31
+ pi-loops status # 看一眼没有 pi 开着时在跑的自动化
32
32
  ```
33
33
 
34
34
  上面这几条都得先有 `pi-loops` 这个命令,它在 [上手](#上手) 里装。其余都是会话里的斜杠命令(`/cron`、`/inbox`、`/triggers`、`/goal`),装上扩展重启 pi 就能用。
@@ -39,7 +39,7 @@ pi-loops host status # 看一眼没有 pi 开着时在跑的自
39
39
 
40
40
  Node ≥ 22.6(pi 直接加载 TypeScript 源码),pi ≥ 0.84.3(实际在 0.85 上测;更老的 pi 会在加载时被明确拒绝,报一句写明所需版本的话,而不是一个缺导出的链接错误),以及**一个真的能说上话的 provider**——先跑一次 `pi`,发一句,确认有回答。pi-loops 会在你不看着的时候替你跑子代理;凭据没配好,别等到第二天早上看见空 inbox 才知道。
41
41
 
42
- pi-loops 自己没有任何运行时依赖。
42
+ pi-loops 自己没有任何运行时依赖。负责跑 loop 的 后台进程始终用 node 运行;pi 跑在 bun 下时,pi-loops 使用 PATH 上的 `node`。
43
43
 
44
44
  ### 2. 装上
45
45
 
@@ -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` 之后的第一个 pi 会话会问你要不要把 `pi-loops` 放进 `PATH`,写下之后也一直替你维护着——包被 pi 挪到另一个位置,下一个会话会把启动器改回能用的样子,并用一行告诉你改了什么。答 no 就什么都不写,也不会再问第二次;改了主意,`/pi-loops install-launcher` 随时给你写一个:
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 里面,扩展本来就加载着
@@ -86,9 +86,9 @@ pi-loops --model anthropic/claude-opus-5 -e .
86
86
 
87
87
  重新开始不用回终端:**clear** 开一个新会话,**resume** 回到这个项目里早先的某个会话,**compact** 压缩当前上下文并告诉你压成了多少。三个都是按钮,也都可以在输入框里直接敲(`/clear` 或 `/new`、`/resume`、`/compact 保住 API 的形状`)。谁都不删东西:离开的那个会话就是磁盘上的一个文件,`resume` 里列的就是它,标题是当时说的第一句话。
88
88
 
89
- 浏览器那个地址默认是 **`http://127.0.0.1:4173/`**(`--port` 可以换):端口不随机,token 存在文件里,所以这个地址明天还是它,可以直接收藏。第一次访问会留一个 cookie,之后再也看不到 token。已经开着一个时再敲 `pi-loops`,它会把已经在跑的那个打开给你。只有你自己用的机器上,`--no-auth` 可以把这层也去掉。
89
+ 浏览器那个页面由后台的 pi-loops 提供,地址是 **`http://127.0.0.1:4173/`**:`pi-loops` 把它打开就退出,关掉那个终端,页面照样开着。token 存在文件里,所以这个地址明天还是它,可以直接收藏(4173 被占了,用 `config.toml` 里的 `[page] port` 换一个)。第一次访问会留一个 cookie,之后再也看不到 token。token 去不掉:页面一直开着,所以一直挡在它后面。
90
90
 
91
- 手机上用:最好的路是 `tailscale serve --bg 4173`——服务器仍然只绑 127.0.0.1,TLS 和身份都交给 tailnet,手机打开 `https://<机器>.<tailnet>.ts.net/`。同一个 wifi 里也可以 `pi-loops --host 0.0.0.0`(这时候再加 `--no-auth`,它当场拒绝)。手机第一次进什么都不用输:在已经登录的浏览器里点 **add device**,拿手机扫那个二维码就进去了,之后一直是登录状态(不想扫也可以输下面那六位数)。加到主屏幕会以独立窗口打开(有 manifest,没有 service worker)。
91
+ 手机上用:最好的路是 `tailscale serve --bg 4173`——服务器仍然只绑 127.0.0.1,TLS 和身份都交给 tailnet,手机打开 `https://<机器>.<tailnet>.ts.net/`。同一个 wifi 里也可以在 `config.toml` 里写 `[page] host = "0.0.0.0"`,再 `pi-loops restart`。手机第一次进什么都不用输:在已经登录的浏览器里点 **add device**,拿手机扫那个二维码就进去了,之后一直是登录状态(不想扫也可以输下面那六位数)。加到主屏幕会以独立窗口打开(有 manifest,没有 service worker)。
92
92
 
93
93
  ### 4. 第一个 loop
94
94
 
@@ -99,7 +99,7 @@ pi-loops --model anthropic/claude-opus-5 -e .
99
99
  每天早上一个干净上下文的子代理带着上次写的笔记跑一遍,回复末尾给出 `<loop-state>…</loop-state>`(留给明天的笔记)和 `<inbox>一行 finding</inbox>`:状态写进一个 Markdown 文件,finding 进 inbox,你的对话一个字都不动。下面是一个盯着 TODO 文件的 loop 相隔半小时的两次运行,第一次:
100
100
 
101
101
  ```text
102
- cron todo · 5s · $0.000 · 0 findings · state updated
102
+ todo · 5s · $0.000 · 0 findings · state updated
103
103
  MD5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No new unchecked items — nothing to report.
104
104
  /cron trace todo · /inbox
105
105
  ```
@@ -107,7 +107,7 @@ MD5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No
107
107
  中间提交了一次之后,第二次:
108
108
 
109
109
  ```text
110
- cron todo · 4s · $0.000 · 1 finding · state updated
110
+ todo · 4s · $0.000 · 1 finding · state updated
111
111
  md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
112
112
  • TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results")
113
113
  /cron trace todo · /inbox
@@ -120,7 +120,6 @@ md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
120
120
  Inbox (acme-api, 3 new, times +00:00):
121
121
  1. [inb-aae794d5] TODO: rate-limit the /search endpoint (acme-api, cron:todo, 2026-09-12 05:25)
122
122
  2. [inb-74b73c72] TODO.md: new unchecked item — cache the /search results for 60s (acme-api, cron:todo, 2026-09-12 05:28)
123
- claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
124
123
  ```
125
124
 
126
125
  ```text
@@ -144,7 +143,7 @@ claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
144
143
  daily_budget_usd = 5.0
145
144
  ```
146
145
 
147
- 最后一个 pi 退出时,无头宿主会接手时钟,所以早上九点那次照跑(`/cron host`、`pi-loops host status`)。不想要就在同一个文件里写 `[host] auto = false`。
146
+ 只要有启用的 loop、规则或待处理推送,pi 窗口就会启动后台运行的 pi-loops,所以关闭窗口后早上九点的任务仍会运行(`/pi-loops`、`pi-loops status`)。不想自动启动就在同一个文件里写 `[host] auto = false`。
148
147
 
149
148
  ## 升级
150
149
 
@@ -163,14 +162,27 @@ pi-loops upgrade --check # 只看看有没有新的
163
162
 
164
163
  ## 卸载
165
164
 
165
+ 卸载扩展前先停止 pi-loops,并删掉 `pi-loops` 启动器(`uninstall-launcher` 只删 pi-loops 自己写的那个):
166
+
166
167
  ```bash
168
+ pi-loops stop
169
+ pi-loops uninstall-launcher
167
170
  pi remove npm:@alphacoder-v0/pi-loops # 当初怎么装的就怎么删:npm: 包名、git: ref 或检出路径
168
171
  pi remove git:github.com/alphacoder-v0/pi-loops
169
172
  pi -e /path/to/pi-loops # 或者:只在这次启动试用,什么都不装
170
173
  pi update --extensions # 对齐已安装的包
171
174
  ```
172
175
 
173
- 数据留在 `~/.pi/agent/loops`,想清就删目录。
176
+ 数据留在 `~/.pi/agent/loops`,想清就删目录。以后重新安装时,在 pi 里运行 `/pi-loops install-launcher` 把命令放回来:删掉的启动器不会再自动写回。
177
+
178
+ 要退回 0.22.8:趁 0.23 还装着,先停止 pi-loops,再安装 0.22.8:
179
+
180
+ ```bash
181
+ pi-loops stop
182
+ pi install npm:@alphacoder-v0/pi-loops@<version> # <version> 填 0.22.8
183
+ ```
184
+
185
+ 0.22.8 能直接读你现有的 job 和规则,但不会运行已关闭对话里的 plain job,也不理会只有 0.23 才写的那些文件,所以 `deliveries/` 里还在排队的规则结果不会再送达。
174
186
 
175
187
  包里附带一个 skill(`skills/pi-loops`),让 agent 知道什么时候该用 `cron_create`、`new_trigger` 和 inbox。
176
188
 
@@ -247,11 +259,11 @@ playbook 拷到 `.agents/skills/<name>/`,通过 `.git/info/exclude` 排除在
247
259
  /cron state ci loop 的笔记(状态脊柱)
248
260
  /cron runs [ci] 最近运行,最新在前
249
261
  /cron trace ci 2 第 2 新那次运行的 transcript:prompt、工具调用、结果、回复
250
- /cron scheduler 谁在走时间、有哪些 run 在跑(`/cron status` 是 `/cron` 的别名)
262
+ /cron scheduler 当前窗口、它的会话、pi-loops 状态(`/cron status` 是 `/cron` 的别名)
251
263
  /cron cost [today|7d|all] 自动化花了多少,按任务分,对照 [limits] daily_budget_usd
252
264
  /cron disable --all 本项目全停(--all-projects 停整台机器);/cron clear <ref> 清掉死进程留下的 running 标记
253
265
  /cron gc 清掉会话已消失的任务(本项目;--all 才跨项目)
254
- /cron host [start|stop] 最后一个 pi 退出后接手时钟的无头宿主
266
+ /pi-loops [start|stop] 查看、启动或停止后台运行的 pi-loops
255
267
  /cron snapshot 把「只有这个进程知道的状态」写进会话:哪些 MCP 连上了、暴露了什么工具、
256
268
  当前 active tools、hooks、谁拥有时钟。给非终端的前端读的
257
269
 
@@ -385,9 +397,9 @@ backup.pisession 无压缩 ustar,0600,拒绝覆盖已有文
385
397
  - 结果默认只落在 TUI 卡片和 audit 里;规则带 `promote_to_chat` 时,它以 `[Trigger <trace_id>] …` 开头插进主对话,后面的 turn 看得见。
386
398
  - 5 分钟 dedup 窗口,`listChanged` 类通知用稳定 key 折叠成最新一条,自定义通知必须带 `_meta.pi_dedup_key`,否则在源头丢弃并计数。
387
399
  - 自然语言里出现「每小时 / daily / cron / 定时任务」之类时,`new_trigger` 会拒绝并让 agent 改用 `cron_create`。
388
- - 工具:`new_trigger`(condition / action / spec / fire_once / promote_to_chat)、`list_triggers`、`remove_trigger`(id | all)、`set_trigger_state`,描述与返回文本固定。建规则、删规则、重新启用自动化这三件事都要你点头:工具把原因摆出来等你确认,没有 UI 可点的场合(子会话、无头宿主)一律拒绝。
400
+ - 工具:`new_trigger`(condition / action / spec / fire_once / promote_to_chat)、`list_triggers`、`remove_trigger`(id | all)、`set_trigger_state`,描述与返回文本固定。建规则、删规则、重新启用自动化这三件事都要你点头:工具把原因摆出来等你确认,没有 UI 可点的场合(子会话、后台进程)一律拒绝。
389
401
  - 规则是机器全局的(带 cwd),`/triggers rules` 默认只看本项目;子代理进程里不注册这些工具,所以一条 trigger 的动作造不出新的 trigger。
390
- - 轮询间隔:`--trigger-poll-secs 60` 或 `config.toml` 的 `[triggers] poll_interval_secs`。
402
+ - 轮询间隔:`config.toml` 的 `[triggers] poll_interval_secs`,pi-loops 每分钟重读一次。`--trigger-poll-secs` 已不起作用,传了会提示一句。
391
403
 
392
404
  ### MCP 推送作为 trigger 源
393
405
 
@@ -446,7 +458,7 @@ stdio 服务器崩溃后自动重连;`Mcp-Session-Id` 会话头;服务器发
446
458
  - 单条规则写错只跳过那条并提示,其它照常加载。同一事件的规则按文件顺序串行执行,不阻塞 agent。
447
459
  - 超时或 Ctrl-C 时杀掉 hook 的整棵进程树,不只是 `sh`。
448
460
  - 项目级 `<repo>/.pi/hooks.toml` 默认忽略;用户 `hooks.toml` 顶层写 `allow_project_hooks = true`、或 `config.toml` 同名键、或 `PI_ALLOW_PROJECT_HOOKS=1`(`true` 也算,其它值都算关)才启用。**在无头宿主里还额外要求 pi 对那个任务的 cwd 有精确的信任记录**——`allow_project_hooks` 是对「你会打开的项目」的表态,不是对「模型顺手指过去的目录」的表态。
449
- - hook 打到 stdout 的东西写进本进程日志(`logs/pi-<pid>.log`,宿主写 `host.log`),截到 4000 字符——「打印点东西再去看」这个最常用的调试手段现在是通的。
461
+ - hook 打到 stdout 的东西写进本进程日志(`logs/pi-<pid>.log`,后台日志写入 `host.log`),截到 4000 字符——「打印点东西再去看」这个最常用的调试手段现在是通的。
450
462
  - 子代理进程里不触发 hooks,只有你交互的那个 pi 触发。
451
463
 
452
464
  ## 子代理每次收到的 prompt
@@ -473,7 +485,7 @@ Output protocol (mandatory):
473
485
  | **状态脊柱** — 每个 loop 一份 ≤2000 字符的笔记,run N+1 读到 run N 写的 | `<loop-state>` 标签解析后写入 Markdown,下次运行拼进 prompt 头部 | `~/.pi/agent/loops/state/<id>.md` |
474
486
  | **maker/checker** | `--verify`:第二个对抗式子代理逐条核实 findings,drop 的不进 inbox | `runs.jsonl` 的 `checker` 字段 |
475
487
  | **路由层** — 产出既不打断你也不沉进日志 | `<inbox>` 标签 → JSONL 追加,`new → claimed/dismissed` 生命周期,状态栏 `Inbox: N new` 角标;条目 id `inb-<32hex>`、来源 `cron:<id或name>`、`/inbox` 与 `/inbox all` 的行格式、错误措辞都固定下来,已核实的多一个 `✓` 标记 | `~/.pi/agent/loops/inbox.jsonl` |
476
- | stateful job 走 SubAgent、永不碰主对话 | 同进程子会话(pi SDK),干净上下文,共享父会话的 MCP 客户端、扩展、model/thinking;完整 transcript 保留在 `sessions/<id>/`,`/cron trace` 可看 | `~/.pi/agent/loops/sessions/<id>/*.jsonl` |
488
+ | stateful job 走 SubAgent、永不碰主对话 | 在后台运行的 pi-loops 里开的子会话(pi SDK),干净上下文;MCP 连接由 pi-loops 按 `mcp.toml` 和项目配置自己建立,不再借用某个窗口的连接;完整 transcript 保留在 `sessions/<id>/`,`/cron trace` 可看 | `~/.pi/agent/loops/sessions/<id>/*.jsonl` |
477
489
  | trigger 的产出看得见、可追溯 | 每次运行结束在 transcript 里落一张卡片(耗时、成本、findings、摘要),run log 记退出码与用量 | `runs.jsonl` |
478
490
  | 预览与 audit 一律脱敏 | `src/redact.ts` 一套 redactor,列表、卡片、run log、trace 全部过一遍 | — |
479
491
  | 普通 cron 走 inject-and-run | 不加 `--stateful` 的任务到点用 `pi.sendUserMessage` 注入创建它的会话,消息带 `[Trigger <trace>] ` 前缀(空闲直接发,忙则 followUp 排队) | — |
@@ -486,18 +498,23 @@ Output protocol (mandatory):
486
498
  ## 自动化要活过开它的那个窗口
487
499
 
488
500
  定时任务值不值得信,取决于你关掉编辑器之后会发生什么。所以这里把「pi 重启过」当成常态:
489
-
490
- 1. **任务是机器全局的**,存在 `~/.pi/agent/loops/jobs.json`,不绑会话、不绑目录(每个任务记住自己的 `cwd`,子代理在那里跑)。任何目录里打开的任何 pi 都能看到并执行 stateful 任务;普通任务因为要注入对话,只在创建它的那个会话里触发(`--resume` 回来就继续),会话没了,leader 会把它停用,`/cron gc` 清掉;子代理创建的普通任务归它所服务的那个父会话。
491
- 2. **loop 归本机唯一的 leader 跑;动态检查归「开在那个项目里的那个 pi」跑。** `scheduler.json` 里放 pid 加心跳,30 秒一 tick;leader 退出或者崩了,其它 pi 在下一 tick 接手。
492
-
493
- 每个进程每 tick 往 `presence/` 登记自己的 pid、会话和 cwd。一个项目的规则检查和推送评估,就交给开在那个项目里的 pi——优先创建规则的那个会话,其次 pid 最小的那个——所以 promote_to_chat 一定落在对的对话里。项目里一个 pi 都没开,才轮到 leader 代跑,结果进 inbox。
494
-
495
- 轮询间隔看共享的 `polls.json`,全机一份,交接不会重复检查。
496
- 3. **loop 错过的 tick 默认补发一次**(多次错过折叠成一次,就像 systemd `Persistent=true`),`--no-catchup` 关掉。普通注入任务反过来,默认不补,要补得写 `--catchup`:它是往对话里插一句话的,迟到的提醒不如不提醒。
497
- 4. **没有过期时间。** 任务只在你 `/cron remove` 时消失。
498
- 5. **有 run log 和完整 transcript。** `runs.jsonl` 记每次运行的退出码、耗时、成本、finding 数、有没有更新状态;子代理的 session 文件每个 loop 保留最近 20 份,`/cron trace <job> [k]` 直接看它调了什么工具、看到了什么,`pi --session <文件>` 可以整个接管回放。
499
-
500
- 子代理在父进程内运行:共享父进程活着的 MCP 服务器实例(浏览器标签页、数据库会话都是同一份)、`-e` 扩展、system prompt、skill 标志与模型,同一项目下继承信任。「一个 pi 进程都没有」也解决了:本机最后一个交互式 pi 退出时,如果还有 loop、规则或 MCP 服务器,它会拉起一个无头宿主进程(`src/host.ts`,同一套存储、同样的进程内运行器、自己的 MCP 客户端)继续走时钟;本来要进对话的结果进 inbox;下一个打开的 pi 抢回时钟,宿主退出。`/cron host [start|stop]` 看和控制(start = 即使 `[host] auto = false` 也在本 pi 退出时交接),`host.log` 是它的日志,重启机器后要等下一个 pi 打开才会再交接。
501
+ 任务和规则保存在本机的 loops 目录。
502
+
503
+ 一个 loops 目录最多运行一个后台运行的 pi-loops。pi 窗口在会话开始、每 30 秒一次的 tick,以及新建 loop、规则后,
504
+ 只要存在启用的 loop、规则或有待处理推送的 MCP 服务器,或者某个已关闭的会话有普通任务或排队的规则结果,
505
+ 就会按需启动 pi-loops。pi-loops 运行所有 stateful loop、动态规则检查和 MCP 推送,也在已关闭的会话里运行它们的
506
+ 普通任务,每次都是一轮模型调用;窗口运行在它里面打开的那个会话的普通任务。规则结果回到创建规则的
507
+ 会话:窗口持有该会话时送入会话,其他进程判定时排队给该窗口;会话已关闭时由 pi-loops 追加进会话。只有会话
508
+ 文件无法写入时,结果才进入 inbox。
509
+ 由 shell 创建且没有会话的规则,结果进入 inbox。
510
+
511
+ pi-loops 不会因窗口打开或关闭而退出。`/pi-loops` 显示 pid、pi-loops 版本、启动时间、正在运行的工作、窗口
512
+ 持有的会话和日志;`/pi-loops stop` 会记录手动停止,在 `/pi-loops start` 前窗口不会重新启动它。
513
+ `[host] auto = false` 或单进程环境变量 `PI_LOOPS_HOST=off` 禁止窗口自动启动。无法启动时会在 1、5、30
514
+ 分钟后重试,状态行显示暂停原因,`/pi-loops status` 和 `host.log` 提供详情。窗口连续三次探测不到控制 socket
515
+ 会替换失联的 pi-loops:先发 SIGTERM,10 秒后再发 SIGKILL;仍在启动的 pi-loops 不会因此被杀掉。新版窗口会替换旧版
516
+ pi-loops,旧版窗口不会替换新版 pi-loops。升级期间,pi-loops 0.22 窗口让后台进程运行 loop、自己待命,但仍会运行自己持有
517
+ 会话的普通任务;重启这些旧窗口才能让普通任务也采用新行为。机器重启后,要等 pi 窗口打开才会恢复运行。pi-loops 还有一个只读状态页,`pi-loops status --url` 打印带令牌的地址。
501
518
 
502
519
  ## 对 pi 的无侵入性
503
520
 
@@ -517,13 +534,17 @@ Output protocol (mandatory):
517
534
  | `~/.pi/agent/loops/spend.json` | 轮转掉的那部分花费按天留一份,预算上限不会因为 run log 被截断而失效 |
518
535
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | 每个 pi 进程的自动化诊断,超 2 MB 保留后半,只留最近五个进程(进程还活着的那份不算) |
519
536
  | `~/.pi/agent/loops/sessions/<id>/*.jsonl` | 子代理完整 transcript,每个 loop 保留最近 20 份 |
520
- | `~/.pi/agent/loops/scheduler.json` | 当前 leader 的 pid / 心跳 |
537
+ | `~/.pi/agent/loops/host-service.json`、`host.log` | 后台运行的 pi-loops 记录与日志 |
538
+ | `~/.pi/agent/loops/leases/`、`requests/` | 每个会话在哪里打开,以及等着送进会话的内容 |
539
+ | `~/.pi/agent/loops/pages/` | 浏览器页面里开着的每个会话一条记录:它的 pi 的 pid,以及标签页最近一次用它的时间 |
521
540
  | `~/.pi/agent/loops/triggers.json` | 动态 trigger 规则(全局,带 cwd) |
522
541
  | `~/.pi/agent/loops/triggers-audit.jsonl` | trigger audit,超 2 MB 保留后半 |
523
542
  | `~/.pi/agent/loops/sessions/triggers-<项目>-<哈希>/*.jsonl` | 动态检查子代理的 transcript,每个项目一个目录(目录名带项目路径的哈希,同名的两个项目不会共用),保留最近 40 份 |
524
543
  | `~/.pi/agent/loops/config.toml` | `[triggers] poll_interval_secs`、`allow_project_hooks` |
525
544
  | `~/.pi/agent/loops/mcp.toml` · `hooks.toml` | MCP 推送源、生命周期 hooks |
526
545
 
546
+ 完整的文件列表见 [docs/configuration.md](docs/configuration.md)。
547
+
527
548
  跨进程写入靠 `mkdir` 锁 + 原子改名,没有原生依赖。
528
549
 
529
550
  ## 代码结构
@@ -531,12 +552,12 @@ Output protocol (mandatory):
531
552
  ```
532
553
  src/extension-entry.ts pi 加载的入口:先查 pi 版本(src/pi-floor.ts),够了再导入 src/pi-loops.ts
533
554
  src/pi-loops.ts 扩展本体:命令、工具、生命周期、状态栏角标、面板
534
- src/cli.ts `pi-loops`:会话入口(网页或终端)与 sessions / inspect / export / import / host
555
+ src/cli.ts `pi-loops`:会话入口(网页或终端)与 sessions / inspect / export / import / start / stop / status
535
556
  src/cli-entry.mjs bin
536
557
  src/ts-entry.mjs .mjs 入口怎么加载本包的 .ts:Node 自己剥类型,装在 node_modules 下就借 pi 的 jiti
537
558
 
538
559
  流水线
539
- src/scheduler.ts tick 循环、leader 选举、到期判定、错过补发、并发/重叠控制、子会话执行、写回
560
+ src/scheduler.ts tick 循环、到期判定、错过补发、并发/重叠控制、子会话执行、写回;pi-loops 在后台运行 loops,窗口跑其会话的普通任务
540
561
  src/trigger-runtime.ts 动态规则运行时:admit、投递(sub_agent / inject_summary / inject_and_run)、fire-once、promote、audit
541
562
  src/triggers.ts 规则本身:解析、prompt、id 提取、存储、dedup 窗口
542
563
  src/goal.ts /goal:停止条件状态机、评估器 prompt、续跑预算
@@ -557,12 +578,12 @@ src/subagent-guard.ts 把该策略装进每个子会话的合成扩展
557
578
  src/transcript.ts 把子代理 session 文件压成可读的几十行
558
579
 
559
580
  没有 pi 开着的时候
560
- src/host.ts 最后一个 pi 退出后接手时钟的无头宿主
561
- src/host-entry.mjs 宿主实际被拉起的那个进程,好让 host.ts 在 node_modules 下也能加载
562
- src/host-control.ts host.json、拉起/停止、交接判定
563
- src/host-control-channel.ts 宿主的 unix socket:snapshot、abort、stop
564
- src/host-runtime.ts 宿主里跑的东西(调度器 + triggers + 按请求服务的工具宿主)
565
- src/presence.ts 每个活着的 pi 登记一份:谁开在哪个项目,结果该落进哪个对话
581
+ src/host.ts 后台运行的 pi-loops:认领、自检、0.22 兼容边界
582
+ src/host-entry.mjs pi-loops 的启动入口,负责加载 node_modules 下的 TypeScript
583
+ src/host-control.ts 服务记录、启动、退出与恢复决策
584
+ src/host-control-channel.ts 控制 socket:状态、中止、停止、运行与升级
585
+ src/host-runtime.ts 后台运行逻辑(调度器、triggers、按请求提供的工具)
586
+ src/presence.ts pi-loops 0.22 窗口写入;升级时只读
566
587
  src/register-pi.mjs 在 pi 之外解析 pi 包的 node --import 钩子(宿主、测试用)
567
588
  src/pi-resolver.mjs 这个钩子去哪里找
568
589
 
@@ -606,13 +627,13 @@ TypeScript 的类型信息判断。
606
627
 
607
628
  - 子会话继承父会话的 model/thinking(任务固定了模型则用固定的);子会话里不再加载这个扩展本身,不会递归。
608
629
  - 子会话没有 UI 就没有审批弹窗:需要确认的工具在子会话里 **fail-closed 拒绝**。没人能点「同意」的时候,默认答案只能是「不」。要收紧就用 `--tools read,grep,ls`。
609
- - 状态栏角标最多五段,按需出现,之间用 ` · ` 隔开:`Inbox: N new`、`N job(s) failing (<name> ×K)`、`running: <loop>`(多个用逗号连起来,trigger 检查在这儿显示成 `trigger-check`)、`mcp: 1 source down` / `mcp: 2 sources down`、`loops standby`。
630
+ - 状态栏角标最多五段,按需出现,之间用 ` · ` 隔开:`Inbox: N new`、`N loops failing (<name> ×K)`、`running: <loop>`(多个用逗号连起来,trigger 检查在这儿显示成 `trigger-check`)、`mcp: 1 source down` / `mcp: 2 sources down`、`loops paused: the pi-loops could not start`。
610
631
  - 同一任务上一轮还在跑时新 tick 直接跳过并计数(`overlap-skipped`),不排队。
611
632
  - 同时最多 3 个子代理在跑(`[cron] max_concurrent_runs`)——loop 运行、trigger 检查、`/goal` 评估器**共用这一个池子**(`src/slots.ts`),三条路加起来一共 3 个。`/goal` 评估器和 `/cron run` 占槽但永不被拒(你直接要的东西,机器悄悄不做和从没设过是一样的),所以 `/triggers running` 有可能显示 `4 of 3 sub-agent slot(s) in use`。
612
633
  - `[limits] daily_budget_usd` 不只挡派发,也会**停掉正在跑的运行**:算的是运行日志里已落账的花费加上本进程在飞的(并行的兄弟运行,以及 `--verify` 那对共用 runId 的 maker/checker)。被预算停掉记为 aborted 而不是 failed,所以时隙还欠着、失败连击不累加。
613
634
  - inbox 的状态改写是「最后写者赢」:两个进程同时处理同一条时,后写的那个说了算,不做冲突检测。
614
- - 常驻面板是编辑器上方的 widget:`Triggers` 规则最多 5 条 + `Polling` 最近一次检查、`Inbox N new`、`Cron` 启停统计与任务最多 5 条、`MCP` 各服务器连接状态与工具数——这几段各自没内容时不显示;`Hooks`(cli_hooks 规则数与事件,没有就写 `none`)和 `Runtime`(dedup · cycle suppress · fire-once rules · inject-and-run)总是画出来。规则、任务、新 findings、MCP 服务器全都没有时整个面板不显示;`/cron panel off` 或 `/triggers panel off` 关闭,偏好存在 `ui.json`。
615
- - 跨设备访问不放 broker 在中间:让你从别处够到那个还待在原地的前端——`tailscale serve` 在 tailnet 上终结 TLS、代理到本机 loopback,手机就能开,中间不经过任何第三方;同一网段则用 `--host`。手机第一次进用配对码和二维码([docs/cli.md](docs/cli.md#from-a-phone))。真正没覆盖的是「手机两个网络都不在」,那种情况才需要中继。
635
+ - 常驻面板是编辑器上方的 widget:`Triggers` 规则最多 5 条 + `Polling` 最近一次检查、`Inbox N new`、`Cron` 启停统计与任务最多 5 条、`MCP` 各服务器连接状态与工具数、`Hooks` cli_hooks 规则数与事件——这几段各自没内容时不显示。规则、任务、新 findings、MCP 服务器全都没有时整个面板不显示;`/cron panel off` 或 `/triggers panel off` 关闭,偏好存在 `ui.json`。
636
+ - 跨设备访问不放 broker 在中间:让你从别处够到那个还待在原地的前端——`tailscale serve` 在 tailnet 上终结 TLS、代理到本机 loopback,手机就能开,中间不经过任何第三方;同一网段则在 `config.toml` 里设 `[page] host`。手机第一次进用配对码和二维码([docs/cli.md](docs/cli.md#from-a-phone))。真正没覆盖的是「手机两个网络都不在」,那种情况才需要中继。
616
637
  - 这是一个自动化层,不是一个 agent:写代码、读网页、管 skill 都是 pi 自己的本事,这里只决定什么时候、在哪里、带着什么上下文去跑它们。
617
638
 
618
639
  ## 致谢