pi-async 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,140 @@
2
2
 
3
3
  本文件记录用户可见变更。格式参考 Keep a Changelog,版本遵循 SemVer。
4
4
 
5
+ ## 0.3.0 — 2026-11-02
6
+
7
+ 自 0.2.0 起的累积发布(含 0.2.0 之后未发版的 6 个提交)。主题是**不再对用户和模型撒谎**:任务在
8
+ session cwd 执行、通知诚实标注 best-effort、工具契约对未知输入报错、子进程默认不继承凭证、
9
+ `/reload` 不再杀任务。升级前请先看 Breaking。
10
+
11
+ ### Breaking
12
+
13
+ - **通知语义改为 best-effort**:`pi.sendMessage` 无 ack,删除「发送失败重试 ≤3 次 + 回滚」的假重试。
14
+ `notified` 只在 `sendMessage` 返回后置 true;失败保持 false 并累加 `notifyAttempts`。
15
+ - **子进程环境默认 `safe`**:默认剥离 `*_API_KEY` / `*_TOKEN` / `*_SECRET` / `*_PASSWORD` /
16
+ `*_CREDENTIAL*` 以及 AWS / GitHub / npm 专用键;显式 `env` 传 `PATH`、`LD_PRELOAD`、`NODE_OPTIONS`
17
+ 等危险键**直接报错**。`PI_ASYNC_ENV_INHERIT=all` 恢复完整继承,但**显式危险键覆盖在任何策略下仍被
18
+ 拒绝**(安全规则不可关闭)。
19
+ - **sync 非 0 退出现在返回 `isError: true`**(payload 仍保留完整 `SyncResult`);但 promote/abort 的**等待
20
+ 超时不算错误**(REV-19,见 Changed)。
21
+ - **非 sync 模式传 `onTimeout` 会报错**(此前静默忽略);**`async_kill` 对未知 id/name 报错**
22
+ (此前静默成功)。
23
+ - **数据目录移出项目根**:改为 `<session dir>/pi-async/<sessionId>/`,无 sessionManager 时退回
24
+ `~/.pi/async/<pid>/`(0.2.0 为 `<cwd>/.pi/async/<session-id>-<pid>/`)。
25
+ - **TUI 键位**:`k` 不再是 kill(改为上移),kill 改为 `x`(或 `K`,需二次确认);
26
+ `c` 不再「复制」输出路径(pi 无 clipboard API),只显示路径。
27
+ - **删除 `PI_ASYNC_HEARTBEAT_MS`**:settle 守卫心跳整体删除(自定义 entry 不进 LLM 上下文,
28
+ 60s 周期也命不中 legion 的 500ms `collectArtifactGrowing` 窗口)。
29
+ - **footer 改为默认关闭(opt-in)**:任务汇总默认显示在 rail 首行;仅当 `PI_ASYNC_FOOTER=1` **且**宿主
30
+ 提供 `setStatus` 时才额外渲染一行汇总(不再无条件设置 `pi-async` footer)。
31
+ - **删除 `ctrl+alt+a` 快捷键**:打开任务面板请使用 `/async` 命令。
32
+ - **删除 `PI_ASYNC_DOCK_SHORTCUT` 环境变量**:升级后此前配置的该变量不再生效。
33
+ - **rail 行数上限从 ≤10 行收紧为 ≤4 行**(含汇总行与溢出指示)。
34
+ - **面板从全屏改为居中百分比尺寸**:列表 80%×70%,从 rail 直入的详情 85%×80%。
35
+
36
+ ### Added
37
+
38
+ - rail 首行新增汇总:running、pending(非零时)、elapsed、`✓N` 与 `✗N`(均仅在非零时显示,全失败时不出绿色的 `✓0`);终态计数遵循 `/async-clear` watermark。
39
+ - `async_run` 新增 `promptSnippet` / `promptGuidelines`,模型在系统提示里就知道何时该用
40
+ `async_run` / `async_wait` / `async_logs`。
41
+ - `async_wait` / `async_logs` / `async_kill` 的 `taskId` 支持任务 **name** 别名(重名报错并列出
42
+ 候选 id);`async_status` 新增 `status` / `name`(大小写不敏感子串)/ `limit`(1–50,默认 50)
43
+ 过滤,`includeHistory: true` 按完成时间降序。
44
+ - `async_logs` 新增 `stream: "merged" | "stdout" | "stderr"`;输出文件缺失返回 `missing: true`
45
+ 而不抛错;越界路径读取被拒。
46
+ - 通知消息新增结构化 `details`,并通过 `registerMessageRenderer("async-task-notification")` 渲染成
47
+ 卡片(`✓ name (completed, 1.2s) · exit 0`,失败带 `✗` 与 killKind/failureKind)。
48
+ - `async_status` 快照暴露 `cwd` / `timeoutAt` / `watchDeadline` / `notifyAttempts` / `processAlive`;
49
+ `delayedEnv` 的值打码为 `***`(保留键)。
50
+ - `session_start` 对未确认任务**补发一次**通知(24h 内、`notifyAttempts < 2`、未被消费)。
51
+ - 新增环境变量 `PI_ASYNC_ENV_INHERIT`(`safe` / `all` / `none`)、`PI_ASYNC_MAX_SYNC_TIMEOUT_MS`
52
+ (默认 600000)、`PI_ASYNC_SETTLE_GUARD`(`0` 关闭守卫)。
53
+ - `safe` 环境策略在冻结清单(`*_TOKEN` / `*_API_KEY` / `*_SECRET` / `*_PASSWORD` / `*_CREDENTIAL*` 等)
54
+ 之外额外剥离 `npm_config_*` 中带 `auth` / `token` / `password` 的键(如 `npm_config__authToken`):
55
+ 父 shell 导出的 npm 凭据不再进入任务,普通 `npm_config_registry` 等仍透传。
56
+ - delayed 任务启动失败(`cwd` 不存在 / 危险 `env` / spawn 报错)写 `metadata.startError`,并在完成通知的
57
+ `guidance` 中以 `Start error: ...` 出现。
58
+ - 任务 `cwd` 持久化并在 reload 后恢复;`taskTimeoutMs` 的 `timeoutAt` 与 watch 的 `watchDeadline`
59
+ 在 reload 后按剩余预算重建定时器。
60
+ - 启动时孤儿清理:非终态且 pid 已死(或无 pid)的任务标 `failed` + `failureKind: "lost"`;pending
61
+ delayed 的 `dueAt` 超出 retention 窗口同样标 `lost`,未过期的 pending delayed 按剩余 delay 恢复
62
+ (不区分进程)。
63
+ - `<taskId>.stdout` / `<taskId>.stderr` sidecar 与按流读取;所有产物文件创建为 0600。
64
+ - settle guard 新增 `agent_settled` 兜底:aborted 且有活跃任务时发一条 `async-task-unresolved`
65
+ followUp(不 `triggerTurn`,每 generation 最多 1 条)。
66
+ - 新增 `npm run verify` 与 `prepublishOnly`;CI 增加 node 22/24 matrix、pi 1.5.0 相容 job 与
67
+ `npm pack --dry-run`;新增 tag 触发的 `release.yml`(publish + GitHub release)。
68
+ - 新增 `PI_ASYNC_FOOTER`(默认关):设为 `1` 且宿主有 `setStatus` 时额外显示一行汇总。
69
+
70
+ ### Changed
71
+
72
+ - settle guard 提醒改用 `custom_message`(`display: false`,进入 LLM 上下文),不再用
73
+ `appendEntry`(custom entry 不进上下文)。
74
+ - `session_shutdown` 读取 `reason`:`reload` / `resume` → `suspend()`(不杀进程,交给下一代收养);
75
+ `quit` / `new` / `fork` / 未知 → `shutdown()`(kill)。
76
+ - factory 期不再 `mkdir` / `reloadFromDisk` / 起 timer;首个 `session_start` 才 `configure()` 并补发。
77
+ - 工具 `execute` 使用 `ctx.cwd`(第 5 参);delayed 任务保留提交时 cwd;任务不再依赖 pi 进程目录。
78
+ - 面板改为事件驱动刷新(订阅 registry 事件,`dispose()` 退订),overlay 单例;kill 失败不再冒泡成
79
+ unhandledRejection,改为 `actionError` + `ui.notify`。
80
+ - 详情视图不再静默裁掉 footer 键位提示:超出预算时保留 header + 最后 N 行并插入 `… (+K lines)`。
81
+ - rail 删除不可达的 PageUp/PageDown `handleInput` 代码;汇总计数与任务行使用同一可见窗口。
82
+ - peerDependencies 保持 `^1.2.0`(本次用到的 `ctx.cwd`、`execute` 第 5 参、`onUpdate`、
83
+ `promptSnippet`、`registerMessageRenderer`、`agent_settled.aborted` 在 1.2.0 类型里已存在),
84
+ CI 另跑 pi 1.5.0 的 typecheck/test。
85
+ - sync `onTimeout: "promote"` / `"abort"` 的等待超时**不再是工具错误**(REV-19):`isError` 只在任务真的以
86
+ 非 0 状态结束时为 true;promoted/abort 的 payload 带 `promoted` 与 `status: "running"`。
87
+ - `R` rerun 在原任务的 `cwd` 执行(不再用当前 session cwd)。
88
+
89
+ ### Fixed
90
+
91
+ - **settle 守卫心跳会在 ctx 失效后杀死整个 pi 进程**(崩溃 2026-10-21,`pi` 1.3.0)。
92
+ 宿主一旦使捕获的 factory ctx 失效(会话替换或 reload),`pi.appendEntry` 就会抛
93
+ "This extension ctx is stale";而心跳跑在 `setInterval` 回调里,**不在 pi 事件循环的
94
+ try/catch 内**,异常逃逸成 `uncaughtException`,pi 直接退出。0.3.0 的处置:心跳已整体删除
95
+ (见 Breaking),守卫只保留事件驱动路径,且不论哪条 teardown 没跑到都能自愈:
96
+ - 不再在被失效 ctx 上调用 `appendEntry` —— 提醒走 `agent_before_settle` 返回的 `custom_message` entry;
97
+ - `reportActivity` 读取 `pi.ui` 时捕获 stale ctx,异常即 `dispose()` 自身,不再重试已死的 API;
98
+ - `init()` 在生成新一代时先 `dispose()` 上一代守卫(`activeGuard`),不依赖某个 teardown 事件已先抵达;
99
+ - 钩子释放改用 `pi.on()` 返回的退订函数 —— 真实宿主**没有** `pi.off()`,此前的
100
+ `hooks.off?.()` 恒为空操作,等同于从不解绑。
101
+ - 任务在 pi **进程** cwd 而不是 session cwd 执行(`async_run("pwd")` 跑错目录)的问题。
102
+ - 通知 claim 在投递前置位、导致一次失败后永久静默的问题。
103
+ - 面板打开期间任务完成不刷新的问题;面板 `kill` 失败导致进程退出的问题。
104
+ - 数据目录落在项目根、可能被 git 或分享带走凭证的问题。
105
+ - `safe` 环境策略此前仅按 `*_TOKEN` / `*_SECRET` 等后缀匹配,会漏掉 npm 的 `npm_config_*` 凭据键
106
+ (如 `npm_config__authToken`,其名字不以 token 结尾);现在按前缀 + `auth`/`token`/`password`
107
+ 子串一并剥离,普通 npm 设置不受影响。
108
+
109
+ ### 契约修订(REV-13 … REV-19)
110
+
111
+ - **REV-13(cwd)** 原因:任务继承 pi 进程 cwd,与 session cwd 不一致。行为:`RunOptions.cwd` /
112
+ `TaskSnapshot.cwd` 持久化,`spawnDetached` 显式传 `cwd`,delayed 与 reload 保留提交目录。
113
+ 回退:无开关(正确性修复)。
114
+ - **REV-14(notify honesty / 补发)** 原因:`sendMessage` 无 ack,重试与回滚是死代码,claim 前置会
115
+ 永久静默。行为:单次投递、claim 后置、`session_start` 补发(24h / attempts<2)、`async_wait`
116
+ 消费即抑制、状态暴露 `notified` / `outputConsumed` / `notifyAttempts`。回退:单任务
117
+ `notifyOnCompletion: false`。
118
+ - **REV-15(name 别名 / 工具契约)** 原因:模型按 name 调用失败、未知 id 静默成功、非 sync 的
119
+ `onTimeout` 被吞、sync 失败不带 `isError`。行为:name 别名 + 重名候选、未知 id 报错、模式错配
120
+ 参数报错、sync 非 0 → `isError`、`async_status` 过滤/排序。回退:无(只放宽入参、收紧错误面)。
121
+ - **REV-16(env policy)** 原因:子进程默认拿到全部 provider 凭证,`env` 可经模型覆盖 `PATH` /
122
+ `LD_PRELOAD`。行为:默认 `safe` 剥离敏感键(含 `npm_config_*` 中带 auth/token/password 的键)、
123
+ 危险键显式拒绝、delayed env 脱敏、`PI_ASYNC_INTERNAL_MAX_OUTPUT_BYTES` 始终删除。回退:
124
+ `PI_ASYNC_ENV_INHERIT=all`(完整继承;显式危险键在任何策略下仍被拒绝)/ `none`(最小白名单)。
125
+ - **REV-17(dataDir / reload)** 原因:dataDir 在项目根可被 git 带走;`/reload` 会 kill 正在跑的
126
+ 任务;超时与 watch 预算 reload 后重置。行为:session 目录 + 0600 + 迁移失败保留旧目录、
127
+ `session_shutdown.reason` 分流 `suspend` / `shutdown`、`timeoutAt` / `watchDeadline` 重建、
128
+ `configure()` → `reapOrphans()` 清孤儿(running 死 pid / 超出 retention 的 pending delayed → `failed/lost`),
129
+ 未过期的 pending delayed 在下一个加载该 dataDir 的会话按剩余 delay 恢复。回退:
130
+ `PI_ASYNC_RETENTION_MS` / `PI_ASYNC_RETENTION_COUNT` 控制终态产物保留窗口(orphan pending delayed
131
+ 只看 `PI_ASYNC_RETENTION_MS`);不提供「回到项目根」开关。
132
+ - **REV-18(TUI)** 原因:`k` 在列表是上移、在详情是 kill,易误杀;`copyToClipboard` 不存在导致
133
+ `c` 永远走 toast fallback;overlay 可叠加;面板打开时冻结。行为:kill 改 `x` / `K` 并二次确认、
134
+ `c` 只显示路径、overlay 单例、事件驱动刷新、详情保留 footer。回退:无。
135
+ - **REV-19(sync 超时不是工具错误)** 原因:`promote` / `abort` 超时后任务仍在跑(`status: "running"`),
136
+ 旧口径把它当成失败;PLAN §2.7 的 isError 口径是「非 0 退出」。行为:`isError` 只在 sync 任务真的以
137
+ 非 0 状态结束时为 true,payload 保留 `promoted` / `status` / `exitCode`。回退:调用方改看 `promoted`。
138
+
5
139
  ## 0.2.0 — 2026-10-16
