Skip to content

chore: release v0.87.0 - #616

Open
stamp-build[bot] wants to merge 1 commit into
mainfrom
agent/cindy/release-0.87.0
Open

stamp-build[bot] wants to merge 1 commit into
mainfrom
agent/cindy/release-0.87.0

Conversation

@stamp-build

@stamp-build stamp-build Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Cindy from open-agent-runtime opened this pull request through Stamp
View in Stamp →


Minor release: manual compaction (Session.compact(), added in 0.86.0) now also works on Grok, Qoder, Cursor and OpenCode v2 sessions. Compaction events gain the runtime's own operation identity, and a native frame can now say which OAR request it answers. ACP queues now record when OAR actually sends a held input, which changes the request records hosts see on Kimi, Grok, OpenCode, Antigravity and CodeBuddy. A host reacting synchronously to a turn ending can now send its next prompt right away on every runtime except Pi, unless OAR is still holding queued input.

Manual compaction on more runtimes (#614)

  • Newly supported, idle sessions only (activeTurn: "reject"): Grok, Qoder and Cursor declare Runtime.manualCompaction as supported.
  • OpenCode: sessions on OpenCode v2 have compact; OpenCode v1 sessions do not, because a project command can replace /compact and completion cannot be confirmed. OAR reads the version when the session opens (from the installation, or by running opencode --version), so the shared OpenCode Runtime stays undeclared: check session.compact after opening. Shared placement is v2-only and has it.
  • Pi Durable stays undeclared; it moves with the separate Pi Durable 1.5 API migration.
  • Cursor declares support, but the session has compact only when the SDK agent provides summarize().
  • How each new runtime decides the outcome:
    • Grok: an object answer to its dedicated _x.ai/compact_conversation request is completed. Structured failure or cancellation errors give failed or aborted; other RPC errors give rejected (runtime_refused); a null answer gives unconfirmed and frees the session. OAR reports no compaction_started for Grok.
    • Qoder: a started lifecycle, a manual compact_boundary and completion, all for the same command UUID. Success without a boundary gives failed (not_compacted); an error result gives failed (native_failed); cancellation before a boundary gives aborted. A boundary already seen stays completed even if the command later reports cancelled.
    • Cursor: Agent.summarize() returns a run id; that run's root summary and terminal result decide. A finished run without a terminal summary gives unconfirmed but frees the session; a cancelled run without one gives aborted; an SDK refusal gives rejected (runtime_refused). A lost stream or rejected run.wait() gives unconfirmed and keeps the session reserved until the run ends or dispose(). Cursor SDK 1.2.0 summarize() cannot pass local.force, so it refuses a restored agent whose run is still marked active.
    • OpenCode v2: native compaction start and end markers with the same messageId, plus the answer to the /compact prompt. An ordinary successful prompt is not proof. A native RPC error gives failed (native_failed); an answer without matching markers gives unconfirmed and frees the session.
    • On all four, losing the transport before a known answer gives unconfirmed and keeps the session reserved until dispose().
  • Compacting an empty history differs by runtime: Grok completed (a native no-op), Qoder failed / not_compacted, OpenCode v2 failed / native_failed, Cursor rejected.
  • While compaction is unresolved, queue() input is held and sent after it ends on Claude, Pi, Grok, Qoder, Cursor and OpenCode v2; Codex still refuses it as busy. abort() uses the runtime's own cancel: session/cancel on Grok and OpenCode v2, run.cancel() on Cursor (an abort before the run exists cancels it once summarize() returns), and the interrupt on Qoder.

Compaction events and new record fields (all optional and additive)

  • Runtimes that now report compaction events: Qoder emits compaction_started on its compacting status, automatic compactions included. OpenCode v2 emits compaction_started and compaction_ended for every compaction, with the native trigger. Grok emits compaction_ended for manual compactions.
  • compaction_started.compactionId? and compaction_ended.compactionId?: the runtime's own identity for that compaction (currently only OpenCode v2's messageId), present on automatic compactions too. It is not the envelope's turn spanId.
  • compaction_ended.requestId?: set only when the native response explicitly answers that OAR compact request (currently Grok). Never inferred from timing.
  • FrameBody.responseTo?: the id of the OAR request this native frame explicitly answers. Currently set on Grok and OpenCode v2 compact RPC answers, and on Qoder's terminal command_lifecycle whose command_uuid is the id OAR sent. Exact equality only; it does not mean the request succeeded.
  • compact requests can carry settledBy: "native_response" (Grok, OpenCode v2, Qoder): only a frame whose responseTo equals that request id ends the operation. Earlier turn_ended or compaction_ended events stay in the stream but do not end it.
  • New native frame types: Grok _x.ai/compact_conversation and _x.ai/compact_conversation/error. OpenCode v2 compact answers reuse session/prompt and session/prompt/error, but carry responseTo and no turn_ended.
  • Cursor:
    • Frames now carry the native run id as the envelope spanId: callback frames (child tasks too), stream frames with a run_id, cursor/run_result, and run failure frames. Root session and child-task attribution are unchanged.
    • The compact accepted response carries native: { runId }.
    • cursor/stream/summary frames whose native agent_id is missing or is not the root session no longer carry compaction events, automatic summaries included.
    • A cursor/run_failed frame during an unresolved compaction carries no turn_ended.
    • Callbacks that arrive before send() returns are recorded after it, keeping their original receivedAt, so receivedAt is not always increasing.

