Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ jobs:
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm test
# build before test: tests/host-api.test.ts resolves "billion-context-pi"
# via Node self-reference to dist/index.js (package-name-only import contract)
- run: npm run build
- run: npm test

pr-validation:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## Unreleased (master, since v0.1.38)
- **fix(host): Pi-fork 宿主(Prime)可加载 + 可识别(#364 验收补充)** — Prime 0.9.4 实测两个启动阻断:① Prime loader 把 `@earendil-works/pi-coding-agent` alias 到自身构建,公共入口不导出 `CONFIG_DIR_NAME`,ESM interop 下缺失命名导出在运行时表现为 `undefined`(而非链接错误)→ `path.join()` 直接抛 "The path argument must be of type string"。新增 `src/config-dir.ts`:feature-detect 缺失/非法导出时回退 Pi 规范值 `.pi`,五个路径使用点(log/index/user-config/setup-subagent-tools/update)全部收敛到该常量;责任边界写入 docs/host-adapter.md §4(导出归宿主、回退归适配器)。② `isOmpHost = !isPiHost` 把一切无 `buildContextEntries()` 的宿主(含 Prime)一律判为 OMP 并禁用 ACP——但 OMP 与 Prime 同为 Pi fork、同为 getBranch-only 形状,形状本身无法区分。新 `src/host.ts`:`entrySourceOf`(buildContextEntries > getBranch)、`isDeclaredForkHost`(`PI_ACP_FORK_HOST=1/true`,调用时读取)、`isUnsupportedHost = !isPiHost && !declared`;OMP 默认保护不变(不声明即拒绝),声明的 fork 走既有 `!isPiHost` live-merge 补偿 getBranch 落后一条消息;delegate-tool 的 CLI flag 语义保持严格 isPiHost(fork 宿主不会用 pi-only flag 拉起子进程)。`OMP_UNSUPPORTED_MESSAGE` → `UNSUPPORTED_HOST_MESSAGE`,文案同时指向两条出路(fork opt-in / billion-context proxy);docs/omp.md(+zh)同步;CONFIGURATION(+zh)env 表增 `PI_ACP_FORK_HOST`。测试:`tests/host-detection.test.ts`(Prime-shaped fixture 矩阵)+ `tests/omp-refuse.test.ts` 增 opt-in 用例。③ 回合谓词同步收紧(验收项 c):空内容 `custom_message`(纯控制信号)不再开启新回合——与投影共用同一 `extractText` 判据,`isCustomMessageEntry` 因此移回 `src/messages.ts` 与投影同源(经 type-only import 保持无环),非空 agent_message 开启新回合、UI/control/synthetic 策略在 host-adapter §1 写明

- **fix(host): 包入口导出 `createRuntime` / `SessionRef` / `deriveChildState`,宿主派生契约可达(closes #367)** — #366 的 `docs/host-adapter.md` 要求内联子会话在首个 context 事件前调用 `runtime.deriveChildState(childRef, parentRef)`,但扩展的 runtime 实例是 `createAcpExtension` 工厂闭包内的私有变量:npm `exports` 只暴露 `.` → dist/index.js,`createRuntime` 仅从 src/runtime.ts 导出(dist 无对应 subpath)→ 以 `billion-context-pi` 为依赖的外部宿主(Prime 风格适配器)拿不到文档契约所需的 runtime,显式派生路径不可达,只能退回 parentSession 头隐式继承(连同 nudge 节奏逐字拷贝——正是 #366 要避免的行为)。修复:入口重新导出 `createRuntime` + 类型 `AcpRuntime`/`SessionRef` + 纯函数 `deriveChildState`(派生经 session ref 只触碰磁盘 sidecar,跨 runtime 实例天然成立);docs/host-adapter.md "API surfaces" 改写为从包入口 `import { createRuntime } from "billion-context-pi"`;CI test job 调整为 build 先于 test(新测试经 Node self-reference `import "billion-context-pi"` → dist/index.js,CI 上必须已构建);新增 `tests/host-api.test.ts` 仅从包名导入并端到端执行文档化调用(入口 typeof 检查、纯函数深拷贝/节奏重置、temp-dir sidecar 派生 + 一次性 marker + 重复派生拒绝,dist 缺失时 skip)。存量 pi 用户行为零变化
- **feat(host): 宿主多会话支持(二) — 回合边界判定统一 + 子会话状态继承(closes #364)** — #317 遗留的两个结构性缺口(记账隔离已由 #327 修复)。① 回合边界("什么消息算新回合起点")此前在三处独立判定且互不一致(tokens.ts 的 lastUserMessageId / index.ts 的 turnKey+turnStartIndex / messages.ts 的上下文条目投影):宿主以 custom_message 注入的 agent 回合进入 LLM 上下文但不算回合起点 → 多个真实回合塌缩进同一 turnKey(nudge 账本格子错位、重试上限与节流周期统计失真)。现收敛为单一谓词 `isTurnBoundary(entry, policy)` + 两个扫描助手(`src/turn-boundary.ts`),三处全部走它;新增 `hostSession` 配置(boolean 简写或 `{countCustomMessages}`,默认关闭 = pi 原生行为,存量单会话用户逐字节不变——单测以 legacy user-role-only 扫描为 oracle 断言等价)。② 内联同进程子会话(Prime RLM 等)的状态派生契约 `deriveChildState(parentState)`:继承 blocks(深拷贝)/messageRefs/tokenSnapshot(原始消息索引)/nextBlockId/nextRunId(保证继承块可 decompress/search、新块 id 不冲突),重置 nudge 节奏基线/stats/absorbed(子会话重新起算);一次性迁移标记 `derivedFrom:{parentSessionId,derivedAt}` 持久化进子 sidecar(沿用 `<childSid>.jsonl.acp.json`,与父文件独立),拒绝重复派生;护栏:子会话已有自有非派生块 / 父无块 / 子无 sessionFile 时拒绝且不改任何状态;显式派生优先于隐式 parentSession 头继承(恰好升级一次)。pi 原生 delegate(独立进程)路径零变化。文档:新增 `docs/host-adapter.md`(回界契约 + 子会话派生契约),CONFIGURATION.md(+zh-CN) 增 `hostSession` 节。测试:新增 `tests/turn-boundary.test.ts`(谓词矩阵 + id/index 双视图一致性 + 默认策略 ≡ legacy 扫描回归)与 `tests/derive-child-state.test.ts`(继承/重置矩阵、深拷贝隔离、marker 往返、各护栏拒绝、内联子代理 header 场景)
- **fix(degeneration): thinking/text 单字符退化熔断 + 一次性恢复通知(closes #351)** — 长会话末尾模型偶发退化为单字符长连击(实测:thinking 块末尾 4655 个连续「【」,跨轮升级直至 turn abort、会话停死)。根因链已代码级验证:pi 的 openai-completions 转换把历史 assistant thinking 在**每个后续请求**中回传 provider(`reasoning_content`,或 requiresThinkingAsText 时转纯文本),aborted turn 的部分消息又持久化在会话日志里 → 退化尾部随每轮 prompt 重放 → 模型看到自己上一轮以数千个重复字符结尾 → 续写偏置再次触发退化 → 连环 abort。新增 `src/degeneration.ts`:每个 context 事件对出站视图的 assistant text/thinking 块折叠 ≥`minRun`(默认 200,下限 8,codepoint/代理对安全)的单 codepoint 连击为短标记(保留 ≤3 份样本;纯函数、幂等——标记固定文案无相邻重复码点、fail-safe;持久化历史不改,toolCall 参数不动以免与实际执行脱钩);当最近一条 assistant 消息已退化时追加一次性 `[ACP recovery notice]`(位置自限:模型产出新 turn 后自动消失,无持久状态不累积,#223 教训)。检测走持久化 originals 而非出站视图:thinking-only aborted turn 会被 projectMessage 丢弃(空文本在 OpenAI 兼容 provider 400),但它仍是模型的"上一轮",通知必须照发。acp.json 新键 `degenerationGuard`(boolean 或 `{enabled,minRun}`,默认开;`false` 为 kill-switch)。附带修复:`repetitionGuard` 此前不在 user-config KNOWN 白名单内,acp.json 中配置被静默丢弃(dead key),本次补入。测试 `tests/degeneration.test.ts`(31 例:单元 + context transform 端到端 wiring)
- **fix(reasoning): 闭合判定改按回合证据——无用户消息的长 agent 会话不再永久保留 compress thinking(closes #348)** — 原门控“compress 调用之后存在真实用户消息才算闭合”在长 agent 会话不可达(整个会话只有开头 1–2 条用户消息,后续 30 个 compress 全部被永久视为活跃回合,观察会话 0 次触发,thinking 地板 20.6K/8.4K/10.6K 字符全部滞留)。现在闭合判定改为:消息内**每个** compress toolCall 的 toolResult(role `toolResult`、`toolCallId` 匹配)已出现在更晚位置,且其后至少还有一条消息(回合已实际推进)。安全门不变:结果未返回或结果仍是最后一条消息(在飞中)绝不动;nudge 在 drop 之后才注入,不可能光当“结果后的消息”闭合在飞回合;per-provider `compress.providers.<name>.reasoning.drop=false` 逃生阀保留(GLM 等 reasoning 回显模型)。测试重写 + 新增 #348 场景(无用户消息的助手链闭合、result 悬置、result 在 call 之前、多 toolCall 部分闭合、误 id 不闭合)
- **fix(overflow): output headroom 预留按窗口比例封顶,默认 25%(closes #207)** — `reserveOutputHeadroom` 原按模型注册表 maxTokens **全额**预留输出预算:maxTokens 占窗口比例大的模型(qwen3.8-27b:262144 窗口 / 131072 maxTokens)输入预算被砍半,kernel 75% 强制压缩带在完整窗口 ~37% 处触发(host pct 仅 ~34%,两个口径不同加剧误导)。现在预留量 = min(maxTokens, `outputHeadroomMaxPct` × window):新增 acp.json 配置键 `outputHeadroomMaxPct`(默认 0.25,接受比例或 `"N%"`;0 完全禁用预留,≥1 恢复旧的全额行为)。小预留不受影响(同窗口 int4 版 32K maxTokens 保持原样),超出预留的超长回复溢出一次后由既有 overflow self-heal(learned window + armed emergency)下一轮恢复。可观测性:`[turn]` 日志新增 `fullWindow` 字段(仅当 limit 被预留削减时出现,= 本轮 recenter 后的完整窗口),消除 pct(完整窗口口径)vs limit(预留后口径)混淆;`output-headroom` 事件日志新增 `cap` 字段;`/acp` 面板与 `acp_status` 分母经 `applyOutputHeadroom` 同步使用同一封顶值(#267 统一口径不回归)
Expand Down
34 changes: 34 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ All keys below are currently **ACTIVE**.
| `throttleRetry` | boolean \| object | `true` | 🟢 ACTIVE | Auto-retry provider token rate-limit errors with progressive backoff. |
| `repetitionGuard` | boolean \| object | `true` | 🟢 ACTIVE | Break infinite loops of byte-identical tool calls (warn at 3 consecutive, block + abort at 5). |
| `degenerationGuard` | boolean \| object | `true` | 🟢 ACTIVE | Collapse degenerate single-codepoint runs (e.g. 4655×「【」) in assistant text/thinking of the outgoing view and inject a one-shot recovery notice — breaks the abort loop where pi replays degenerated thinking back to the provider on every request (#351). |
| `hostSession` | boolean \| object | `false` | 🟢 ACTIVE | Turn-boundary policy for multi-session hosts: count injected `custom_message` entries as turn starts. Off by default (pi-native behavior). |

**Delegate keys**

Expand Down Expand Up @@ -164,6 +165,7 @@ All keys below are currently **ACTIVE**.
| `ACP_MODEL_CONTEXT_LIMIT` | Override the context limit (takes highest precedence). |
| `ACP_DEBUG` | Set to `1` / `true` to enable debug logging. |
| `ACP_LOG_FILE` | Override the log file path (default `~/.pi/acp.log`). |
| `PI_ACP_FORK_HOST` | Set to `1` / `true` to declare a Pi-compatible fork host (no `buildContextEntries()`) as supported. OMP stays refused by default. See [docs/host-adapter.md](./docs/host-adapter.md). |
| `PI_ACP_DELEGATE_MAX_DEPTH` | Override `delegate.maxDepth`. |
| `PI_ACP_DELEGATE_SYNC_TIMEOUT_MINUTES` | Override `delegate.syncTimeoutMinutes`; `0` disables the sync hard timeout. |
| `PI_ACP_DELEGATE_IDLE_TIMEOUT_MINUTES` | Override `delegate.idleTimeoutMinutes`; `0` disables the idle watchdog. |
Expand Down Expand Up @@ -485,6 +487,38 @@ Tool-call arguments are never rewritten (rewriting them would desync the model's

---

## Host Multi-Session

The `hostSession` key controls **turn-boundary detection** for hosts that run several sessions inside one process (e.g. Prime with inline RLM sub/sibling sessions). The full contract — including child-session state derivation (`deriveChildState`) — is documented in **[docs/host-adapter.md](./docs/host-adapter.md)**.

**Background.** ACP's per-turn ledgers (nudge-shown tracking, compress retry caps, outcome scoping) are keyed by the start of the current *turn*. Under Pi-native semantics a turn starts only at a genuine user-role message. Inline multi-session hosts additionally inject agent turns into the session log as `custom_message` entries; those are projected into LLM context (Pi-native semantics) but — under the default policy — start no turn, so several real host turns collapse into one turn key: nudge cadence cells misalign and retry-cap/throttle cycle statistics distort. Every turn-boundary decision in the adapter goes through the single predicate `isTurnBoundary(entry, policy)` (`src/turn-boundary.ts`).

### `hostSession`

- **Type:** boolean \| object
- **Default:** `false` (off)
- **Status:** 🟢 ACTIVE
- **Description:** Turn-boundary policy for host-injected messages. `hostSession: true` is shorthand for `{ "countCustomMessages": true }`. Object form (any subset):

```json
{
"hostSession": {
"countCustomMessages": true
}
}
```

**Default-off keeps existing single-session behavior byte-for-byte** — enable this only if your host actually injects agent turns into session logs.

### `hostSession.countCustomMessages`

- **Type:** boolean
- **Default:** `false`
- **Status:** 🟢 ACTIVE
- **Description:** Count host-injected `custom_message` entries with non-empty text (except UI-only `acp-status` panels) as turn boundaries for all per-turn ledgers; empty injections are pure control signals and start no turn. Does not change LLM-context projection — those entries were already projected as user-role messages under Pi-native semantics.

---

## Compression Tuning

The `compress` sub-object groups the three thresholds that form a **three-tier escalation** for context management. They control *when* the model is nudged to compress and *when* large outputs are forcibly truncated to keep the session alive. Lower thresholds mean the extension compresses earlier and more aggressively.
Expand Down
34 changes: 34 additions & 0 deletions CONFIGURATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
| `throttleRetry` | boolean \| object | `true` | 🟢 ACTIVE | 自动重试 provider 侧 token 限流错误(递进退避)。 |
| `repetitionGuard` | boolean \| object | `true` | 🟢 ACTIVE | 打断字节级完全相同的工具调用死循环(连续 3 次告警,连续 5 次拦截并中止本轮)。 |
| `degenerationGuard` | boolean \| object | `true` | 🟢 ACTIVE | 折叠出站视图中 assistant text/thinking 里的单字符退化连击(如 4655×「【」)并注入一次性恢复通知——打破 pi 每轮请求都回传退化 thinking 导致的连环 abort 死循环(#351)。 |
| `hostSession` | boolean \| object | `false` | 🟢 ACTIVE | 多会话宿主的回合边界策略:是否把注入的 `custom_message` 计为回合起点。默认关闭(pi 原生行为)。 |

**delegate 键**

Expand Down Expand Up @@ -163,6 +164,7 @@
| `ACP_MODEL_CONTEXT_LIMIT` | 覆盖上下文窗口大小(优先级最高)。 |
| `ACP_DEBUG` | 设为 `1` / `true` 开启调试日志。 |
| `ACP_LOG_FILE` | 覆盖日志文件路径(默认 `~/.pi/acp.log`)。 |
| `PI_ACP_FORK_HOST` | 设为 `1` / `true` 声明当前宿主是兼容 Pi 的 fork(无 `buildContextEntries()`),使其被识别为受支持宿主。OMP 默认仍被拒绝。见 [docs/host-adapter.md](./docs/host-adapter.md)。 |
| `PI_ACP_DELEGATE_MAX_DEPTH` | 覆盖 `delegate.maxDepth`。 |
| `PI_ACP_DELEGATE_SYNC_TIMEOUT_MINUTES` | 覆盖 `delegate.syncTimeoutMinutes`;`0` 禁用同步硬超时。 |
| `PI_ACP_DELEGATE_IDLE_TIMEOUT_MINUTES` | 覆盖 `delegate.idleTimeoutMinutes`;`0` 禁用闲置看门狗。 |
Expand Down Expand Up @@ -477,6 +479,38 @@

---

## 宿主多会话

`hostSession` 键控制**回合边界判定**,面向在单进程内运行多个会话的宿主(如 Prime 的内联 RLM 子/兄弟会话)。完整契约——包括子会话状态派生(`deriveChildState`)——见 **[docs/host-adapter.md](./docs/host-adapter.md)**。

**背景。** ACP 的按回合账本(nudge 已展示追踪、compress 重试上限、结果归口)以"当前回合起点"为键。pi 原生语义下,只有真正的 user-role 消息开启新回合。内联多会话宿主还会把 agent 回合以 `custom_message` 条目注入会话日志;这些条目会进入 LLM 上下文(pi 原生投影),但在默认策略下**不开启回合**——多个真实宿主回合塌缩进同一个 turnKey:nudge 节奏格子错位、按回合 compress 重试上限跨回合失真、节流/溢出周期统计失真。适配器中所有回合边界判定都走同一谓词 `isTurnBoundary(entry, policy)`(`src/turn-boundary.ts`)。

### `hostSession`

- **类型:** boolean \| object
- **默认值:** `false`(关闭)
- **状态:** 🟢 ACTIVE
- **说明:** 宿主注入消息的回合边界策略。`hostSession: true` 等价于 `{ "countCustomMessages": true }`。object 形式(任意子集):

```json
{
"hostSession": {
"countCustomMessages": true
}
}
```

**默认关闭保证存量单会话行为逐字节不变**——只有当你的宿主确实向会话日志注入 agent 回合时才启用。

### `hostSession.countCustomMessages`

- **类型:** boolean
- **默认值:** `false`
- **状态:** 🟢 ACTIVE
- **说明:** 把宿主注入的非空文本 `custom_message` 条目(UI-only 的 `acp-status` 面板除外)计为所有按回合账本的回合起点;空内容注入是纯控制信号,不开启回合。不改变 LLM 上下文投影——这些条目的 user-role 投影本就是 pi 原生行为。

---

## 压缩调优

`compress` 子对象包含三个阈值,构成上下文管理的**三级递进**。它们控制模型*何时*被 nudge 压缩,以及大输出*何时*被强制截断以维持会话存活。阈值越低,扩展压缩得越早、越激进。
Expand Down
Loading
Loading