@oldflag2333333/pi-subagent 0.1.0 → 0.1.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/README.md CHANGED
@@ -8,7 +8,7 @@ A **profile** defines a specialist's instructions, tools, and optional skills or
8
8
 
9
9
  ## Install
10
10
 
11
- You need **Node.js 22.19+**, **[Pi](https://pi.dev) 1.0.4+**, and **Herdr**. To launch specialists, run Pi inside a Herdr workspace with the `herdr` command available.
11
+ You need **Node.js 22.19+**, **[Pi](https://pi.dev) 1.2.0+**, and **Herdr**. To launch specialists, run Pi inside a Herdr workspace with the `herdr` command available.
12
12
 
13
13
  ```bash
14
14
  pi install npm:@oldflag2333333/pi-subagent
@@ -75,7 +75,7 @@ Ask the reviewer profile to review src/auth for bugs and missing tests.
75
75
  | Check progress | Ask Pi to list its Subs, or open the specialist's Herdr tab |
76
76
  | Stop its current task without closing it | Ask Pi to interrupt the Sub |
77
77
  | Finish with a specialist | Ask Pi to close the Sub, or close its Herdr tab |
78
- | Remove communication files left by deleted Main sessions | `/clean-sub` |
78
+ | Remove Sub sessions and communication files left by deleted Mains | `/clean-sub` |
79
79
 
80
80
  Within the same Main session, repeated `/sub:reviewer` commands reuse that profile's open Sub. If it is busy, your next task is queued. Finishing a task does not automatically close the tab.
81
81
 
@@ -145,12 +145,20 @@ These instructions apply only to your main Pi, not to its Subs. Keep shared proj
145
145
 
146
146
  ## Clean up deleted sessions
147
147
 
148
- Run `/clean-sub` to immediately delete runtime communication directories whose Main ID is no longer found among saved Pi sessions. It checks the default session store and the current session directory, always preserves the current Main, and creates no index. If session files cannot be read or their headers are invalid, cleanup stops rather than treating them as deleted.
148
+ Persistent Sub histories are grouped by their owning Main, using Pi's native `--session-dir`:
149
149
 
150
- Persistent Sub `.jsonl` files are **not deleted**; remove them manually through Pi's session picker. Profiles and global settings are untouched. The command does not close Herdr tabs. If you moved Main files or use other custom session directories, make them available in the scanned locations before cleaning.
150
+ ```text
151
+ ~/.pi/agent/subagent/sessions/<main-session-id>/*.jsonl
152
+ ```
153
+
154
+ Run `/clean-sub` to immediately delete both the Sub session directory and runtime communication directory for any Main ID no longer found among saved Pi sessions. It checks Pi's default session store and the current session directory, always preserves the current Main, and creates no index. Unreadable session files or invalid headers stop cleanup instead of being treated as deleted.
155
+
156
+ Close any orphaned Sub tabs before cleaning; the command only deletes files. Profiles and global settings are untouched. Older Sub files outside the managed directory still need manual deletion. If you moved Main files or use other custom session directories, make them available in the scanned locations before cleaning.
151
157
 
152
158
  ## Things to know
153
159
 
160
+ Messages use custom inbox cards and Pi’s native run preparation, without extra user-role wake messages. See [messaging behavior](docs/messaging.md).
161
+
154
162
  - **Herdr is required for Subs.** There is no headless fallback. Up to four Subs can be open at once, and Subs cannot delegate further.
155
163
  - **Profiles are not a sandbox.** Subs run as your OS user. A read-only role is not a filesystem permission boundary.
156
164
  - **MCP follows Pi's normal configuration.** Profiles select non-MCP tools; they do not isolate or filter MCP servers. Custom non-MCP tools must come from extensions already loaded in Main.
package/README.zh-CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## 安装
10
10
 
11
- 需要 **Node.js 22.19+**、**[Pi](https://pi.dev) 1.0.4+** 和 **Herdr**。启动助手时,Pi 必须运行在 Herdr 工作区中,并且能调用 `herdr` 命令。
11
+ 需要 **Node.js 22.19+**、**[Pi](https://pi.dev) 1.2.0+** 和 **Herdr**。启动助手时,Pi 必须运行在 Herdr 工作区中,并且能调用 `herdr` 命令。
12
12
 
13
13
  ```bash
14
14
  pi install npm:@oldflag2333333/pi-subagent
@@ -75,7 +75,7 @@ mkdir -p ~/.pi/agent/subagent/profiles/reviewer
75
75
  | 查看进度 | 让 Pi 列出子助手,或打开助手的 Herdr 标签页 |
76
76
  | 停止当前任务,但保留助手会话 | 让 Pi 中断该子助手 |
77
77
  | 结束使用 | 让 Pi 关闭子助手,或手动关闭其 Herdr 标签页 |
78
- | 清理已删除 Main 遗留的通信文件 | `/clean-sub` |
78
+ | 清理已删除 Main 遗留的 Sub 会话和通信文件 | `/clean-sub` |
79
79
 
80
80
  在同一个主会话(Main)中,多次调用 `/sub:reviewer` 会复用该角色已打开的子助手(Sub)。如果它正在工作,新任务会排队。任务完成后,标签页不会自动关闭。
81
81
 
@@ -145,12 +145,20 @@ reviewer/
145
145
 
146
146
  ## 清理已删除会话的遗留文件
147
147
 
148
- 执行 `/clean-sub`,立即删除找不到对应 Main ID 的运行时通信目录。它检查 Pi 默认会话目录和当前会话目录,始终保留当前 Main,不新增索引文件。会话文件无法读取或文件头无效时会停止清理,不会将它们当作已删除。
148
+ 持久化 Sub 使用 Pi 原生的 `--session-dir`,按所属 Main 分目录保存:
149
149
 
150
- 持久化 Sub 的 `.jsonl` **不会自动删除**,请通过 Pi 的会话列表手动删除。角色配置和全局设置不受影响,命令也不会关闭 Herdr 标签页。如果移动过 Main 文件或使用其他自定义会话目录,请先确保这些会话位于扫描范围内,再执行清理。
150
+ ```text
151
+ ~/.pi/agent/subagent/sessions/<main-session-id>/*.jsonl
152
+ ```
153
+
154
+ 执行 `/clean-sub`,找不到对应 Main ID 时,立即删除它的 Sub 会话目录和运行时通信目录。命令检查 Pi 默认会话目录和当前会话目录,始终保留当前 Main,不新增索引。会话文件无法读取或文件头无效时会停止清理,不会将它们当作已删除。
155
+
156
+ 清理前请关闭遗留的 Sub 标签页,命令只删除文件。角色配置和全局设置不受影响;以前散落在专用目录之外的 Sub 文件仍需手动删除。如果移动过 Main 文件或使用其他自定义会话目录,请先确保这些会话位于扫描范围内,再执行清理。
151
157
 
152
158
  ## 使用须知
153
159
 
160
+ 助手间消息使用自定义收件卡片,由 Pi 原生处理启动准备,不再添加用户角色唤醒消息。详见[消息机制](docs/messaging.md)。
161
+
154
162
  - **子助手依赖 Herdr。** 不支持无界面后台运行;最多同时打开四个子助手,子助手不能继续委派。
155
163
  - **角色配置不是沙箱。** 子助手以你的系统用户身份运行,配置“只读角色”不等于限制文件系统权限。
156
164
  - **MCP 沿用 Pi 的常规配置。** 角色只选择非 MCP 工具,不隔离或筛选 MCP 服务。自定义的非 MCP 工具必须来自主 Pi 已加载的扩展。
@@ -169,4 +177,4 @@ npm run check
169
177
  pi -e ./src/index.ts
170
178
  ```
171
179
 
172
- 维护者可参考[发布指南](docs/releasing.md)(英文),了解 CI 和 npm 自动发布流程。
180
+ 维护者可参考[发布指南](docs/releasing.zh-CN.md),了解首次 npm 发布、Trusted Publisher 配置和后续自动发布流程。
@@ -0,0 +1,17 @@
1
+ # Message delivery and rendering
2
+
3
+ Pi Subagent requires **Pi 1.2.0 or newer**. Main ↔ Sub messages use `pi.sendMessage()` with `deliverAs: "followUp"` and `triggerTurn: true` in both directions:
4
+
5
+ - **Busy receiver:** Pi queues the custom message behind its current work.
6
+ - **Idle receiver:** Pi prepares and starts the run, including `before_agent_start`, the tool loadout, and prompt sections. The event has `messageType: "custom"`, with the starting `message` rather than a user `prompt`.
7
+
8
+ There are no synthetic user-role wake messages, version fallbacks, or patches to Pi internals. Custom messages do not pass through user-input processing or execute peer text as slash commands or templates.
9
+
10
+ ## Delivery guarantees
11
+
12
+ - `content` carries the peer message and routing instructions; `details` carries rendering and receipt metadata and is not sent to the model.
13
+ - Custom cards retain their title, collapsed/expanded rendering, and saved-session rendering.
14
+ - A message file is removed only after its receipt appears in the receiving session. Existing user-message receipts remain recognized to avoid redelivery of saved messages; this is data compatibility, not support for older Pi runtimes.
15
+ - In-flight message IDs prevent duplicate submission. Synchronous submission failures leave the file retryable. Pi owns run preparation and queueing; the plugin does not keep a separate wake reservation or retry timer.
16
+
17
+ Pi 1.2.0 includes the native custom-message preparation fix in [#10739](https://github.com/earendil-works/pi/pull/10739), resolving [#5581](https://github.com/earendil-works/pi/issues/5581) and [#10267](https://github.com/earendil-works/pi/issues/10267). Real SDK regression tests require profile sections to remain present on the first idle delivery, subsequent turns, busy follow-ups, and after reload. No upstream TODO or workaround is retained.
package/docs/releasing.md CHANGED
@@ -1,61 +1,72 @@
1
1
  # Releasing Pi Subagent
2
2
 
3
- Maintainer guide. For installation and usage, see the [README](../README.md).
3
+ English | [简体中文](releasing.zh-CN.md)
4
4
 
5
- ## CI and automatic npm releases
5
+ ## What CI does
6
6
 
7
- GitHub Actions runs type checks, tests, package metadata validation, and a packaging dry run for pull requests and pushes to `main` and `dev`, on Node.js 22.19 and 24.
7
+ `.github/workflows/release.yml` runs on PRs and pushes to `main`/`dev`, and through **Actions → CI and Release → Run workflow**.
8
8
 
9
- A **version increase in `package.json` pushed to `main`** releases automatically after both check jobs pass:
9
+ - Type checks and tests on Node **22.19.0** and **24**.
10
+ - Package/lockfile validation and an npm packaging dry run.
11
+ - The Node 24 job uploads a `.tgz` as `npm-package-<commit SHA>` (retained for 14 days).
12
+ - Only a **version increase pushed to `main` in `oldflag2333333/pi-subagent`** triggers publication, after both check jobs pass. Ordinary pushes, PRs, manual CI runs, and tag pushes do not publish.
10
13
 
11
- 1. Validate the matching version in `package-lock.json`.
12
- 2. Reserve `v<version>` at the exact checked commit.
13
- 3. Publish the npm tarball through OIDC trusted publishing, with provenance.
14
- 4. Create the GitHub Release with automatically generated release notes.
14
+ Publishing uses npm OIDC with provenance, reserves `v<version>` at the checked commit, and creates a GitHub Release. Stable versions use `latest`; prereleases use `next`.
15
15
 
16
- Ordinary code/dependency changes with no version increase do not publish. Neither PRs, `dev` pushes, tag pushes, nor installing the workflow alone publish a package. Stable versions use npm's `latest` tag; prereleases such as `0.2.0-rc.1` use `next` and GitHub's prerelease flag.
16
+ ## First npm release: 0.1.0
17
17
 
18
- ### One-time npm setup
18
+ **npm requires the package to exist before a trusted publisher can be configured.** The first publish therefore needs your npm login, not OIDC. See [npm's prerequisites](https://docs.npmjs.com/cli/v11/commands/npm-trust/#prerequisites).
19
19
 
20
- For a new package, publish the initial version once after committing and checking the source:
20
+ The npm package is **@oldflag2333333/pi-subagent**.
21
21
 
22
- ```bash
23
- npm ci
24
- npm run check
25
- npm login
26
- npm publish --access public --ignore-scripts
27
- ```
22
+ 1. Enable 2FA on your npm account.
23
+ 2. Push the intended release commit to `main`. Wait for the entire **CI and Release** run to succeed.
24
+ 3. Download that run's `npm-package-<commit SHA>` artifact and extract it. Use a trusted `main` run, not a PR artifact.
25
+ 4. In the extracted directory, run:
26
+
27
+ ```bash
28
+ npx --yes npm@11.21.0 login --registry=https://registry.npmjs.org
29
+ npx --yes npm@11.21.0 publish ./oldflag2333333-pi-subagent-0.1.0.tgz --access public --ignore-scripts --registry=https://registry.npmjs.org
30
+ ```
31
+
32
+ Complete the browser/2FA prompts yourself. Do not send credentials or OTPs to an agent. The tarball is the exact package produced by CI; no need to repack it locally. This initial interactive publish does not have CI provenance and does not create a GitHub Release.
33
+
34
+ 5. Verify:
28
35
 
29
- This bootstrap publish creates the package so its trusted publisher can be configured. Subsequent releases use the workflow below, not local `npm publish`.
36
+ ```bash
37
+ npm view @oldflag2333333/pi-subagent version --registry=https://registry.npmjs.org
38
+ ```
30
39
 
31
- In the npm settings for **@oldflag2333333/pi-subagent**, add a **GitHub Actions Trusted Publisher**:
40
+ ## Configure automatic publishing on npm
32
41
 
33
- | Setting | Value |
42
+ Open **npmjs.com → @oldflag2333333/pi-subagent → Settings → Trusted publishing → Add trusted publisher → GitHub Actions**:
43
+
44
+ | Field | Value |
34
45
  | --- | --- |
35
46
  | Organization or user | `oldflag2333333` |
36
47
  | Repository | `pi-subagent` |
37
- | Workflow filename | `release.yml` (filename only, not a path) |
38
- | Environment | Leave blank; this workflow does not use a GitHub Environment |
39
- | Allowed actions | Allow direct `npm publish` |
48
+ | Workflow filename | `release.yml` — filename only |
49
+ | Environment name | Leave empty |
50
+ | Allowed actions | Enable direct `npm publish`, not just staged publishing |
51
+
52
+ No `NPM_TOKEN` or other npm secret is needed in GitHub. The workflow requests `id-token: write` and uses GitHub's built-in token for tags/releases.
40
53
 
41
- No `NPM_TOKEN` secret is needed. GitHub supplies its built-in `GITHUB_TOKEN` for tags/releases; only the release job has `contents: write` and `id-token: write`. Publishing runs on a GitHub-hosted runner with Node 24 and a pinned npm 11 CLI. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) for package-side setup.
54
+ A new trust configuration must complete a successful OIDC publish **within 2 days**, or it expires. Configure it when you are ready for the next version, or recreate it if it expires. After verifying OIDC publishing, npm recommends **Publishing access → Require two-factor authentication and disallow tokens**.
42
55
 
43
- Commit the workflow to GitHub and enable Actions before the first version bump. Keep `package.json`'s `repository.url` aligned with this repository; npm provenance and the release script verify that identity.
56
+ Source: [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
44
57
 
45
- ### Releasing
58
+ ## Subsequent releases
46
59
 
47
- On your feature/release branch:
60
+ With a clean working tree:
48
61
 
49
62
  ```bash
50
- npm version minor --no-git-tag-version
51
- # Or: npm version patch --no-git-tag-version
63
+ npm version patch --no-git-tag-version
64
+ # From 0.1.0, this produces 0.1.1.
52
65
  git add package.json package-lock.json
53
- git commit -m "chore: release v0.2.0"
66
+ git commit -m "chore: release v0.1.1"
67
+ git push origin main
54
68
  ```
55
69
 
56
- Merge that change into `main` (or push it directly if your branch policy allows). The workflow handles the tag, npm publication, and GitHub Release; do not run `npm publish` yourself.
57
-
58
- If publishing fails, correct the external setup and **re-run the original failed workflow** on its original commit. An identical already-published artifact is not republished, so a failed GitHub Release step can be completed safely. An existing tag pointing elsewhere, an npm version with different contents, or a version that would move a dist-tag backwards causes an error instead of an overwrite. A failed npm publish may leave its reserved Git tag, but no GitHub Release is announced until publication succeeds. Publish one version at a time.
59
-
60
- GitHub Releases created by `GITHUB_TOKEN` do not trigger a second release workflow, so npm publication and Release creation deliberately run in the same job.
70
+ Use a PR instead if required by branch policy. Once the version bump reaches `main`, CI publishes npm and creates the tag and GitHub Release automatically. Do not publish locally again or pre-create the tag.
61
71
 
72
+ If publication fails, fix the external configuration and **re-run the original failed workflow**. An identical already-published artifact is not republished; conflicting artifacts or tags fail rather than overwrite existing releases. Never try to reuse an npm version for different contents. Publish one version at a time.
@@ -0,0 +1,72 @@
1
+ # Pi Subagent 发布指南
2
+
3
+ [English](releasing.md) | 简体中文
4
+
5
+ ## CI 已配置的内容
6
+
7
+ 工作流位于 `.github/workflows/release.yml`:
8
+
9
+ - `main`、`dev` 的推送和 PR:在 Node **22.19.0 / 24** 上执行类型检查、测试、包信息校验和打包检查。
10
+ - Node 24 检查通过后上传 `.tgz` 安装包,名称为 `npm-package-<提交 SHA>`,保留 14 天。
11
+ - 可以在 **Actions → CI and Release → Run workflow** 手动运行检查,不会发布。
12
+ - 只有新仓库 `main` 上的**版本递增**才会在所有检查通过后自动发布 npm、创建版本标签和 GitHub Release。
13
+
14
+ 普通代码提交、PR、手动运行 CI、推送标签都不会触发发布。正式版使用 npm 的 `latest`,预发布版使用 `next`。
15
+
16
+ ## 首次发布 0.1.0
17
+
18
+ npm 包名为 **@oldflag2333333/pi-subagent**。
19
+
20
+ **npm 要求包已经存在,才能配置 Trusted Publisher。** 因此首次发布需要你登录 npm 手动完成,之后才使用 CI 自动发布。参见 [npm 官方说明](https://docs.npmjs.com/cli/v11/commands/npm-trust/#prerequisites)。
21
+
22
+ 1. 为 npm 账号开启双因素认证(2FA)。
23
+ 2. 将准备发布的代码推送到 `main`,等待整个 **CI and Release** 工作流通过。
24
+ 3. 下载该次运行的 `npm-package-<提交 SHA>` artifact 并解压。使用确认过的 `main` 构建,不要使用 PR 的产物。
25
+ 4. 在解压目录中执行:
26
+
27
+ ```bash
28
+ npx --yes npm@11.21.0 login --registry=https://registry.npmjs.org
29
+ npx --yes npm@11.21.0 publish ./oldflag2333333-pi-subagent-0.1.0.tgz --access public --ignore-scripts --registry=https://registry.npmjs.org
30
+ ```
31
+
32
+ 按提示完成浏览器登录和 2FA,不要把凭据或验证码发给助手。直接发布 CI 打好的包,不用在本地重新打包。首次交互式发布不带 CI provenance,也不会自动创建 GitHub Release。
33
+
34
+ 5. 确认结果:
35
+
36
+ ```bash
37
+ npm view @oldflag2333333/pi-subagent version --registry=https://registry.npmjs.org
38
+ ```
39
+
40
+ ## npm 网站上需要配置什么
41
+
42
+ 进入 **@oldflag2333333/pi-subagent 包页面 → Settings → Trusted publishing → Add trusted publisher → GitHub Actions**:
43
+
44
+ | 字段 | 填写内容 |
45
+ | --- | --- |
46
+ | Organization or user | `oldflag2333333` |
47
+ | Repository | `pi-subagent` |
48
+ | Workflow filename | `release.yml`,不要填完整路径 |
49
+ | Environment name | 留空 |
50
+ | Allowed actions | 勾选允许直接 `npm publish`,不能只有 staged publishing |
51
+
52
+ **GitHub 不需要添加 `NPM_TOKEN`。** 工作流使用 OIDC 临时身份,标签和 Release 使用 GitHub 自动提供的 token。
53
+
54
+ 新建的信任配置需要在 **2 天内成功完成一次 OIDC 发布**,否则会过期。准备发布下一版时再配置;过期后删除并重建即可。验证自动发布成功后,建议将 **Publishing access** 设置为 **Require two-factor authentication and disallow tokens**。
55
+
56
+ 来源:[npm Trusted publishing 文档](https://docs.npmjs.com/trusted-publishers/)。
57
+
58
+ ## 后续发布
59
+
60
+ 在干净的工作区中执行:
61
+
62
+ ```bash
63
+ npm version patch --no-git-tag-version
64
+ # 0.1.0 → 0.1.1
65
+ git add package.json package-lock.json
66
+ git commit -m "chore: release v0.1.1"
67
+ git push origin main
68
+ ```
69
+
70
+ 如果仓库要求 PR,就通过 PR 将版本变更合入 `main`。之后 CI 自动完成 npm 发布、标签和 GitHub Release,不需要再手动 `npm publish`,也不要提前创建标签。
71
+
72
+ 发布失败时,修正 npm 等外部配置后,重跑**原来失败的工作流**。相同产物不会重复发布;标签或包内容冲突会报错,不会覆盖。不要用同一个 npm 版本号发布不同内容,也不要并行发布多个版本。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oldflag2333333/pi-subagent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Reusable specialist profiles and independent sub-agents for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,15 +45,15 @@
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-agent-core": "*",
47
47
  "@earendil-works/pi-ai": "*",
48
- "@earendil-works/pi-coding-agent": ">=1.0.4",
48
+ "@earendil-works/pi-coding-agent": ">=1.2.0",
49
49
  "@earendil-works/pi-tui": "*",
50
50
  "typebox": "*"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-agent-core": "^1.0.4",
54
- "@earendil-works/pi-ai": "^1.0.4",
55
- "@earendil-works/pi-coding-agent": "^1.0.4",
56
- "@earendil-works/pi-tui": "^1.0.4",
53
+ "@earendil-works/pi-agent-core": "^1.2.0",
54
+ "@earendil-works/pi-ai": "^1.2.0",
55
+ "@earendil-works/pi-coding-agent": "^1.2.0",
56
+ "@earendil-works/pi-tui": "^1.2.0",
57
57
  "@types/node": "^22.15.0",
58
58
  "@types/semver": "^7.8.0",
59
59
  "semver": "^7.8.5",
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { subSessionName } from "../manual-context.js";
5
5
  import { subCapabilityArgs } from "../profiles/launch-args.js";
6
+ import { subSessionDir } from "../sessions.js";
6
7
  import type { SubAgentStatus, SubLaunchSpec, SubSurfaceAdapter, SurfaceHandle } from "../types.js";
7
8
 
8
9
  function parseEnvelope(stdout: string): Record<string, unknown> {
@@ -39,8 +40,9 @@ function subArgs(spec: SubLaunchSpec): string[] {
39
40
  "--no-extensions",
40
41
  "-e", spec.entryPath,
41
42
  ...(fs.existsSync(herdrIntegration) ? ["-e", herdrIntegration] : []),
42
- ...(spec.profile.sessionPersistence === "persistent" || spec.resumeSessionId ? [] : ["--no-session"]),
43
- ...(spec.resumeSessionId ? ["--session", spec.resumeSessionId] : []),
43
+ ...(spec.profile.sessionPersistence === "persistent" || spec.resumeSessionId
44
+ ? ["--session-dir", subSessionDir(spec.mainSessionId)] : ["--no-session"]),
45
+ ...(spec.resumeSessionId ? ["--session", spec.resumeSessionFile ?? spec.resumeSessionId] : []),
44
46
  spec.projectTrusted ? "--approve" : "--no-approve",
45
47
  "--name", subSessionName(safeLabel(spec.title), spec.origin),
46
48
  ...subCapabilityArgs(spec.profile, spec.entryPath),
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { channelPath, runtimeRoot } from "../channel.js";
5
+ import { subSessionDir, subSessionsRoot } from "../sessions.js";
5
6
 
6
7
  function entries(directory: string): fs.Dirent[] {
7
8
  try { return fs.readdirSync(directory, { withFileTypes: true }); }
@@ -26,11 +27,16 @@ function sessionId(file: string): string {
26
27
  } finally { fs.closeSync(fd); }
27
28
  }
28
29
 
29
- export function cleanSubChannels(currentMainId: string, currentSessionDir: string): { removed: number; errors: string[] } {
30
- const root = runtimeRoot();
31
- const candidates = entries(root).filter((entry) => entry.isDirectory());
32
- if (candidates.length === 0) return { removed: 0, errors: [] };
33
- if (fs.lstatSync(root).isSymbolicLink()) throw new Error("Refusing to clean a symlinked runtime directory.");
30
+ export function cleanSubFiles(currentMainId: string, currentSessionDir: string): { removed: number; errors: string[] } {
31
+ const stores = [
32
+ { root: runtimeRoot(), ownerDirectory: (id: string) => path.basename(path.dirname(channelPath(id, "unused"))) },
33
+ { root: subSessionsRoot(), ownerDirectory: (id: string) => path.basename(subSessionDir(id)) },
34
+ ].map((store) => {
35
+ const candidates = entries(store.root).filter((entry) => entry.isDirectory());
36
+ if (candidates.length && fs.lstatSync(store.root).isSymbolicLink()) throw new Error(`Refusing to clean symlinked directory: ${store.root}`);
37
+ return { ...store, candidates };
38
+ });
39
+ if (stores.every((store) => store.candidates.length === 0)) return { removed: 0, errors: [] };
34
40
 
35
41
  const sessionsRoot = path.join(getAgentDir(), "sessions");
36
42
  const directories = new Set([sessionsRoot]);
@@ -46,19 +52,21 @@ export function cleanSubChannels(currentMainId: string, currentSessionDir: strin
46
52
  if (entry.name.endsWith(".jsonl")) live.add(sessionId(path.join(directory, entry.name)));
47
53
  }
48
54
  }
49
- const liveDirectories = new Set([...live].map((id) => path.basename(path.dirname(channelPath(id, "unused")))));
50
55
  let removed = 0;
51
56
  const errors: string[] = [];
52
- for (const entry of candidates) {
53
- if (liveDirectories.has(entry.name)) continue;
54
- const directory = path.join(root, entry.name);
55
- try {
56
- // Do not follow a directory replaced with a symlink during discovery.
57
- if (!fs.lstatSync(directory).isDirectory()) continue;
58
- fs.rmSync(directory, { recursive: true, force: true });
59
- removed++;
60
- } catch (error) {
61
- errors.push(`${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
57
+ for (const store of stores) {
58
+ const liveDirectories = new Set([...live].map(store.ownerDirectory));
59
+ for (const entry of store.candidates) {
60
+ if (liveDirectories.has(entry.name)) continue;
61
+ const directory = path.join(store.root, entry.name);
62
+ try {
63
+ // Do not follow a directory replaced with a symlink during discovery.
64
+ if (!fs.lstatSync(directory).isDirectory()) continue;
65
+ fs.rmSync(directory, { recursive: true, force: true });
66
+ removed++;
67
+ } catch (error) {
68
+ errors.push(`${directory}: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
62
70
  }
63
71
  }
64
72
  return { removed, errors };
@@ -66,12 +74,12 @@ export function cleanSubChannels(currentMainId: string, currentSessionDir: strin
66
74
 
67
75
  export function registerCleanSubCommand(pi: ExtensionAPI): void {
68
76
  pi.registerCommand("clean-sub", {
69
- description: "Delete runtime communication files whose Main session can no longer be found; keep persistent Sub sessions.",
77
+ description: "Delete managed Sub sessions and communication directories whose Main session can no longer be found.",
70
78
  handler: async (_args, ctx) => {
71
79
  try {
72
- const result = cleanSubChannels(ctx.sessionManager.getSessionId(), ctx.sessionManager.getSessionDir());
80
+ const result = cleanSubFiles(ctx.sessionManager.getSessionId(), ctx.sessionManager.getSessionDir());
73
81
  ctx.ui.notify([
74
- `Removed ${result.removed} orphaned Main communication directories. Persistent Sub sessions were not deleted.`,
82
+ `Removed ${result.removed} orphaned Sub data directories (sessions and communication).`,
75
83
  ...result.errors,
76
84
  ].join("\n"), result.errors.length ? "warning" : "info");
77
85
  } catch (error) {
@@ -49,8 +49,8 @@ export function forgetTalk(ctx: ExtensionContext, direction: TalkDirection, runI
49
49
  /** Queue behind streaming work, but never compete with manual compaction or prompt preflight. */
50
50
  export function canUseNativeQueue(ctx: ExtensionContext): boolean {
51
51
  const value = state(ctx);
52
- // sendUserMessage awaits input/auth hooks before Pi becomes busy. Submit only
53
- // one idle prompt at a time; its receipt or agent_start will wake the rest.
52
+ // Serialize idle submissions until Pi starts the run or records its receipt.
53
+ // Once active, subsequent deliveries use Pi's native follow-up queue.
54
54
  if (ctx.isIdle()) return ![...value.pending.values()].some((pending) => pending.queued && !pending.queuedWhileBusy);
55
55
  return value.active || ctx.signal !== undefined;
56
56
  }
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
24
24
  startupProfile.register();
25
25
  new MainContextRuntime(pi).register();
26
26
 
27
- // Preserve rendering for legacy custom messages in resumed sessions.
27
+ // Render live deliveries and saved custom messages with the same inbox card.
28
28
  pi.registerMessageRenderer(MESSAGE_TYPE, (message, options, theme) => {
29
29
  const details = message.details as { title?: string; message?: string } | undefined;
30
30
  const title = details?.title ?? "Sub";
@@ -226,7 +226,7 @@ export class MainRunManager {
226
226
  cwd,
227
227
  projectTrusted,
228
228
  ...(input.origin ? { origin: input.origin } : {}),
229
- ...(resume ? { resumeSessionId: resume.sessionId } : {}),
229
+ ...(resume ? { resumeSessionId: resume.sessionId, resumeSessionFile: resume.sessionFile } : {}),
230
230
  profile: input.profile,
231
231
  channelDir: channel.channelDir,
232
232
  token: channel.token,
package/src/sessions.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
4
  import type { RunSnapshot } from "./types.js";
3
5
 
6
+ export function subSessionsRoot(): string {
7
+ return path.join(getAgentDir(), "subagent", "sessions");
8
+ }
9
+
10
+ export function subSessionDir(mainSessionId: string): string {
11
+ if (!mainSessionId || mainSessionId === "." || mainSessionId === "..") throw new Error("Invalid Main session ID.");
12
+ return path.join(subSessionsRoot(), encodeURIComponent(mainSessionId));
13
+ }
14
+
4
15
  export interface ResumableSubSession {
5
16
  sessionId: string;
6
17
  sessionFile: string;
@@ -1,12 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { MESSAGE_TYPE, removeTalk, type TalkDirection } from "./channel.js";
3
3
  import type { DelegateManifest, TalkMessage } from "./types.js";
4
- import { formatTalkInput, readTalkReceipt } from "./talk-message.js";
4
+ import { readTalkReceipt } from "./talk-message.js";
5
5
  import { canUseNativeQueue, forgetTalk, markTalkQueued, rememberTalk } from "./inbox-state.js";
6
6
 
7
7
  function hasReceipt(ctx: ExtensionContext, direction: TalkDirection, runId: string, messageId: string): boolean {
8
8
  return ctx.sessionManager.getEntries().some((entry) => {
9
- // Old custom-message receipts still count after upgrading or reloading.
9
+ // Accept both custom receipts and user-message receipts from v0.1.0.
10
10
  const details = entry.type === "message" ? readTalkReceipt(entry.message)
11
11
  : entry.type === "custom_message" && entry.customType === MESSAGE_TYPE
12
12
  ? entry.details as { direction?: string; runId?: string; messageId?: string } | undefined
@@ -15,7 +15,7 @@ function hasReceipt(ctx: ExtensionContext, direction: TalkDirection, runId: stri
15
15
  });
16
16
  }
17
17
 
18
- /** pi.sendUserMessage is fire-and-forget; its return is not a session receipt. */
18
+ /** pi.sendMessage is fire-and-forget; its return is not a session receipt. */
19
19
  export function deliverTalk(
20
20
  pi: ExtensionAPI,
21
21
  ctx: ExtensionContext,
@@ -37,11 +37,13 @@ export function deliverTalk(
37
37
  try {
38
38
  // Pi owns waiting behind current work, not Pi Subagent. In-flight IDs prevent
39
39
  // repeated file notifications from submitting the same follow-up twice.
40
- pi.sendUserMessage(formatTalkInput({ direction, runId: manifest.runId, messageId: message.id }, content), {
41
- deliverAs: "followUp",
42
- // Peer text is input, not an instruction to execute slash commands or templates.
43
- expandPromptTemplates: false,
44
- });
40
+ // Pi 1.2+ prepares idle custom-message runs and preserves their prompt sections.
41
+ pi.sendMessage({
42
+ customType: MESSAGE_TYPE,
43
+ content,
44
+ display: true,
45
+ details: { title: manifest.title, message: message.message, direction, runId: manifest.runId, messageId: message.id },
46
+ }, { deliverAs: "followUp", triggerTurn: true });
45
47
  } catch (error) {
46
48
  pending.queued = false;
47
49
  throw error;
package/src/tools/sub.ts CHANGED
@@ -154,7 +154,7 @@ export function registerSub(pi: ExtensionAPI): void {
154
154
  return sections;
155
155
  });
156
156
 
157
- // Preserve rendering for legacy custom messages in resumed sessions.
157
+ // Render live deliveries and saved custom messages with the same inbox card.
158
158
  pi.registerMessageRenderer(MESSAGE_TYPE, (message, options, theme) => {
159
159
  const details = message.details as { title?: string; message?: string } | undefined;
160
160
  const title = details?.title ?? loaded.manifest.title;
package/src/types.ts CHANGED
@@ -97,6 +97,8 @@ export interface SubLaunchSpec {
97
97
  projectTrusted: boolean;
98
98
  origin?: "manual";
99
99
  resumeSessionId?: string;
100
+ /** Recorded path also supports sessions saved before per-Main directories. */
101
+ resumeSessionFile?: string;
100
102
  profile: ResolvedProfile;
101
103
  channelDir: string;
102
104
  token: string;