Behaviour change: ACP queue records (Kimi, Grok, OpenCode, Antigravity, CodeBuddy)

  • Every accepted ACP queue(), even on an idle session, now answers accepted with queued: "adapter". When OAR sends it to the runtime, the stream gets a new prompt request whose carries names that input; the queue request gets no second response. Before, an idle queue was sent at once with no prompt request.
  • Hosts that count requests or group by request kind must treat that prompt as part of the same submission, not a new host input. The dispatch prompt carries only input, carries and images; read inputId and origin from the carried queue input. A queue with no inputId is identified by its queue request id within the same record stream, and that fallback never links across a resumed stream.
  • Conversation and session views show queue plus dispatch as one input.
  • ACP prompt() now returns busy while OAR holds or is sending a queued input, not only during an active turn. A host prompt({ carries: [heldId] }) now returns busy instead of unsupported, and that attempt is added to the held input.
  • While OAR holds an ACP input and no turn is running, status shows running / waiting_model instead of idle, until the dispatch prompt is accepted or a withdraw is accepted. If the dispatch is rejected, the input stays held.

Bug fix: the next prompt can follow a turn end immediately

  • A synchronous observer of turn_ended could see idle but still get busy, and in Codex compaction the old cleanup could clear the turn the observer had just started. OAR now releases its own turn ownership before publishing the terminal frame:
    • ordinary turns on Claude, Qoder, Qwen, Codex, Cursor and the five ACP runtimes;
    • compaction on the runtimes that support it.
  • Native payloads and the relative order of frames are unchanged.
  • Held input keeps its place: a new prompt() or queue() from that observer cannot overtake input OAR is already holding.
  • A compact() the runtime refuses at dispatch releases its reservation before observers see that refusal restore idle (every runtime with compact()).
  • Known gaps:
    • Pi SDK and Pi RPC ordinary turns are not fixed yet: a synchronous turn_ended observer there still gets busy; send the next prompt after the callback returns.
    • Claude, Qoder, Qwen and Cursor: while OAR holds a queued input, status() can show idle while prompt() returns busy to keep first-in, first-out order. These four do not yet record queue ownership the way ACP now does.
    • With no held input, status and prompt admission change on the same record on every runtime except Pi. Both gaps are tracked as follow-ups.

Upgrade notes

  • Rejected outcome, accepted response: a rejected outcome can now come with an accepted response. When the runtime refuses after accepting the dispatch (currently only Grok), outcome.kind is rejected while outcome.response.body.kind stays accepted; the refusal is in the correlated frame. Don't assume a rejected outcome has a rejected response.
  • Status checkpoints: hosts that fold status incrementally must still persist the complete StatusState, as in 0.86.0. It now also holds the compaction identity learned from the first native manual start, the native settlement rule, and ACP held-queue ownership. SessionView.statusState can now appear outside compaction too.
  • Custom runtimes: the status fold honours accepted.queued: "adapter", settledBy and responseTo from any runtime built on the kernel SPI.
  • Public types: no union members were added (event, request and response kinds, RejectionCode, ControlAction, CompactionOutcome kinds and codes), and no exports changed.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants