Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Cancel compaction through the native turn

Status: implemented
Translation: current

[中文](2026-09-12-native-compaction-cancellation.zh.md)

## Abstract

Manual Codex `/compact` was treated as a command without a native turn, so Stop
could return an ACP cancellation while Codex kept compacting. The adapter now
captures the native turn and interrupts it through the ordinary cancellation
path, retaining ownership until terminal confirmation. Lody sends provider cancel
without interrupting an in-flight prompt's owner fiber, so normal cancellation
finalizes history only after ACP returns. A shared five-second drain terminates
an unresponsive provider; failed termination keeps ownership. PR #618 removes its
automatic historical reconciliation protocol and fixes these execution boundaries.
Existing stale histories are not migrated by opening a conversation.

## Decision

Codex 0.153.4 emits standard turn and item notifications for manual compaction.
`thread/compact/start` acknowledges submission with an empty result; the turn id
arrives through `turn/started`. The previous assumption that compaction had no
interruptible turn confused an empty acknowledgement with an absent lifecycle.

The app-server client registers an owner before submission and resolves only on
the matching `turn/completed`. Start failure and connection closure reject the
owner. The command routes the native id and terminal result through the same ACP
command lifecycle used by other native turns. Stop and request cancellation both
interrupt the captured turn. A cancellation before its id arrives stays pending
until the turn starts; the ACP prompt remains occupied while cancellation drains.

Lody records `runtime.cancelRequested` and sends provider cancel while the prompt
is in flight. It retains the existing owner fiber and runtime until ACP returns;
the scope then runs cancellation finalization and releases ownership. A new user
message remains pending and cannot reach ACP during that interval. Steer admission
also checks the existing cancellation flag after asynchronous preparation and after
the provider's acceptance ACK. A successful ACK after Stop cannot replace the source
invocation, force its settlement to handled, or transfer ownership. It marks that exact
steer user turn `canceled` without changing dispatch pointers, then returns `stale-turn`
and releases the application lease. The accepted steer cannot remain `pending_apply`
or be requeued; the existing owner continues to its cancelled terminal outcome. Neither the
daemon nor the client treats that disposition as permission to replay the steer.
Cancellation before prompt submission and finalization teardown retain their existing paths.

The earlier CLI code already retained the runtime through `pendingPromptCompletion`
inside its scope finalizer; it did not unconditionally release ownership at Stop.
It nevertheless interrupted the owner fiber and finalized history before draining
the provider. Normal Stop now waits in the prompt itself and starts the existing
five-second raw-request drain independently of the cancel acknowledgement. If the
prompt remains pending at the deadline, Lody terminates that session; connection
closure rejects the prompt and lets the owner enter its finalizer naturally.
Failed termination waits for raw ACP completion without releasing ownership.
Stop and external-interruption finalization share one drain promise on the runtime,
so repeated cancellation neither resets the deadline nor terminates twice.
This restores #571's recovery policy, which the earlier in-flight Stop change had
unintentionally restricted to external interruption.

After ACP completion, the existing CLI finalization from
[provider-failure settlement](2026-09-10-context-compaction-terminal-state.md)
then persists unresolved compaction as failed before releasing execution ownership.
Explicit provider terminal item updates remain authoritative.

The Session view does not initiate data repair. The branch's new reconciliation
capability, both transport methods, renderer retry hook, and daemon history repair
are removed. Historical unresolved records are a separate maintenance concern;
neither hiding progress nor rewriting history can interrupt native execution.

## Evidence and verification