6
140
 
7
141
  自 0.1.0 起的 16 个提交,主体是 Wave1/Wave2 缺陷修复,附带少量新能力与若干
@@ -47,7 +181,7 @@
47
181
  ### 未变更
48
182
 
49
183
  - 5 个工具(`async_run` / `async_wait` / `async_logs` / `async_status` / `async_kill`)
50
- 的注册面与参数名集合不变;没有删除任何工具、参数或环境变量。
184
+ 的注册面与参数名集合不变;没有删除任何工具或工具参数。`PI_ASYNC_DOCK_SHORTCUT` 属于环境配置而非工具注册面,已在 0.3.0 的 Breaking 节单独记录其删除。
51
185
 
52
186
  ## 0.1.0 — 2026-10-15
53
187
 
package/README.md CHANGED
@@ -2,22 +2,28 @@
2
2
 
3
3
  Pi extension for running shell commands asynchronously with bounded output capture, completion notifications, and a task panel.
4
4
 
5
+ Tasks run in the **session working directory** — the directory pi is currently in, not the directory the pi process was started from. Every task records its `cwd`, so delayed tasks and `/reload` recovery keep the directory they were submitted from.
6
+
5
7
  ## Install
6
8
 
7
- Install the package from a local path with Pi's package manager:
9
+ Install the package from npm with Pi's package manager:
10
+
11
+ ```text
12
+ pi install npm:pi-async
13
+ ```
14
+
15
+ Local path (the other form of the same command, for a checkout):
8
16
 
