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
29 changes: 29 additions & 0 deletions .github/workflows/compiled-process-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Compiled POSIX process smoke

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions: {}

jobs:
compiled-process-smoke:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.11
- run: npm ci
- name: Compile and exercise native override without Node on child PATH
timeout-minutes: 3
run: npm run test:compiled-process
43 changes: 43 additions & 0 deletions .github/workflows/native-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Native loopback E2E

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions: {}

jobs:
native-e2e:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install locked dependencies (including native optional package)
run: npm ci
- name: Fresh build and real native stdio tests with loopback model
timeout-minutes: 5
run: npm run test:native
- name: Preserve build hashes, RPC, model, spawn and failure evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: native-e2e-${{ matrix.os }}
path: tmp/native-e2e/
include-hidden-files: true
if-no-files-found: warn
retention-days: 7
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol]
- Typed warnings and errors through the opt-in [AIR session failure extension](docs/air-extensions.md#session-failure).
- All AIR extensions, capabilities, and `_meta` keys: [AIR extensions](docs/air-extensions.md).
- A per-turn [agent file-change report](docs/air-extensions.md#agent-file-change-report) after capability negotiation.
- In-place message editing through the AIR [session rewind extension](docs/session-rewind-extension.md), without a provider fork.
- Client-provided MCP servers over command-based stdio config and HTTP transport.
- Slash commands: `/status`, `/mcp` (with `/mcp reconnect`), `/skills`, `/goal`, `/review`, `/review-branch`, `/review-commit`, `/compact`, and `/logout`, as well as configured skills.

Expand Down Expand Up @@ -107,3 +108,5 @@ See [AIR extensions](docs/air-extensions.md#async-tasks) for the capability, lif
## License

By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.

Local native-session test extensions: [English](docs/native-session-extensions.md) | [简体中文](docs/native-session-extensions.zh-CN.md).
41 changes: 41 additions & 0 deletions docs/native-session-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Local native session extensions

This local test build integrates PR #508 on main `51aa211` (adapter 2.1.1, locked Codex 0.160.1, ACP SDK 1.5.0). It preserves current-main hooks, attachments, tool-call reports and compaction handling. No Codeg changes, global installation, push or new PR are included.

## Negotiated operations

Read the `initialize` metadata before invoking any extension. These are named methods with strict schemas, not a generic native RPC tunnel.

| Capability | Method | Contract |
|---|---|---|
| AIR `sessionRewind` | `_session/rewind` | Same ID, exclusive `beforeMessage`, optional `resumeAtMessage`; `{rewound:true}` only after persisted-prefix verification. |
| `_meta.steering.idleBehavior` | `_session/steering` | Opt in with `_meta.steering.idleBehavior:"promptRequired"`; idle returns `{outcome:"promptRequired"}` without starting a model request. Non-opted clients retain the existing idle new-turn behavior. |
| `_meta.runtime` | `_session/runtime/read` | `resource`: context, usage, mcp, commands, plugins. Unknown measurements stay null. Commands are native skills; this is not a list of every Desktop UI command. |
| `_meta.runtime` | `_session/runtime/control` | `action`: reloadSkills, reconnectMcp, reloadPlugins. Skill reload republishes available commands. MCP/plugins are provider-wide idle operations; their acknowledgement is not runtime readiness. |
| `_meta.archive` | `_session/archive`, `_session/unarchive` | `{sessionId}`; reversible native archive, same ID. Existing standard session/delete keeps its prior archive-backed behavior. No permanent delete. Archive closes local routing; unarchive requires load to reopen. |
| `_meta.discovery` | `_session/search` | Bounded searchTerm/cursor/limit/archived. Searches user sources appServer/cli/vscode. Current native search can miss text retained only through ancestor rollout lineage after rewind. |
| `_meta.discovery` | `_session/attachments` | list/add/remove native metadata only. Does not create/delete a file, worktree, PR or prompt attachment. Loaded-session ownership required. |
| `_meta.queue` | `_session/queue` | Only list is advertised after actual-native version and positive nonmutating method probe. Writes/start return unsupported before native mutation. Pending native queues prevent ACP load/resume to avoid unowned auto-dispatch. |
| `_meta.fileRevert` | `_session/files/revert` | `{sessionId,toolCallId,dryRun,previewToken?}`; reverse one completed native fileChange tool's recorded text patches in Git. Preview token required for apply. History stays unchanged. |

## File reversal

Preview and commit re-read native history and fingerprint affected files. A stale preview, conflict, unknown change kind, binary/unsupported patch or unsafe path fails closed for the entire batch. Git uses reverse check/apply without --index/--cached/--3way, preserving the user's index and unrelated bytes. Reverted text follows Git attributes/autocrlf and is not promised to be byte-identical to its original encoding/newlines. Shell changes, unrecorded subagents and whole-workspace checkpoints are outside coverage.

The server reserves the provider while validating loaded native threads and background terminals. External editors and unrelated processes are not controlled by this fence; Git conflict checks and fresh fingerprints narrow but do not eliminate an OS-level race.

## Timeouts and recovery

POSIX supervision uses the current runtime and adds no external Node dependency. Compiled Bun executables re-enter fixed built-in supervisor/guardian modes before importing the ACP application or reading configuration/log settings; these modes require private pipe descriptors and accept no executable source. Node and direct Bun runs execute the same built-in helper implementation. The two small helpers preserve the provider's exit status and perform cleanup from within the owned group; they do not interpret ACP traffic. stdin EOF and stdout/stderr use inherited descriptors, common termination signals are forwarded, and the extra control pipe is unreferenced in the adapter. A POSIX provider spawn error is reported on stderr with supervisor exit code 1. Orphaned processes must be reaped by the host init (use --init in containers); unreaped groups can time out rather than be reported stopped. Compiled native CODEX_PATH launch is tested with Node absent from PATH; this does not establish compatibility of the separate bundled JavaScript launcher path in compiled distributions.

Runtime mutations have a 30-second deadline. Timeout invalidates local sessions and attempts to terminate the owned provider; reconnect the ACP adapter and explicitly load a session. No mutation is replayed. POSIX launches the provider (including the default Node launcher and native child) in a dedicated process group with a guardian. A private control pipe requests SIGKILL from inside that group; launcher/supervisor exit or adapter pipe closure also triggers cleanup. The adapter never sends a destructive signal to a remembered POSIX PID or PGID. Termination succeeds only after the supervisor exits and the group disappears; an unverifiable group or a five-second deadline is a failure, not a stopped-provider result. Processes that deliberately leave the group are outside this guarantee. Windows retains taskkill /T /F for a live owned launcher; if the launcher has already exited, tree ownership can no longer be verified and termination fails without targeting its old PID. Repeated termination calls share the same result, including failures. Read timeouts return unavailable. A history mutation with an uncertain outcome blocks further sends until successful load/reconciliation.

## Desktop parity boundaries

Supported and locally exercised: same-ID first/latest/historical/repeated rewind, cancel-and-settle, cold load, resend context truncation, runtime status/control, archive/unarchive, attachment metadata persistence, safe idle steering, native search of current rollout, queue inspection and per-tool text patch reversal.

Not claimed: full native queue automatic-turn adoption, realtime audio/voice, timeline API, complete Desktop project/worktree/UI management, whole-session file checkpoint restore, binary reverse patches, all child process/background writer isolation, or complete search over retained ancestor rollouts. A native schema method does not establish an implemented ACP handler.

## Validation

Full suite: 82 test files passed, 8 skipped; 1342 tests passed, 39 skipped on Windows. Typecheck/build passed. Additional file/lifecycle regression: 28 passed. Isolated ACP plus native plus localhost mock model covered 0.160.1 and CODEX_PATH 0.159.3; no real model or real user session was used. Independent focused review passed 6/6 reproductions, including real owned-child termination. The native file RPC E2E passed 9 checks (actual apply_patch, persisted replay, preview, stale/conflict refusal, index/unrelated-byte preservation and same-session continuation). These are scoped findings, not proof of every interleaving.
33 changes: 33 additions & 0 deletions docs/native-session-extensions.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 本地原生会话扩展

本测试版在 main `51aa211` 上整合 PR #508,adapter 2.1.1,冻结 Codex 0.160.1、SDK 1.5.0。保留主线 hooks、附件回放、工具调用协议与压缩处理。没有改 Codeg、全局安装、push 或新 PR。

## 已接入

- `_session/rewind`:保留原接口和作者提交;同 ID 回退首条、历史、最近消息。验证指纹/保留边界,生成中先取消并等待旧 prompt 清理,再调用原生回退并核验持久前缀。
- `_session/steering`:opt-in `_meta.steering.idleBehavior:"promptRequired"`,空闲返回 promptRequired,不偷偷启动模型。未 opt-in 保留旧行为。
- runtime read:context、usage、mcp、commands(native skills)、plugins。未知用量保留 null,不捏造分类。
- runtime control:reloadSkills、reconnectMcp、reloadPlugins;后两者要求 provider 空闲,确认回执不等于 runtimeReady。技能刷新后重发可用命令。
- archive/unarchive:原生可逆归档、同 ID;不新增永久删除。旧 session/delete 仍保持原来的 archive 语义。
- search:限定参数和 appServer/cli/vscode 来源;已验证当前 rollout synthetic 文本命中。回退后仅保留在祖先 rollout 的文本可能被原生搜索漏掉。
- attachments:原生 thread 元数据 list/add/remove、冷重启持久化;不创建/删除磁盘文件、PR 或 worktree。
- queue:只在真实 native 版本和无副作用探测通过后广告 list。写入/start 在 native mutation 前 unsupported;已有 durable pending queue 阻止 ACP load/resume,避免无人归属的自动生成。
- files/revert:限定单个已完成 native fileChange 工具的 Git 文本补丁。preview token 绑定原生补丁与文件指纹,提交前复核;不改会话历史。

## 文件语义与恢复

文件撤销不等于 whole checkpoint:不覆盖 shell、未记录子代理、二进制补丁。Git reverse check/apply 不用 --index/--cached/--3way;用户 index 和无关文件严格保持。恢复后的文本走 Git 属性/autocrlf,不保证原始字节或换行完全一致。未知 change kind、缺失 diff、冲突、陈旧 token、越界/链接路径整批拒绝。

runtime mutation 30 秒超时后使会话失效并尝试终止 provider,用户重连 ACP 再显式 load;不会自动重放 mutation。POSIX 将 provider(包括默认 Node launcher 及 native 子进程)置于独立进程组,通过私有控制管道请求组内 guardian 执行 SIGKILL;launcher/supervisor 退出或适配器管道关闭也触发清理。适配器不会向保存的 POSIX PID/PGID 发破坏性信号。只有 supervisor 已退出且组已消失才报告终止成功;无法确认或五秒超时均报失败。主动脱离该组的进程不在保证内。Windows 对仍存活的 owned launcher 使用 taskkill /T /F;launcher 已退出时无法再确认整树归属,明确报错且不针对旧 PID 清树。重复关闭复用同一结果,包括失败。读取超时返回 unavailable。历史回退 dispatch 后失败可能已改变磁盘,必须成功 load 对账再继续发送。

POSIX 监督复用当前运行时,不新增外部 Node 依赖。Bun 编译产物通过固定内置 supervisor/guardian 模式重入,在加载 ACP 模块或读取配置/日志设置前分发;模式要求私有管道描述符,不接收可执行源码。Node 和直接 Bun 运行使用同一份内置 helper 实现。两个小型 helper 分别负责保留 provider 退出状态和从 owned 组内部清理,不解释 ACP 消息;stdin EOF 和 stdout/stderr 使用继承的文件描述符,常规终止信号转发给 provider,额外控制管道在适配器中 unref。POSIX provider 启动失败通过 stderr 和 supervisor 退出码 1 报告。宿主 init 必须回收孤儿进程(容器使用 --init);未回收的组可能超时报错,不虚报已停止。compiled native CODEX_PATH 路径已在 PATH 不含 Node 的条件下验证;这不代表另一个 bundled JavaScript launcher 路径在 compiled 发行物中的兼容性已得到验证。

## 尚未实现的 Desktop 能力

完整队列自动回合 observer/adoption、realtime voice、timeline、Desktop 项目/worktree/UI 全部管理、全工作区文件 checkpoint、二进制反向恢复、外部写入进程完全隔离、祖先 rollout 全文搜索均不宣称完成。native schema 存在某方法不等于本 ACP 已支持。

## 验证

Windows 完整套件 82 文件通过/8 跳过,1342 项通过/39 跳过;typecheck/build 通过;文件/lifecycle 增量 28 项通过。0.160.1 与 CODEX_PATH 0.159.3 的真实 ACP+native+localhost mock 模型隔离实验通过,未调用真实模型或用户会话。独立定向 review 六项复现全通过(含真实 owned-child 终止);独立文件 RPC E2E 九项通过,覆盖真实 apply_patch、持久回放、预览、陈旧/冲突拒绝、index/无关文件字节保持、同会话继续;仍不代表所有交错均已验证。

协议细节见 [English](native-session-extensions.md);原 rewind 作者 Nikita Ashikhmin,来源 [PR #508](https://github.com/agentclientprotocol/codex-acp/pull/508),四条原提交保留在历史中。
67 changes: 67 additions & 0 deletions docs/session-rewind-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Session rewind extension

Standard ACP can fork a session, but it cannot remove a transcript suffix from the same provider session. The experimental AIR session rewind extension adds that operation without creating another session.

## Capability negotiation

The adapter advertises `sessionRewind` in its `initialize` response:

```json
{
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"capabilities": ["sessionRewind"]
}
}
}
}
```

A client must send `_session/rewind` only when the adapter advertises this capability. The leading underscore identifies a method outside standard ACP.

## Request and response

The request names the current ACP session and the first user message to remove:

```json
{
"sessionId": "thread-1",
"beforeMessage": {
"messageId": "user-message-2",
"messageFingerprint": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"messageOccurrence": 1
},
"resumeAtMessage": {
"messageId": "assistant-message-1",
"messageFingerprint": "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"messageOccurrence": 1
}
}
```

`beforeMessage` is excluded from the retained history. `resumeAtMessage` identifies the last visible assistant message to retain. It is absent when the client rewinds the first user turn.

Each history point contains the ACP message ID, the SHA-256 fingerprint of its complete text, and the one-based occurrence of that fingerprint for its role. The adapter uses the message ID first and verifies the selected message fingerprint. It uses the fingerprint occurrence when restored provider history has different message IDs. Repeated fingerprint fallback requires a current `resumeAtMessage` boundary; fallback is refused when visible text omits attachment identity. A supplied retained boundary must identify the last retained assistant message. Reload rather than guessing when a point is stale.

The adapter returns `{ "rewound": true }` only after Codex accepts the rewind and a native history read verifies the same thread ID and retained prefix. Errors after mutation dispatch can have an unknown outcome: the client must load the session to reconcile before retrying or sending. The adapter blocks further prompt/rewind requests until that load succeeds. It never retries a mutation automatically.

## Codex mapping

The adapter reads the existing Codex thread history and resolves `beforeMessage` to its containing turn. Rewind is rejected when the selected message is a steer inside an existing turn because Codex cannot remove only that suffix. For paginated history, the adapter calls `thread/revert` with the containing turn as the exclusive boundary; legacy history uses the equivalent turn-count rollback operation.

The Codex thread ID remains the ACP session ID. The adapter does not call `thread/fork`, create a thread, or add a session-list entry. `resumeAtMessage` validates the retained boundary and disambiguates repeated fingerprint fallback.

After a successful response, the client can remove the same transcript suffix and place the selected user text in its editor.


## Lifecycle and compatibility

An active prompt is cancelled only after the target validates; the adapter waits for prompt cleanup before native mutation. A pending turn/start registration is rejected, because an unacknowledged native start could write after the rewind. Competing lifecycle/settings/steering/runtime operations are fenced. Provider replacement waits for an existing rewind.

`thread/rollback` is used only when native history explicitly reports `legacy`. It is a separately typed compatibility request for old `CODEX_PATH` binaries, not part of current generated API types. Unknown history mode, arbitrary errors, malformed responses and `thread/revert` failures never trigger rollback.

History rewind does not restore files. See [native session extensions](native-session-extensions.md) for the separate, previewed single-tool Git patch reversal.

The interface and original implementation come from [Nikita Ashikhmin's PR #508](https://github.com/agentclientprotocol/codex-acp/pull/508), preserved as four commits in this branch. This local integration merges current main and adds validation/lifecycle safeguards; it is not a new incompatible rewind protocol.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"release:preflight": "bash scripts/release-preflight.sh",
"test": "vitest run --no-file-parallelism --retry=2",
"test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run --no-file-parallelism --retry=2 src/__tests__/CodexACPAgent/e2e",
"test:native": "node scripts/native-e2e.mjs",
"test:compiled-process": "node scripts/CodexProcessTermination-smoke.mjs",
"test:watch": "vitest",
"typecheck": "tsc --noEmit && tsc --noEmit -p examples/tsconfig.json",
"codex-test": "tsx .claude/skills/run-codex/scripts/run-codex-test.ts"
Expand Down
Loading