- [Pinned native protocol](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/app-server/README.md#example-trigger-thread-compaction).
- Adapter owner: `packages/acp-extension-codex/src/CodexAppServerClient.ts`;
command and cancel routing: `CodexCommands.ts` and `CodexAcpServer.ts` in that directory.
- Deterministic tests cover Stop and request abort before/after native start,
rejection of a second prompt while draining, successful continuation after
interruption, failure, terminal events before the start ACK, start rejection,
unrelated turn completion, and process exit before/after native start.
- The Lody execution suite uses the real `AgentClient` with a controlled ACP
transport: cancel ACK leaves the prompt signal live and history unfinished,
a second dispatch stays pending, and native terminal evidence enables the next
prompt. Fake timers cover normal Stop's five-second termination, a withheld cancel
acknowledgement, repeated Stop without deadline reset, and failed termination
retaining unfinished history and ownership until ACP ends. External-interruption
coverage retains raw completion, process termination, and failed-termination cases.
- Steer coverage retains undelivered history and its dispatch pointer when Stop
precedes the request or arrives while prompt blocks are being built. A controlled
acceptance ACK arriving after Stop preserves the source invocation, user-turn and
dispatch owner, leaves source history unfinished, and never reports handled or
replays the steer. The accepted steer is `canceled` both before and after provider
terminal, which settles the source as cancelled and releases ownership. The terminal
steer assertion failed on `ae24a723` with the entry stranded in `pending_apply`.
- Contract: [Session history writes](../../../../specs/session-history-writes.md).
- Pull request: [Lody #618](https://github.com/LodyAI/Lody/pull/618).
- Adapter implementation: [Codex adapter #41](https://github.com/LodyAI/acp-extension-codex/pull/41).

Adapter typechecks and all 617 enabled adapter tests passed; 27 tests are skipped
by their existing environment gates. Real Codex smoke checks completed manual
compaction normally and confirmed that Stop produces native `interrupted` before
the ACP prompt returns `cancelled`.

Root typechecks, lint, formatting, i18n, documentation and boundary checks passed.
The targeted Lody execution, dispatch-watcher and AgentClient suites pass 207 tests.
The full `pnpm check` reached Electron tests: 103 passed, while the relay suite
could not load because this checkout lacks the installed Electron binary. All
preceding workspace test suites passed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 通过 native turn 取消压缩

Status: implemented
Translation: current

[English](2026-09-12-native-compaction-cancellation.md)

## 摘要

手动 Codex `/compact` 原本被当作没有 native turn 的命令,Stop 可以返回 ACP 取消,
但 Codex 仍在压缩。适配器现在捕获 native turn,通过常规取消路径中断它,并持有 ownership
直到 terminal 确认。Lody 发送 provider cancel,不中断 in-flight prompt 的 owner fiber,
因此正常取消在 ACP 返回后才完成历史收尾;共用的五秒 drain 会终止无响应的 provider,
终止失败则继续持有 owner。PR #618 删除自动历史 reconciliation 协议,改为修复这些执行边界;
打开会话不会迁移已有的陈旧历史。

## 决策

Codex 0.153.4 的手动压缩会发出标准 turn 和 item 通知。`thread/compact/start` 用空结果确认
提交,turn id 随 `turn/started` 到达。此前认为压缩没有可中断 turn,是把空 ACK 与不存在
生命周期混为一谈。

app-server client 在提交前注册 owner,只在匹配的 `turn/completed` 到来时 resolve。
启动失败和连接关闭会 reject owner。命令通过普通 native turn 的 ACP 命令生命周期传递
native id 和 terminal 结果。Stop 与请求取消均中断捕获的 turn;若取消早于 id 到达,
则等待 turn 启动后中断。在取消 drain 期间,ACP prompt 一直被占用。

Lody 在 prompt in-flight 时记录 `runtime.cancelRequested` 并发送 provider cancel,
保留原有 owner fiber 和 runtime,直到 ACP 返回,再由 scope 执行取消收尾并释放 ownership。
期间新用户消息保持 pending,不能到达 ACP。steer 在异步准备后及 provider 接受 ACK 后都
检查取消标记。Stop 后到达的成功 ACK 不得替换 source invocation、将 source 强制结算为
handled 或转移 ownership。它将该 exact steer 用户轮次标为 `canceled`,不改变 dispatch pointer,
然后返回 `stale-turn` 并释放 application lease。已接受的 steer 不能停留在 `pending_apply`,
也不能重新排队;现有 owner 继续等待 cancelled terminal。daemon 与客户端均不会把这个
disposition 当作重放 steer 的许可。提交 prompt 前的取消及 finalization teardown 保留原路径。

此前 CLI 的 scope finalizer 已通过 `pendingPromptCompletion` 保留 runtime,并非在 Stop 时
无条件释放 ownership;但它会先中断 owner fiber、完成历史收尾,然后才 drain provider。
正常 Stop 现在等待 prompt 本身,同时启动原有五秒 raw-request drain,不等待 cancel ACK。
到期时 prompt 若仍 pending,则终止该 session;连接关闭 reject prompt,让 owner 自然进入
finalizer。终止失败时继续等待 raw ACP 完成,不释放 ownership。Stop 与外部中断后的
finalizer 共用 runtime 上的一个 drain promise,重复取消不会重置期限或重复终止。
这恢复了 #571 的恢复策略;此前保留 in-flight owner 的修改误将该策略限制在外部中断路径。

ACP 完成后,既有的 [provider 失败收尾](2026-09-10-context-compaction-terminal-state.md)
会在释放执行 ownership 前,将尚未结束的压缩持久化为 failed。provider 明确发出的 terminal
item 更新仍是权威结果。

Session 视图不主动修复数据。本分支新增的 reconciliation capability、两种传输方法、
renderer retry hook 和 daemon 历史修复均已移除。历史上的未完成记录是独立维护问题;
隐藏进度或改写历史都不能中断 native 执行。

## 证据与验证

- [固定版本的 native 协议](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/app-server/README.md#example-trigger-thread-compaction)。
- 适配器 owner 位于 `packages/acp-extension-codex/src/CodexAppServerClient.ts`;
同目录的 `CodexCommands.ts` 和 `CodexAcpServer.ts` 负责命令及取消路由。
- 确定性测试覆盖 native start 前后的 Stop/abort、drain 期间拒绝第二个 prompt、
中断后继续执行、失败、start ACK 前的 terminal、启动拒绝、无关 turn 完成及进程退出。
- Lody execution 测试使用真实 `AgentClient` 与受控 ACP transport:cancel ACK 后 signal
仍存活、历史未完成、第二次 dispatch 保持 pending,native terminal 后下一次 prompt 才能运行。
fake timer 覆盖五秒终止、cancel ACK 不返回、重复 Stop 不重置期限,以及终止失败后继续
保留未完成历史和 ownership。外部中断测试仍覆盖 raw 完成、进程终止及终止失败。
- steer 测试覆盖请求前和异步准备期间的 Stop,验证未投递历史及其 dispatch pointer 保留。
受控的接受 ACK 在 Stop 后到达时,source invocation、用户轮次和 dispatch owner 均不改变,
source 历史不提前完成、不报告 handled,也不重放 steer。已接受的 steer 在 provider terminal
前后均为 `canceled`;terminal 将 source 结算为 cancelled 并释放 owner。steer 终态断言在
`ae24a723` 上失败,当时条目会停留在 `pending_apply`。
- 契约:[会话历史写入](../../../../specs/session-history-writes.zh.md)。
- PR:[Lody #618](https://github.com/LodyAI/Lody/pull/618)。
- 适配器实现:[Codex adapter #41](https://github.com/LodyAI/acp-extension-codex/pull/41)。

适配器类型检查及 617 个启用的测试通过;27 个测试由原有环境条件跳过。真实 Codex smoke
验证手动压缩正常完成,并确认 Stop 后 native `interrupted` 先于 ACP prompt 的 `cancelled`。

根仓库类型、lint、格式、i18n、文档和边界检查通过。Lody execution、dispatch-watcher 和
AgentClient 三组相关测试共 207 个通过。完整 `pnpm check` 到达 Electron 测试时,103 个通过,
relay suite 因本地缺少已安装的 Electron 二进制无法加载;此前的 workspace 测试均通过。
23 changes: 12 additions & 11 deletions apps/cli/src/session/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Contract: specs/session-orchestration.md.
- Never re-dispatch a late-arriving history entry; recovery is a fresh send.
- `hasPendingUserTurnActivation` is the ONLY pending-turn predicate; never compare those two
pointers in a consumer.
- Session metadata is the activation index: never inspect historical Session documents to infer
work, and never publish or clear active presence here (`../lib/loro/session-active-presence.ts`).
- Never inspect historical Session documents to infer work, or publish or clear active presence
here (`../lib/loro/session-active-presence.ts`).
- Keep bootstrap and live reconciliation bounded as README describes; add no per-trigger scan or
extra throttle.

Expand All @@ -47,26 +47,27 @@ Contract: specs/session-orchestration.md.
- Gate turn-scoped history LIST writes on user-entry sync (`turn-history-gate.ts`, 20s);
never gate status or meta writes.
- Goals obey [this contract](../../../../specs/session-goal-control.md).
- Keep `TurnRuntimeState` until raw ACP completion or confirmed termination after cancel; no
second visible turn. Assistant ids use `userTurnId`. `invocation` atomically
owns source Turn, requester and config; steer replaces it before tools.
- In-flight Stop cancels ACP, never its owner fiber. Keep `TurnRuntimeState` until raw ACP
completion or confirmed termination; no second turn. Assistant ids use `userTurnId`.
`invocation` owns source Turn, requester and config atomically; steer replaces it before tools.
- Publish `latestUserMsgId` in the SAME write as the history append (`appendUserTurn`). Only
dispatch producers publish it. Renderer sends and queue promotion retain the missing-history
tombstone; CLI dispatch producers keep their own marker policy.
- Ordinary turn execution writes only `processingUserMsgId` and `lastHandledUserMsgId`; no start
or terminal path may read-await-rewrite the other slots.
- INVARIANT: a steer the agent never accepted must not stay parked in `pending_apply`. Requeue it
through the pointer, not the entry status, only for pre-submission rejections or
`AgentSteerNotDeliveredError`; skip active or already-handled entries.
- Never submit steer after Stop. A late accepted ACK cancels that exact steer entry without
transferring ownership, changing dispatch pointers or requeueing it.
Requeue unaccepted steer via its pointer, not entry status, only before submission or on
`AgentSteerNotDeliveredError`; skip active or handled entries.
- Resume must REOPEN the in-progress assistant entry, clearing
`finished`/`endedAt`/`permissionWaitMs` there only; never write `finished=false` from teardown.
- Keep JSON-RPC/transport matching in `acp-error-classification.ts`: disposed/stale `-32603` is
`agent_disconnected`, Harness compression mismatch is `acp_session_storage_incompatible`.
- Continue-session recovery may restore the ACP session and retry the same prompt once, only
while that turn has no ACP output.
- INVARIANT: a resolved prompt is not proof of success. A turn that emitted no ACP update takes
`recordSilentTurnFailure`, not `setDispatchHandled` (read `turnProducedVisibleOutput` before
`finalizeTurn` clears it); it still finalizes, still ADVANCES the pointer, and fails open.
- A turn with no ACP updates takes `recordSilentTurnFailure`, not `setDispatchHandled`.
Read `turnProducedVisibleOutput` before `finalizeTurn` clears it; still finalize, advance
the pointer, and fail open.
- Diff content comes only from the CLI-local ACP evidence store; GitHub `diffStats` use PR compare
semantics, and `session-diff-stats-target.ts` skips rather than overwrites a good total.

Expand Down
Loading
Loading