9
17
  ```text
10
18
  pi install /path/to/pi-async
11
19
  ```
12
20
 
13
- For development, load the extension directly (for example from this checkout):
21
+ For development, load the extension directly:
14
22
 
15
23
  ```sh
16
24
  pi -e ./extensions/index.ts
17
25
  ```
18
26
 
19
- The extension uses the Pi host's project working directory to store task metadata and captured output in `.pi/async/<session-id>-<pid>/`.
20
-
21
27
  ## Tools and commands
22
28
 
23
29
  The extension registers five tools:
@@ -26,24 +32,33 @@ The extension registers five tools:
26
32
  |---|---|
27
33
  | `async_run` | Start a background, delayed, or synchronous shell command. |
28
34
  | `async_wait` | Wait for task completion or return a `notReady` result on timeout. |
29
- | `async_logs` | Read bounded, ANSI-stripped captured output. |
35
+ | `async_logs` | Read bounded, ANSI-stripped captured output (`merged` / `stdout` / `stderr`). |
30
36
  | `async_status` | List tasks and their current state. |
31
37
  | `async_kill` | Terminate a task and its process group. |
32
38
 
39
+ `async_wait`, `async_logs`, and `async_kill` accept a task **name** as well as a task id. Ambiguous names are rejected with the candidate ids listed; unknown ids/names are an error instead of a silent success.
40
+
41
+ Tool notes:
42
+
43
+ - `async_run`: `mode` is `background` (default), `delayed` (requires `delayMs`) or `sync` (requires `timeoutMs`). `timeoutMs` is sync-only, `delayMs` is delayed-only, `onTimeout` is sync-only — passing them to the wrong mode is rejected. `onTimeout: "promote"` (default) hands an expired wait to background; `"abort"` stops waiting only, the command keeps running and still notifies. A sync run that **finishes** without exit 0 returns `isError: true` with the full payload (`status` / `exitCode` / `outputTail` / `promoted`) intact. `promoted: true` means the sync wait expired and the task was handed to background (`status: "running"`) — a promoted or aborted wait is **not** a tool error.
44
+ - `async_status`: defaults to active (running/pending) tasks. `includeHistory: true` lists up to `limit` (1–50, default 50) tasks sorted newest first; an explicit `status` also sorts newest first. `status` and `name` (case-insensitive substring) filter the list. `delayedEnv` values are masked as `"***"`. Snapshots expose `notified`, `outputConsumed`, `notifyAttempts`, `cwd`, `timeoutAt`, and `watchDeadline`. A delayed task that fails to start (missing `cwd`, rejected `env` key, spawn error) is finalized as `failed` (exit 1) and records the cause in `metadata.startError`.
45
+ - `async_logs`: `stream` selects the merged capture or a single stream; `maxBytes` is capped at 50 KiB. A missing output file returns `missing: true` instead of throwing; paths outside the data directory are refused.
46
+ - `async_kill`: returns `killKind`, `alreadyTerminal`, `processAlive` and, when the kill could not be confirmed, `warning`.
47
+
33
48
  | Command / key | Action |
