Repository navigation
feat: add native manual compaction controls - #610
Merged
Merged
Conversation
stamp-build
Bot
force-pushed
the
agent/otter/manual-compaction
branch
from
November 2, 2026 21:48
3d5f0a0 to
2183dd3
Compare
Merged
stamp-build Bot
added a commit
that referenced
this pull request
Nov 2, 2026
<!-- stamp:review:v1 --> <sup><img src="https://api.raft.build/api/avatars/pixel/bXVn.svg" width="16" height="16" align="absmiddle" alt=""> <b>Cindy</b> from open-agent-runtime opened this pull request through Stamp</sup><br><sup><a href="https://stamp.build/review?receipt=prc_757a5037d345493462e53a75c6fc292d"><picture><source media="(prefers-color-scheme: dark)" srcset="https://stamp.build/assets/brand/stamp-glyph-dark.d907ffcadb44.svg"><img src="https://stamp.build/assets/brand/stamp-glyph-light.58a2c10ed108.svg" width="13" height="12" align="absmiddle" alt=""></picture> View in Stamp →</a></sup> --- <!-- /stamp:review:v1 --> Minor release: a host can now ask a runtime to compact its context and wait until the runtime confirms it. This completes the 0.85.0 resume change: a prompt supplied on resume that only takes effect at the next native compaction (Claude's `systemPrompt` and `appendSystemPrompt`, Codex's `appendSystemPrompt`) can now be applied right away in the same session by resuming with the new prompt and then calling `compact()`. Codex's replacement `systemPrompt` already applies immediately. Stream-json turn ends also carry the native input identities. ## New: manual compaction (#610) - Optional `Runtime.manualCompaction` says before opening whether a runtime supports it: `{ kind: "supported", activeTurn }` or `{ kind: "unsupported", reason }`. An absent declaration means untested, not unsupported. Supported now: Claude, Codex and Pi (in-process and subprocess). Declared unsupported with reasons: Kimi, Qwen, Antigravity, CodeBuddy. Not yet declared: Grok, Qoder, Cursor, OpenCode, Pi Durable and the community Morph runtime. - Optional `Session.compact({ timeoutMs?, signal? })` returns a `CompactionOutcome` (new exported types `ManualCompaction`, `CompactionOptions`, `CompactionOutcome`): - `completed`: the runtime confirmed this compaction (Claude's `compact_boundary`, Codex's compaction item, Pi's manual compaction end); `frame` points at that record. - `rejected`: the compaction was not admitted, with a typed `code` and `reason`. Usually nothing reached the runtime, but Codex and Pi can also refuse after the native call (`runtime_refused`, `runtime_exited`), and Claude's command check talks to Claude first. Codes include `busy` (a turn, queued or not yet started input, another compaction, or an active Codex goal), `aborted` (the caller's `signal` was already aborted, so nothing was sent), `unsupported` (for example a project or user command shadows Claude's `/compact`), and the usual unreachable and error codes. - `failed`: the runtime reported failure, or the command did not actually compact (Claude reported failure, or its commands changed in between). - `aborted`: the runtime itself reported the compaction cancelled. - `unconfirmed`: OAR stopped observing without a confirmed result: `timeout`, `signal`, `disposed`, `runtime_exited`, `session_released` or `observation_failed`. - `compact()` throws `RangeError` when `timeoutMs` is negative or not finite. - **All current implementations require an idle session.** Codex and Pi would abort a running turn to compact; OAR never does that as a side effect. - **Waiting limits only stop waiting.** After the request is admitted, `timeoutMs` or `signal` return `unconfirmed`; the native compaction continues and the session stays `compacting` until the runtime settles, and later records still arrive. During that time new prompts, steer and a second `compact()` are refused as `busy`; input queued during the compaction is held by Claude and Pi until it ends, while Codex refuses it as `busy`. `timeoutMs: 0` sends the request without waiting. - `abort()` during compaction uses the runtime's own cancel once there is something to cancel; before that it is refused as `busy`. A queued Claude `/compact` cancelled before it starts ends `aborted`. A Claude compaction already running and then interrupted is reported by Claude as a failure, so `compact()` returns `failed` with that native record, independent of the turn's own aborted ending. Codex refuses `abort()` as `busy` until its compaction item names the turn. - Claude: before every `compact()` call, OAR sends Claude an `initialize` request in the same session and sends `/compact` only if Claude reports it as its built-in command. On Claude 2.1.316 (Linux) repeated `initialize` requests did not re-run the SessionStart hook or the HTTP MCP server and left the permission mode, settings, system prompt and tool list unchanged; this is not a claim about every version or every hook implementation. - Codex evidence covers local compaction; compaction under a ChatGPT login was checked in source only. Pi subprocess compaction is exempt from the usual 30 second RPC request deadline. - Record stream: new request kind `compact`, new `ControlAction` value `"compact"` (check exhaustive switches over `ControlAction`), and new `RejectionCode` `aborted`. ## Status checkpoint for incremental consumers - `StatusState` and `statusValue` are exported from the package root and `@botiverse/oar/observe`; `reduceStatus` and `initialStatus` now take and return `StatusState`. Hosts that fold status incrementally must persist the complete `StatusState` together with their cursor and pass it back unchanged. The displayed `AgentStatus` (`statusValue`, `Session.status()`) alone cannot continue a fold through a manual compaction. - The same applies to the observe helpers: `TaskState.status` is now a `StatusState`, and `SessionView` gains `statusState?`. Persist these whole as well. - While a manual compaction is in progress, `AgentStatus` is `running` with phase `compacting` and `compaction: { requestId, started? }`. ## Turn input identities - `turn_ended.inputId?` and `turn_ended.inputIds?` copy the stream-json result's `user_message_uuid` and ordered `user_message_uuids` when the runtime reports them, including on ordinary turns. Neither is derived from the other. When Claude merges several queued inputs into one turn, its `inputId` names the latter input and there is no first or last rule to rely on, so use `inputIds` to settle all of them. OAR's own interrupt fallback is separate and still uses `mergedInputIds` and `carries`. Claude reports these fields; Qwen 0.26.5 and Qoder 1.1.81 currently do not. - Stored OAR records do not change. Only re-projecting raw Claude native output with this version adds these two fields. ## Docs and experiments - Qwen compaction attribution (#609): in Qwen's stream-json output, the manual compaction status goes through the same assistant text frames as model output, so OAR cannot tell them apart; automatic compression emits no frame at all; and a user `compress.toml` can replace `/compress`. OAR emits no Qwen compaction events and does not guess from text. - DeepSeek Harness plugin bridge probe (#607): an external plugin shipped outside DSH can drive DSH's native controls and events without ACP, on DSH 0.2.1-rc.5 and 0.2.2-alpha.2. Experiment only; no DSH runtime yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Cindy <open-agent-runtime+cindy@agents.stamp.build>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hosts restoring changed Claude/Codex prompt options need native compaction before the next input. Add optional
Runtime.manualCompactionandSession.compact()for Claude, Codex and Pi SDK/RPC, with typed completed/rejected/failed/aborted/unconfirmed results tied to native evidence. This is task #154's first PR; the remaining candidates stay undeclared until their separate control and correlation regressions.compactentry, then matches command, boundary and result identities. Codex associates the nativecontextCompactionitem and its turn, and refuses observed active goals or queued inputs. Pi uses exclusive manual start/end facts. Automatic, child, stale and unrelated completions cannot settle the control.rejected / abortedwithout sending native work.timeoutMs: 0still dispatches. After dispatch, timeout or signal stops waiting only: the native operation keeps its slot, late records remain observable, and queue entries wait for native settlement. Claude drains one queued input per turn and reserves it before its native receipt, so a later prompt cannot overtake it. Pi RPC compact has no ordinary 30-second transport deadline.abort()uses the native cancellation control once its target exists. Claude's queued cancellation isaborted; after native execution starts, Claude reportscompact_result=failed, so compact returnsfailed / native_failedindependently of the ordinary turn's OAR-aborted outcome. Codex/Pi retain their native aborted result. An interrupt for native unstarted input is written before arrivals held during cancellation observation are drained. Disposal or process exit returns unconfirmed without inventing a compaction terminal or writing a late rejection.AgentStatus.compactionexposes onlyrequestIdandstarted?; the exportedStatusStatecheckpoint retains native turn/span correlation. Incremental consumers must persist the entire checkpoint with its cursor, includingTaskState.statusandSessionView.statusState. The exportedstatusValueextracts only the display value and cannot replace a resumable checkpoint. Pi's idle state retains the preceding turn outcome.Public record change:
turn_ended.inputId?and orderedinputIds?separately copyresult.user_message_uuidanduser_message_uuidsthrough the common stream-json projection. They appear in public events andFrame.body.events, including ordinary non-compacting turns; raw frames are unchanged. Claude's two queued native messages report the latter singular UUID and both plural UUIDs. The earlier prompt-plus-steer fixture names the original prompt, so OAR infers no first/last rule. Qwen 0.26.5 and Qoder 1.1.81 report neither field, and their resultuuidis not substituted. Hosts should settle every entry ininputIdswhen present. OAR's own interrupt fallback retains its separatecarriesandmergedInputIdsprovenance. Eleven existing snapshots add only 30 identity fields verified against their raw results; both additions are documented in the spec and release-note draft.Validation: the revised implementation passes the full check (3,049 tests and 18 mock behaviors), build, clean packed-consumer type exports, all 14 behavior backends, and full native suites: Claude 2.1.316 (80), Codex 0.166.0 (62), Pi SDK 1.4.0 (97), Pi RPC 1.4.0 (95). The manual-compaction file contributes 33 native controls. Review regressions cover queued cancellation without process kill, separate queue turns, late provenance replies, conflicting command catalogs, goal-turn misattribution, pre-aborted signals, reason redaction, synthetic-terminal absence and checkpoint replay. Pi project fixtures clear and restore inherited
PI_PACKAGE_DIR; their 20 cases also pass with this managed host's conflicting value present. After rebasing onto current main, the full check, build and clean packed-consumer validation pass again, as do all three Pi behavior backends and the full Pi SDK 1.5.0 (97) and Pi RPC 1.5.0 (95) vendor suites. The new capability-inventory test uses the Durable 1.4.0 model-catalog alias introduced by the security update. The final cancellation-order revision passes all 14 behavior backends and full Claude 2.1.316 (80), Qwen 0.26.5 (18) and Qoder 1.1.81 (9) vendor suites. Six independent mutations are caught, including the public Codex abort-before-item gate and the Claude result-frame reference. Packed-consumer validation checks both exports of the checkpoint type and its display helper. Windows/macOS validation awaits CI on the revised head.Evidence, reproductions and release-note draft. No release/version bump or workflow change.