billion-context-pi 0.1.55 → 0.1.56-pr.266.96

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
@@ -22,7 +22,7 @@ The model decides <em>when</em> and <em>what</em> to compress — not a hard lim
22
22
 
23
23
  ---
24
24
 
25
- > **Host support:** this plugin is for **Pi**. It does **not** support **OMP (oh-my-pi)** — on an OMP host it refuses to run. OMP users: use [billion-context](https://github.com/ranxianglei/billion-context) instead (`bili omp`). Details: [docs/omp.md](./docs/omp.md).
25
+ > **Host support:** this plugin is for **Pi**. It does **not** support **OMP (oh-my-pi)** — on an OMP host it refuses to run. OMP users: use [billion-context](https://github.com/ranxianglei/billion-context) instead (`bili omp`, built-in plugin). Full client → package table: see [Which do I need?](#which-do-i-need); OMP details: [docs/omp.md](./docs/omp.md).
26
26
 
27
27
  ## Why?
28
28
 
@@ -39,6 +39,19 @@ This means:
39
39
  1. **A single session handles enormous workloads.** Per simulation tests of the three-tier architecture (see [opencode-acp](https://github.com/ranxianglei/opencode-acp)), one session can process on the order of 10–60 billion cumulative tokens — while retaining long-term memory of distant key information (paths, decisions, signatures). You can work in the **same session for months** without outgrowing the context.
40
40
  2. **Context stays lean over the long run.** In practice context typically holds under ~150K tokens (opencode-acp keeps it under ~200K), so compared to traditional compaction that lets context balloon toward 1M, **a single session costs roughly 5× less in tokens**.
41
41
 
42
+ ## Which do I need?
43
+
44
+ Pick by your client:
45
+
46
+ | Client | Use |
47
+ |---|---|
48
+ | **pi** | [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi) (in-process extension) |
49
+ | **opencode** | [`opencode-acp`](https://github.com/ranxianglei/opencode-acp) (in-process extension) |
50
+ | **omp** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili omp` (built-in plugin) |
51
+ | **everything else** | [`billion-context`](https://github.com/ranxianglei/billion-context) — `bili <client>` (launcher, preferred) or `/bili/` prefix |
52
+
53
+ > Why OMP can't use this in-process extension, and what happens if you try anyway: see [Host support](#host-support) and [docs/omp.md](./docs/omp.md).
54
+
42
55
  ## Install
43
56
 
44
57
  ```bash
@@ -76,7 +89,7 @@ This has two practical implications:
76
89
 
77
90
  ## Host support
78
91
 
79
- billion-context-pi is built for the **Pi** coding agent (`@earendil-works/pi-coding-agent`) and detects the host at session start:
92
+ billion-context-pi is built for the **Pi** coding agent (`@earendil-works/pi-coding-agent`) and detects the host at session start — the full client → package table lives in [Which do I need?](#which-do-i-need):
80
93
 
81
94
  - **Pi** — fully supported.
82
95
  - **OMP (`can1357/oh-my-pi`)** — **not supported.** OMP's in-process session API diverges from Pi's, so the compression refs the extension injects can drift out of sync with the session's real refs and `compress` calls fail with `does not exist in this session` (issue [#234](https://github.com/ranxianglei/billion-context-pi/issues/234)). On OMP the extension now **refuses service**: it prints a warning, disables the ACP tools, and leaves the host's own context handling untouched.
@@ -120,7 +133,7 @@ Worker runs on Pi's full default toolset - no `--tools` allowlist is applied, so
120
133
 
121
134
  The full delegate result is saved to a file (`/tmp/acp-delegate/<runId>.out`); the tool result and injected notification carry only the **task title + file path** (no preview) - use `read` for the details. This keeps the parent context lean.
122
135
 
123
- - **Interactive (TUI) & RPC modes**: `async:true` (default) runs the child in the background; a short completion notification is injected into the chat when it finishes.
136
+ - **Interactive (TUI) & RPC modes**: `async:true` (default) runs the child in the background; a short completion notification is injected into the chat when it finishes — **unless the model already read the result file after the run finished** (detected via the `read` tool or a bash command referencing the file), in which case the notification is skipped: the model already has the result, so re-injecting it would only waste context. Set `delegate: { notifyIfRead: "always" }` in `acp.json` to restore the always-inject behavior.
124
137
  - **Print / JSON modes** (`pi -p`, SDK): `async:true` auto-downgrades to **synchronous** — the result returns as the tool result in the same turn (the parent exits after one turn, so background injection would be lost).
125
138
  - **Failures are loud, never silent.** A run that fails (nonzero exit, spawn error, watchdog timeout) injects a `FAILED ⚠️` notification carrying a short error excerpt, so a failed delegate cannot hide among sibling completions. If a notification cannot be delivered at all, a recovery notice is attached to the next delegate notification or the next `acp_delegate` / `acp_delegate_wait` / `acp_delegate_cancel` tool result — a dispatched run's failure always reaches the model before it wraps up.
126
139
 
@@ -196,6 +209,37 @@ billion-context protects three categories of content from compression:
196
209
  2. **Soft recent-zone** — the last N messages (default 5) and last ~5K tokens are soft-protected so the model keeps its working set. Tool results from `decompress`, `search_context`, `read`, and `bash` are **excluded** from this zone: they're large and meant to be compressible once consumed, so they don't eat the protected budget.
197
210
  3. **Last user message** — always protected (user intent must survive).
198
211
 
212
+ ## Session storage & migration
213
+
214
+ billion-context-pi persists each session's compression state in a **sidecar file** next to the session transcript. Every session lives as two files in Pi's sessions directory (`~/.pi/agent/sessions/`):
215
+
216
+ | File | Contents |
217
+ |------|----------|
218
+ | `<id>.jsonl` | The conversation transcript (messages, tool calls) |
219
+ | `<id>.jsonl.acp.json` | The ACP compression state (compressed blocks, message refs, nudge + stats) |
220
+
221
+ The `.acp.json` sidecar is what holds your compressed blocks. Without it, the session runs on its full raw history until ACP compresses again.
222
+
223
+ ### Migrating a session (cross-machine copy / backup-restore)
224
+
225
+ Pi's built-in export/import moves only the **transcript**, not the ACP state. Two things are lost:
226
+
227
+ 1. **The `.acp.json` sidecar is not carried.** An imported session therefore has *no* compressed blocks: every LLM call resends the entire raw history until the nudge re-compresses it — a one-time full re-cache cost plus context bloat back to original size. Very long sessions can approach or exceed the model window before re-compression kicks in (Pi's native compaction is disabled while this plugin is active, so ACP is the only context manager).
228
+ 2. **Export drops the `parentSession` header.** Clone/fork child sessions rely on that header field to inherit their parent's compression state; once exported, the link is gone even if the parent's files still exist on the target machine.
229
+
230
+ **To migrate a session with its compression state intact, copy both files together** (they share the same base name):
231
+
232
+ ```bash
233
+ # copy the pair
234
+ cp <id>.jsonl <id>.jsonl.acp.json <dest>/
235
+ # ... or back up / restore the whole directory
236
+ cp -r ~/.pi/agent/sessions <backup>/pi-sessions
237
+ ```
238
+
239
+ Restore them next to each other on the target machine. For clone/fork children, also bring the parent's pair so `parentSession` resolves.
240
+
241
+ > A permanent fix — the host carrying the sidecar through import/export and preserving `parentSession` — belongs upstream in pi-coding-agent and is tracked in issue [#299](https://github.com/ranxianglei/billion-context-pi/issues/299). Until it lands, copy the pair manually.
242
+
199
243
  ## Built on acp-kernel
200
244
 
201
245
  The compression engine is [`acp-kernel`](https://github.com/ranxianglei/acp-kernel) — a platform-agnostic, MIT-licensed library with 208 tests. It's bundled inline into `dist/index.js`, so there are zero runtime dependencies.
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  ---
22
22
 
23
- > **宿主支持:** 本插件面向 **Pi**。它**不支持 OMP(oh-my-pi)** —— 在 OMP 宿主上会拒绝运行。OMP 用户请改用 [billion-context](https://github.com/ranxianglei/billion-context)(`bili omp`)。详细说明:[docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
23
+ > **宿主支持:** 本插件面向 **Pi**。它**不支持 OMP(oh-my-pi)** —— 在 OMP 宿主上会拒绝运行。OMP 用户请直接改用 [billion-context](https://github.com/ranxianglei/billion-context)(启动命令 bili omp);其他客户端的完整对照见[该选哪个?](#该选哪个)。OMP 详细说明:[docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
24
24
 
25
25
  ## 为什么选择 billion-context
26
26
 
@@ -38,6 +38,19 @@
38
38
  1. **一个会话即可支撑海量工作。** 根据三级压缩架构的模拟测试(见 [opencode-acp](https://github.com/ranxianglei/opencode-acp)),单会话累计可处理约 100 亿至 600 亿 token —— 同时对遥远的关键信息(路径、决策、签名)保持长久记忆。用户可以在**同一个会话里连续工作几个月**,而无需因为上下文膨胀而开新会话丢上下文。
39
39
  2. **上下文长期保持精简。** 实际运行中上下文通常稳定在 15 万 token 以下(opencode-acp 实测维持在 20 万以下),相比传统压缩方案动辄撑到 100 万上下文,**单会话累计可节省近 5 倍的 token 费用**。
40
40
 
41
+ ## 该选哪个?
42
+
43
+ 按客户端选:
44
+
45
+ | 客户端 | 用这个 |
46
+ |---|---|
47
+ | **pi** | [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi)(进程内扩展) |
48
+ | **opencode** | [`opencode-acp`](https://github.com/ranxianglei/opencode-acp)(进程内扩展) |
49
+ | **omp** | [`billion-context`](https://github.com/ranxianglei/billion-context),`bili omp`(内置插件) |
50
+ | **其余所有** | [`billion-context`](https://github.com/ranxianglei/billion-context) —— `bili <client>`(启动器,优先)或 `/bili/` 前缀 |
51
+
52
+ > 为什么 OMP 不能用本进程内扩展、以及强行使用的后果:见下文[宿主支持](#宿主支持)与 [docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
53
+
41
54
  ## 安装
42
55
 
43
56
  ```bash
@@ -75,7 +88,7 @@ billion-context 通过拦截 Pi 的 `context` 事件接管上下文管理。**Pi
75
88
 
76
89
  ## 宿主支持
77
90
 
78
- billion-context-pi 面向 **Pi** 编码代理(`@earendil-works/pi-coding-agent`)构建,并在会话开始时检测宿主:
91
+ billion-context-pi 面向 **Pi** 编码代理(`@earendil-works/pi-coding-agent`)构建,并在会话开始时检测宿主——完整的「客户端 → 包」对照表见[该选哪个?](#该选哪个):
79
92
 
80
93
  - **Pi** — 完全支持。
81
94
  - **OMP(`can1357/oh-my-pi`)** — **不支持。** OMP 的进程内会话 API 与 Pi 不同,扩展注入的压缩引用可能与会话的真实引用漂移失步,导致 `compress` 调用失败,报错 `does not exist in this session`(issue [#234](https://github.com/ranxianglei/billion-context-pi/issues/234))。在 OMP 上,扩展现在会**拒绝服务**:打印警告、禁用 ACP 工具,并保持宿主自身的上下文处理不受影响。
@@ -196,6 +209,37 @@ billion-context 保护三类内容不被压缩:
196
209
  2. **软近期区** — 最后 N 条消息(默认 5)和最后约 5K token 被软保护,让模型保留工作集。来自 `decompress`、`search_context`、`read`、`bash` 的工具结果被**排除**出此区:它们体量大、消费后就该能压缩,所以不该占用保护预算。
197
210
  3. **最后一条用户消息** — 始终保护(用户意图必须存活)。
198
211
 
212
+ ## 会话存储与迁移
213
+
214
+ billion-context-pi 把每个会话的压缩状态持久化在会话转录文件旁边的一个**旁挂(sidecar)文件**里。每个会话在 Pi 的会话目录(`~/.pi/agent/sessions/`)下都有两个文件:
215
+
216
+ | 文件 | 内容 |
217
+ |------|------|
218
+ | `<id>.jsonl` | 会话转录(消息、工具调用) |
219
+ | `<id>.jsonl.acp.json` | ACP 压缩状态(压缩块、消息引用、nudge 与统计) |
220
+
221
+ `.acp.json` 旁挂文件承载了你的压缩块。没有它,会话就会以完整原始历史运行,直到 ACP 再次压缩。
222
+
223
+ ### 迁移会话(跨机器拷贝 / 备份恢复)
224
+
225
+ Pi 内置的导出/导入只搬运**转录**,不搬 ACP 状态。会丢失两样东西:
226
+
227
+ 1. **`.acp.json` 旁挂文件不会被携带。** 因此导入后的会话*没有任何*压缩块:每次 LLM 调用都会重发完整原始历史,直到 nudge 重新压缩 —— 一次性全量重缓存成本 + 上下文膨胀回原始大小。超长会话可能在重新压缩生效前逼近甚至超出模型窗口(本插件激活时 Pi 的原生 compaction 被禁用,ACP 是唯一的上下文管理者)。
228
+ 2. **导出会丢弃 `parentSession` 头。** clone/fork 子会话依赖这个头字段来继承父会话的压缩状态;一旦导出,这条链接就断了 —— 即使目标机器上父会话的文件仍然存在。
229
+
230
+ **要带着完整压缩状态迁移会话,请把两个文件一起拷贝**(它们共享同一基础名):
231
+
232
+ ```bash
233
+ # 成对拷贝
234
+ cp <id>.jsonl <id>.jsonl.acp.json <目标目录>/
235
+ # ... 或整体备份 / 恢复整个目录
236
+ cp -r ~/.pi/agent/sessions <备份>/pi-sessions
237
+ ```
238
+
239
+ 在目标机器上把它们放回彼此相邻的位置。对 clone/fork 子会话,还要带上父会话的那一对,以便 `parentSession` 能解析。
240
+
241
+ > 根治方案 —— 让宿主在 import/export 时一并携带旁挂文件并保留 `parentSession` —— 属于上游 pi-coding-agent,已在 issue [#299](https://github.com/ranxianglei/billion-context-pi/issues/299) 跟踪。落地前请手动成对拷贝。
242
+
199
243
  ## 基于 acp-kernel
200
244
 
201
245
  压缩引擎是 [`acp-kernel`](https://github.com/ranxianglei/acp-kernel) — 平台无关、MIT 许可的库,有 208 个测试。它被内联打包进 `dist/index.js`,因此零运行时依赖。
package/dist/config.d.ts CHANGED
@@ -1,5 +1,21 @@
1
1
  import { type Config, type Prompts } from "acp-kernel";
2
2
  import type { ThrottleRetryConfig } from "./throttle-retry.js";
3
+ /** Per-role delegate defaults. Lets long-lived automation pin a cheaper or
4
+ * more capable model and a thinking level per delegate role, so the main
5
+ * agent doesn't have to fill them in on every `acp_delegate()` call. */
6
+ export interface DelegateRoleConfig {
7
+ /** Default model for this role, as `"provider/id"`. Resolution priority:
8
+ * per-call `model` > this role default > parent agent's current model. A
9
+ * value that isn't a valid `"provider/id"` is ignored (treated as unset).
10
+ * If the configured model doesn't exist in the live registry, the child
11
+ * falls back to the parent model and a warning is logged — it never fails. */
12
+ model?: string;
13
+ /** Default thinking level for this role: one of off|minimal|low|medium|
14
+ * high|xhigh|max. Resolution priority: per-call `thinkingLevel` > this role
15
+ * default > global `delegate.thinkingLevel` > Pi's own default. An invalid
16
+ * value is ignored with a warning (never fails). */
17
+ thinkingLevel?: string;
18
+ }
3
19
  /** Delegate sub-agent configuration. */
4
20
  export interface DelegateConfig {
5
21
  /** Enable acp_delegate tools (delegate/wait/cancel) and their system-prompt
@@ -12,7 +28,68 @@ export interface DelegateConfig {
12
28
  * "merged" — delegate token usage folded into the tool-result usage field,
13
29
  * counted as part of the main session totals. */
14
30
  displayUsage?: "merged" | "separate";
31
+ /** Maximum acp_delegate nesting depth. Default: 2 (main → child → grandchild;
32
+ * the grandchild cannot delegate further). Set 1 to forbid nested delegation
33
+ * (orchestrator → leaf workers only). The resolved value is propagated to
34
+ * child processes via PI_ACP_DELEGATE_MAX_DEPTH so the cap follows the whole
35
+ * delegation tree, even when a child loads a different project acp.json. */
36
+ maxDepth?: number;
37
+ /** Hard timeout for synchronous delegates (async=false, or async auto-downgraded
38
+ * on one-shot hosts), in minutes. Default: 5. 0 or null disables the timeout
39
+ * (the run blocks until the child exits or the tool call is cancelled). */
40
+ syncTimeoutMinutes?: number | null;
41
+ /** Idle watchdog for async delegates: kill when no output arrives for this many
42
+ * minutes. Default: 5. This is the main defense against a stuck child holding
43
+ * its stdout fd open, so disabling it (0/null) logs a warning — use
44
+ * acp_delegate_cancel as the manual escape hatch. */
45
+ idleTimeoutMinutes?: number | null;
46
+ /** Hard time limit for async delegates, in minutes. Default: 30. 0 or null
47
+ * disables the limit. */
48
+ asyncTimeoutMinutes?: number | null;
49
+ /** Cap on how many background (async) delegate processes run at once.
50
+ * `1` forces strict serial execution; `N` allows up to N in parallel;
51
+ * omitted means unlimited (existing behavior). Extra launches are queued and
52
+ * start automatically as slots free. Invalid values (non-integer, <1) fall
53
+ * back to unlimited with a warning. Env `PI_ACP_DELEGATE_MAX_CONCURRENT`
54
+ * overrides this. See #294. */
55
+ maxConcurrent?: number;
56
+ /** Global default thinking level applied to every delegate when neither the
57
+ * per-call `thinkingLevel` nor the role's own `thinkingLevel` is set. One of
58
+ * off|minimal|low|medium|high|xhigh|max. When unset at all levels, no
59
+ * `--thinking` flag is passed and each child uses Pi's own default. */
60
+ thinkingLevel?: string;
61
+ /** Per-role defaults keyed by role name (reviewer/researcher/worker/planner/
62
+ * oracle, or any custom role). See DelegateRoleConfig. Only affects roles
63
+ * that are named here; other roles inherit the parent model + Pi defaults. */
64
+ agents?: Record<string, DelegateRoleConfig>;
65
+ /** What happens to the completion notification when the model has already
66
+ * read the delegate's result file after the run finished.
67
+ * "skip" (default) — the notification is not injected; the model already
68
+ * saw the result, so re-injecting it would only waste context.
69
+ * "always" — always inject the notification (previous behavior). */
70
+ notifyIfRead?: "skip" | "always";
71
+ }
72
+ /** Resolved delegate policy: what actually takes effect after merging acp.json,
73
+ * env overrides and defaults. Timeout fields are milliseconds; null means the
74
+ * corresponding timeout/watchdog is disabled. */
75
+ export interface DelegatePolicy {
76
+ enabled: boolean;
77
+ displayUsage: "merged" | "separate";
78
+ maxDepth: number;
79
+ syncTimeoutMs: number | null;
80
+ idleMs: number | null;
81
+ asyncTimeoutMs: number | null;
82
+ /** Resolved cap on concurrent background delegates; Infinity = unlimited. */
83
+ maxConcurrent: number;
84
+ /** Global default thinking level (undefined when unset). */
85
+ thinkingLevel?: string;
86
+ /** Per-role defaults keyed by role name (undefined when unset). */
87
+ agents?: Record<string, DelegateRoleConfig>;
88
+ /** Whether to suppress the completion notification when the model already
89
+ * read the result file after the run finished. Always resolved ("skip" default). */
90
+ notifyIfRead: "skip" | "always";
15
91
  }
92
+ export declare const DEFAULT_DELEGATE_POLICY: DelegatePolicy;
16
93
  /** Compression tuning fields, shared by all three levels (global, provider,
17
94
  * model). Percentage fields accept a ratio (0.75) or percent string ("75%").
18
95
  * Resolution is per-field, deepest-wins (model > provider > global); an
@@ -117,11 +194,10 @@ export interface AdapterConfig {
117
194
  export declare const DEFAULT_TOOL_BASH_TIMEOUT = 60;
118
195
  export declare const DEFAULT_TOOL_OUTPUT_MAX_BYTES = 200000;
119
196
  /** Resolve delegate config from the adapter, handling the boolean shorthand
120
- * and the legacy flat `displayUsage` alias. */
121
- export declare function resolveDelegate(adapter: AdapterConfig): {
122
- enabled: boolean;
123
- displayUsage: "merged" | "separate";
124
- };
197
+ * and the legacy flat `displayUsage` alias. Precedence: env > acp.json >
198
+ * default (same convention as ACP_MODEL_CONTEXT_LIMIT). Invalid values fall
199
+ * back to the default with a logged warning — they never fail the session. */
200
+ export declare function resolveDelegate(adapter: AdapterConfig): DelegatePolicy;
125
201
  /** Per-field deepest-wins merge of the three compression levels (global →
126
202
  * provider → model). An undefined field at a deeper level does NOT clear a
127
203
  * value set at a shallower level — only a defined value overrides. */
@@ -1,15 +1,21 @@
1
1
  import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
2
  import { Type, type Static } from "typebox";
3
3
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { type DelegatePolicy, type DelegateRoleConfig } from "./config.js";
4
5
  import { type Usage } from "./delegate-events.js";
6
+ export declare const OUT_DIR: string;
5
7
  export declare function delegateStdinText(resumeFrom: boolean, task: string | undefined): string;
6
8
  export declare function delegateSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): SpawnOptions;
9
+ /** Child process env for a nested delegate: depth increments by one, and the
10
+ * resolved maxDepth rides along so the cap binds the whole delegation tree
11
+ * even when the child loads a different project acp.json. */
12
+ export declare function delegateChildEnv(parentDepth: number, maxDepth: number): NodeJS.ProcessEnv;
7
13
  /** Resolve the pi CLI entry for delegate child processes.
8
14
  * argv[1] is only the pi CLI under a CLI host; embedded hosts (e.g. pi-web)
9
15
  * run the SDK inside another node process, so probe instead. Non-pi hosts
10
16
  * (omp) keep argv[1] untouched. */
11
17
  export declare function resolvePiCliEntry(argv1: string, env?: NodeJS.ProcessEnv, piHost?: boolean): string;
12
- export type RunStatus = "running" | "completed" | "failed" | "cancelled";
18
+ export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
13
19
  interface DelegateRun {
14
20
  runId: string;
15
21
  agent: string;
@@ -49,11 +55,55 @@ interface DelegateRun {
49
55
  /** True while the run sits in the coalescing notification queue (scheduled
50
56
  * for the next batched flush, not yet delivered). */
51
57
  notifyQueued?: boolean;
58
+ /** When the model successfully read this run's result file (read tool, or a
59
+ * bash command referencing it). A read at/after finishedAt means the model
60
+ * already saw the final result — the completion notification is then
61
+ * suppressed (notifyIfRead: "skip"). */
62
+ readAt?: number;
63
+ /** True when the completion notification was suppressed because the model
64
+ * had already read the result file. Treated as delivered (injected=true)
65
+ * so wait/recovery never re-surface the result. */
66
+ readSuppressed?: boolean;
52
67
  }
53
68
  export declare function addDelegateUsage(u: Usage): void;
54
69
  export declare function getDelegateUsage(): Usage | undefined;
55
70
  export declare function resetDelegateUsage(): void;
56
71
  export declare function setDelegateDisplayUsage(mode: "merged" | "separate"): void;
72
+ export declare function setDelegatePolicy(policy: DelegatePolicy): void;
73
+ export declare class ConcurrencyGate {
74
+ private active;
75
+ private queue;
76
+ private readonly capacityOf;
77
+ constructor(capacityOf: () => number);
78
+ private get capacity();
79
+ get unlimited(): boolean;
80
+ get activeCount(): number;
81
+ get queuedCount(): number;
82
+ /** Start `launch` immediately when a slot is free and nothing waits ahead;
83
+ * otherwise hold it in the FIFO queue. True when it started right away. */
84
+ launchOrQueue(runId: string, launch: () => void): boolean;
85
+ cancelQueued(runId: string): boolean;
86
+ /** A live child hit a terminal state: free its slot, then start waiting
87
+ * launches FIFO (skipping any cancelled while queued). */
88
+ release(): void;
89
+ }
90
+ export declare function setDelegateDefaults(d: {
91
+ thinkingLevel?: string;
92
+ agents?: Record<string, DelegateRoleConfig>;
93
+ } | undefined): void;
94
+ export declare function resetDelegateDefaults(): void;
95
+ declare const VALID_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
96
+ export declare function isValidThinkingLevel(v: unknown): v is (typeof VALID_THINKING_LEVELS)[number];
97
+ export declare function setDelegateNotifyIfRead(mode: "skip" | "always"): void;
98
+ /** Mark the run whose result file the model just read (via the `read` tool).
99
+ * Returns true when a run matched. A read of the final result file after the
100
+ * run finished means the model already saw the result — the completion
101
+ * notification is then suppressed (notifyIfRead: "skip"). */
102
+ export declare function markDelegateResultRead(filePath: string): boolean;
103
+ /** Mark runs referenced by a bash command (e.g. `cat <result file>`).
104
+ * Matches runIds appearing anywhere in the command string; only existing
105
+ * runs are affected, so task text that merely contains "del_..." is inert. */
106
+ export declare function markDelegateRunReadByCommand(command: string): boolean;
57
107
  /** Snapshot of currently-running delegate runs, for the TUI status widget. */
58
108
  export declare function runningRunsSnapshot(): {
59
109
  runId: string;
@@ -61,6 +111,31 @@ export declare function runningRunsSnapshot(): {
61
111
  task: string;
62
112
  startedAt: number;
63
113
  }[];
114
+ export interface FleetRunView {
115
+ runId: string;
116
+ agent: string;
117
+ task: string;
118
+ cwd: string;
119
+ startedAt: number;
120
+ finishedAt?: number;
121
+ status: RunStatus;
122
+ exitLabel: string;
123
+ timedOut?: string;
124
+ resumedFrom?: string;
125
+ /** Streamed reply text (always retained, even for cancelled runs). */
126
+ replyFile: string;
127
+ /** Live tool-activity log (async json-stream runs only). */
128
+ activityFile?: string;
129
+ sessionFile?: string;
130
+ usage?: Usage;
131
+ }
132
+ /** Running first (oldest spawn on top), then recently finished (newest first,
133
+ * capped) — the order the fleet inspector list shows. */
134
+ export declare function orderRunsForFleet(all: DelegateRun[]): DelegateRun[];
135
+ /** Display-safe snapshot of every delegate run known this session, for the
136
+ * fleet inspector (/acp-fleet). In-memory only: runs from previous pi
137
+ * processes are not listed (their files remain under OUT_DIR). */
138
+ export declare function fleetRunsSnapshot(): FleetRunView[];
64
139
  /** Minimal writable surface accepted by makeEventApplier — real WriteStreams
65
140
  * in production, in-memory collectors in tests. */
66
141
  export interface EventApplierWriters {
@@ -102,6 +177,7 @@ declare const DelegateParams: Type.TObject<{
102
177
  resumeFrom: Type.TOptional<Type.TString>;
103
178
  cwd: Type.TOptional<Type.TString>;
104
179
  model: Type.TOptional<Type.TString>;
180
+ thinkingLevel: Type.TOptional<Type.TString>;
105
181
  async: Type.TOptional<Type.TBoolean>;
106
182
  showThinking: Type.TOptional<Type.TBoolean>;
107
183
  }>;
@@ -164,6 +240,7 @@ export declare function formatBatchRunSection(run: DelegateRun): string;
164
240
  * in which case the caller delivers the full payload via formatRunResult(). */
165
241
  export declare function injectedWaitMessage(run: {
166
242
  injected?: boolean;
243
+ readSuppressed?: boolean;
167
244
  result?: {
168
245
  file: string;
169
246
  };
@@ -196,10 +273,30 @@ export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, c
196
273
  useJsonStream: boolean;
197
274
  sessionFile: string | null;
198
275
  }>;
276
+ /** Model-facing summary of the ACTIVE async watchdog limits — built from the
277
+ * resolved policy so it stays truthful when timeouts are customized or
278
+ * disabled via acp.json/env. */
279
+ export declare function asyncWatchdogDescription(): string;
199
280
  /** Watchdog/EOF finalize arrives with code === null (the child was killed or
200
281
  * never exited). If a result was delivered (non-empty reply or stderr), the
201
282
  * run counts as completed (0); otherwise it stays null = genuine failure. */
202
283
  export declare function effectiveExitCode(code: number | null, output: string, stderr: string): number | null;
284
+ /** Pure read-after-finish predicate: should the completion notification be
285
+ * suppressed because the model already read the final result file? Only a
286
+ * read at/after finishedAt counts — a read while the run was still in flight
287
+ * (readAt < finishedAt) saw partial output, so the notification still goes
288
+ * out. Callers additionally gate on run.status === "completed": a FAILED
289
+ * run's file holds only partial output with no failure marker, so the model
290
+ * cannot tell it failed from the file — failure notifications are never
291
+ * suppressed (failures are loud). */
292
+ export declare function shouldSuppressRead(run: {
293
+ readAt?: number;
294
+ finishedAt?: number;
295
+ }, mode: "skip" | "always"): boolean;
296
+ /** Suppress the completion notification for a run the model already read:
297
+ * mark it delivered (so wait/recovery/flush never re-surface the result),
298
+ * account its usage in separate mode, and log the skip. Idempotent. */
299
+ export declare function applyReadSuppression(run: DelegateRun, runId: string): void;
203
300
  /** status (set by finalize from the effective exit code) is the authority for
204
301
  * the FAILED/completed decision; the raw code is diagnostic display only
205
302
  * ("exit ?"), so the notification can never disagree with run.status. */
@@ -1,8 +1,10 @@
1
1
  import type { Readable } from "node:stream";
2
2
  export interface WatchdogOptions {
3
3
  eofGraceMs: number;
4
- idleMs: number;
5
- timeoutMs: number;
4
+ /** Kill when no output for this long. 0/null disables the idle timer. */
5
+ idleMs?: number | null;
6
+ /** Hard time limit. 0/null disables the limit timer. */
7
+ timeoutMs?: number | null;
6
8
  killGraceMs: number;
7
9
  }
8
10
  export interface WatchdogHooks {
@@ -0,0 +1,25 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type FleetRunView } from "./delegate-tool.js";
3
+ export interface InspectorTheme {
4
+ fg(color: string, text: string): string;
5
+ bold(text: string): string;
6
+ }
7
+ export declare function formatDuration(ms: number): string;
8
+ export declare function statusIcon(status: FleetRunView["status"]): string;
9
+ export declare function usageSummary(usage: FleetRunView["usage"]): string | undefined;
10
+ export interface ListRow {
11
+ run: FleetRunView;
12
+ label: string;
13
+ detail: string;
14
+ }
15
+ export declare function buildListRows(runs: FleetRunView[], now: number): ListRow[];
16
+ /** Last maxBytes of a file as utf8; a partial leading line is dropped so the
17
+ * tail always starts on a line boundary. Missing/unreadable → "". */
18
+ export declare function readTailSync(file: string | undefined, maxBytes: number): string;
19
+ export declare function renderListView(rows: ListRow[], selIdx: number, theme: InspectorTheme, width: number): string[];
20
+ export declare function renderTranscriptView(run: FleetRunView, tail: string, now: number, theme: InspectorTheme, width: number): string[];
21
+ /** Plain-text snapshot for non-TUI hosts (rpc/print/json). */
22
+ export declare function buildSnapshotText(runs: FleetRunView[], now: number): string;
23
+ /** Open the live fleet inspector: TUI overlay in interactive mode, plain-text
24
+ * snapshot notification elsewhere. Resolves when the user closes it. */
25
+ export declare function openFleetInspector(ctx: ExtensionContext): Promise<void>;