34
49
  |---|---|
35
- | `/async` | Open the task panel. |
50
+ | `/async` | Open the task panel. The panel is a singleton: running it again while open only shows an `Async panel is already open` notice and does not stack another overlay — the open panel refreshes itself from registry events. |
36
51
  | `/async-clear` | Clear completed-task history badges. |
37
- | `Ctrl+Alt+A` | Open the task panel (when the host allows the shortcut; `/async` remains available as fallback). |
38
52
  | `↑` / `↓` / `j` / `k` | Select tasks; in task details, scroll output. |
39
53
  | `Enter` | Open selected task details. |
40
54
  | `h` | Toggle history in the task list. |
41
- | `k` | Kill a running task (press again to confirm). |
42
- | `R` | Rerun the selected task command. |
43
- | `c` | Copy the captured output file path. |
44
- | `Esc` / `q` | Return from details / close the panel. |
55
+ | `x` (or `K`) | Kill a running task (press again to confirm). |
56
+ | `R` | Rerun the selected task command in that task's original `cwd`. |
57
+ | `c` | **Show** the captured output file path as a toast. Nothing is copied: pi exposes no clipboard API, so there is no copy action. |
58
+ | `Esc` | Close rail-clicked details; return from list-opened details to the list. |
59
+ | `q` | Close the panel. |
45
60
 
46
- The panel and rail are only enabled in the main Pi TUI session; RPC, print, and child contexts retain the tool surface without TUI entrypoints.
61
+ The rail's first row summarizes running, pending, elapsed, completed, and failed/killed task counts; pending and failed/killed counts are shown only when nonzero. **Mouse support (wheel, click) exists only in fullscreen / alt-screen TUI mode** — pi only enables mouse reporting there; in regular mode use `/async` and the keyboard. The panel and rail are only enabled in the main Pi TUI session; RPC, print, and child contexts retain the tool surface without TUI entrypoints.
47
62
 
48
63
  ## Configuration
49
64
 
@@ -51,20 +66,58 @@ The panel and rail are only enabled in the main Pi TUI session; RPC, print, and
51
66
  |---|---:|---|
52
67
  | `PI_ASYNC_MAX_OUTPUT_BYTES` | `20971520` (20 MiB) | Maximum captured output per task; exceeding it terminates the task with `output_cap`. |
53
68
  | `PI_ASYNC_MAX_TASKS` | `32` | Maximum concurrent running and pending tasks. |
54
- | `PI_ASYNC_HEARTBEAT_MS` | `60000` | Settle-guard heartbeat interval in milliseconds. |
55
- | `PI_ASYNC_DOCK_SHORTCUT` | `ctrl+alt+a` | Configured panel shortcut; the command remains available as fallback. |
69
+ | `PI_ASYNC_MAX_SYNC_TIMEOUT_MS` | `600000` (10 min) | Upper bound for `sync.timeoutMs` (schema `maximum` and runtime check). |
70
+ | `PI_ASYNC_ENV_INHERIT` | `safe` | Child env policy: `safe` strips credential-like variables and rejects dangerous explicit keys; `all` restores full inheritance (explicit dangerous-key overrides are still rejected in every policy); `none` passes only a minimal allowlist (`PATH`, `HOME`, `SHELL`, `TERM`, `LANG`, `LC_*`, `TMPDIR`, `USER`). |
71
+ | `PI_ASYNC_SETTLE_GUARD` | `on` | Set `0` to disable the settle guard entirely (no `agent_before_settle` reminder, no `agent_settled` backstop). |
72
+ | `PI_ASYNC_FOOTER` | `off` | Set `1` to also render a one-line task summary through the host's `setStatus` footer (only when the host exposes `setStatus`). The rail's first row is the default status surface. |
56
73
  | `PI_ASYNC_RETENTION_MS` | `604800000` (7 days) | How long terminal-task artifacts are retained; union with `RETENTION_COUNT`. |
57
74
  | `PI_ASYNC_RETENTION_COUNT` | `100` | Max newest terminal tasks kept per dataDir; union with `RETENTION_MS`. |
58
75
  | `PI_ASYNC_RAIL_LINGER_MS` | `30000` | How long completed/failed/killed tasks stay on the rail before disappearing. |
59
76
 
77
+ Under the default `safe` policy, inherited variables matching `*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, `*_CREDENTIAL*`, plus `AWS_SECRET_ACCESS_KEY`, `AWS_ACCESS_KEY_ID`, `AWS_SESSION_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, and `NPM_TOKEN` are removed. `npm_config_*` keys whose name contains `auth`, `token`, or `password` (for example `npm_config__authToken`, which npm reads as a credential) are removed as well, so npm credentials exported by the parent shell do not leak into tasks; ordinary npm settings such as `npm_config_registry` still pass through. Passing a dangerous key explicitly through `env` (`PATH`, `LD_PRELOAD`, `LD_LIBRARY_PATH`, `NODE_OPTIONS`, `BASH_ENV`, `ENV`, `SHELLOPTS`, `PYTHONPATH`, `GIT_SSH_COMMAND`) throws instead of being silently ignored — **under every policy, including `all`**; the safety rule cannot be switched off. Ordinary keys may still be overridden.
78
+
79
+ For delayed tasks the submitted `env` is persisted to `<taskId>.json` (mode `0600`) so the task can still start after `/reload`; the tool surface and the panel render its **values masked as `"***"`** (keys survive).
80
+
81
+ ## Notifications
82
+
83
+ Completion notifications are **best-effort**. `pi.sendMessage` returns `void` — the host is fire-and-forget and gives no acknowledgement, so pi-async sends once, never retries, and never rolls back. A "queued" notification means it was handed to the host, not that the model read it.
84
+
85
+ - `notified` becomes true only after `sendMessage` returns; a failure or a missing host API leaves `notified: false` and increments `notifyAttempts`.
86
+ - On `session_start`, pi-async **resends** terminal tasks that are still unnotified and unconsumed (within 24 h and under 2 attempts), so a missed wake-up is retried once per session start.
87
+ - `async_wait` results are marked consumed (`outputConsumed`), which **suppresses** the completion notification for those tasks to avoid chatter. On a hybrid timeout only the completed side is consumed; not-ready tasks are left alone.
88
+ - `async_status` exposes `notified` / `outputConsumed` / `notifyAttempts`, so an agent can check whether it already received (or suppressed) a result.
89
+ - `notifyOnCompletion: false` opts a task out entirely.
90
+ - A delayed task whose start failed records `metadata.startError`; the failure notification's `guidance` includes it as `Start error: ...`, so the cause (missing `cwd`, rejected `env` key, spawn failure) is visible without an extra `async_logs` call.
91
+
92
+ ## Watches
93
+
94
+ - Conditions inside a group (`success_when`, `failure_when`) are OR-ed: the **first match wins**. There is no AND mode; use one condition with a richer `stdout_contains` value if you need a conjunction.
95
+ - `success_when` records completion without killing the process (task lifetime still belongs to `taskTimeoutMs` / `async_kill`); `failure_when`, blind exhaustion, and watch timeout all kill the process group.
96
+ - `blind_checks` (default 3) counts **only infrastructure errors** (e.g. the output file could not be read). "Not yet decidable" states — process still running, JSON not valid yet — stay pending and never accumulate blind checks.
97
+ - The match window is the trailing **64K characters** of output, not bytes, and early output scrolls out of it. Anchor regexes differently if the marker can appear far from the tail.
98
+ - `output_matches` compiles with the `m` flag, so `^` / `$` are line boundaries.
99
+
100
+ ## Data directory
101
+
102
+ Captured artifacts live per session:
103
+
104
+ - With a session manager: `<session dir>/pi-async/<sessionId>/`
105
+ - Without one: `~/.pi/async/<pid>/`
106
+
107
+ Each task writes `<taskId>.json` (atomically replaced metadata) plus `<taskId>.output` and its `.stdout` / `.stderr` sidecars. Files are created with mode `0600`.
108
+
109
+ Do **not** point the data directory at a git-tracked path: captured output can contain credentials, tokens, or other secrets a command printed. Terminal-task artifacts are pruned by `PI_ASYNC_RETENTION_MS` ∪ `PI_ASYNC_RETENTION_COUNT`; non-terminal tasks are never pruned **directly** — orphan detection (`configure()` → `reapOrphans()`) first marks `running` tasks whose pid is gone as `failed` (`failureKind: "lost"`) at startup, and marks pending delayed tasks as `lost` when their due time is older than **`PI_ASYNC_RETENTION_MS`** (the count setting does not apply to them). Pending delayed tasks still inside that window are restored with their remaining delay.
110
+
111
+ If the resolved data directory changes inside a live process, artifacts are copied to the new location; if migration fails, pi-async logs the error and keeps the old directory so no task is lost.
112
+
60
113
  ## Legion child sessions
61
114
 
62
- In a legion child session, collect task results with `async_wait` before returning a verdict. The settle guard reminds the agent about uncollected tasks, but the completion notification may not be delivered across turns until the upstream busy-hold contract lands. See [CONTRACT.md §6](docs/CONTRACT.md#6-唤醒链路与-legion-兼容冻结) for the frozen notification and collection requirements.
115
+ In a legion child session, collect task results with `async_wait` before returning a verdict. The settle guard pushes a reminder into the model context before settling (up to 3 times, `custom_message` draft), and an aborted run with active tasks gets a follow-up reminder on `agent_settled`. Notifications themselves remain best-effort (see above); see [CONTRACT.md §6](docs/CONTRACT.md#6-唤醒链路与-legion-兼容冻结) for the frozen notification and collection requirements.
63
116
 
64
117
  ## Known limitations
65
118
 
66
- - Crash-durability is not guaranteed: metadata is atomically replaced, but captured output is not fsynced. After a crash, orphaned tasks and delayed tasks are not automatically restored; recovery is limited to `/reload` in the same running process.
67
- - Busy-hold behavior depends on upstream pi-legion issue [#156](https://github.com/zzjcool/pi-legion/issues/156); the extension does not patch Pi or legion.
119
+ - Crash-durability is not guaranteed: metadata is atomically replaced, but captured output is not fsynced. After a crash, orphaned running tasks are marked `failed`/`lost` on the next start, and pending delayed tasks are restored with their remaining delay by whichever session next loads the same data directory (process identity does not matter). Recovery inside the same process is complete for `/reload` (`cwd`, `taskTimeoutMs` deadline, and watch deadline are rebuilt from disk).
120
+ - Cross-turn wake-up in legion children depends on the host delivering follow-up messages (upstream pi-legion issue [#156](https://github.com/zzjcool/pi-legion/issues/156)); the extension does not patch Pi or legion.
68
121
  - TUI rail and panel are available only in the main TUI session, not in RPC/print/child contexts.
69
122
  - `pi --print` notification delivery requires an available configured provider. If no provider is available, end-to-end print-mode verification remains pending a user run.
70
123
 
@@ -73,6 +126,8 @@ In a legion child session, collect task results with `async_wait` before returni
73
126
  ```sh
74
127
  npm run typecheck
75
128
  npm test
129
+ npm run verify # typecheck + test (same gate as prepublishOnly)
130
+ npm pack --dry-run
76
131
  ```
77
132
 
78
133
  ## Container QA
@@ -95,7 +150,6 @@ mounts, and why L3 must not be skipped.
95
150
  ## Known upstream issue (print mode)
96
151
 
97
152
  `pi --print` + background task completing **after** the agent settles: the wake happens but the woken
98
- run's `turn_end` boundary fails with `Extension error (<boundary>): turn_end could not resolve the
99
- persisted assistant entry ID` (pi 1.2.0 core, reproduced identically with pi-background-tasks —
153
+ run's `turn_end` boundary fails with `Extension error (<boundary>): turn_end could not resolve the persisted assistant entry ID` (pi 1.2.0 core, reproduced identically with pi-background-tasks —
100
154
  upstream issue https://github.com/earendil-works/pi/issues/11001). **RPC mode (legion subagents) and
101
155
  turn-internal waits are unaffected.**