fix(integrations): replay Band room history when a remote ACP session can't be restored [INT-1095]#467
Open
AlexanderZ-Band wants to merge 5 commits into
Conversation
…tored [INT-1095] The ACP client adapter relied solely on ACP session/load to restore conversation context; when the remote process was fresh (container restart, fork-per-connection spawn, crash) the agent started with zero context despite the room transcript being intact on the platform. - Converter now carries the room's text transcript as replay lines via the shared build_replay_messages helper (the opencode/letta pattern); adapter narration events never replay - On bootstrap, when no persisted session can be restored (missing id, failed load, or any session/load protocol error - now treated as a recoverable load-miss instead of killing the turn), the transcript is injected once into the fresh session's first prompt under a context-only header, with the current message attributed and last - Codex's private history formatting consolidated onto the shared helper (drops the dead "message" type; whitespace-only lines now filtered for all four consumers) - Live message now attributed via format_for_llm, matching every other adapter - New deterministic e2e: fresh COPILOT_HOME per phase forces the session-load miss, pinning that recall flows through the replay (including agent-authored lines) in every lane Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replay header reworded: affirmative "read-only background / already handled" framing over bare prohibitions, with an escape hatch so an explicit recall request is never refused - Live message now sits under a nonce'd [New Message <nonce>] boundary marker the header names; the per-turn nonce defeats a replayed message spoofing the boundary (transcript content predates the turn) - Dropped the preemptive flaky_infra on the new e2e (no observed transient to cite; a rerun would slow-surface no-reply product bugs) - Converter tests refactored onto intent-named builders; non-obvious assertions now carry why-it-is-a-bug messages - Stale docstrings brought up to date (load_session error contract, replay helper consumers) Live-revalidated: session-load-miss e2e + offline cold-boot matrix cell both green with the new wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Defense in depth alongside the uv lock --check gate that landed on main in parallel: each job's install itself refuses a stale lockfile, including the release build (publish-band), so drift cannot slip in even when a job runs without the lint gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…laying backlog Two P1 review findings, both root-caused: - Rehydration now keys off fresh-session creation, not the one-time bootstrap flag: _get_or_create_session reports created, and a session minted off-bootstrap (the previous runtime was torn down mid-run by a prompt failure) re-fetches the room transcript via tools.fetch_room_context so the respawned turn does not start amnesiac. _load_persisted_session's now-redundant bool contract is deleted. - Bootstrap history stops strictly before the triggering message (messages_before in runtime/formatters.py, applied in the default preprocessor for every adapter): backlog messages that accumulated while the agent was offline are pending turns of their own; replaying them exposed future requests and duplicated them when their own turn arrived. Oneshot's swallow-if-seen drain is deliberately untouched (its seen_ids coupling depends on the LLM having seen the page). Live-revalidated: session-load-miss e2e + offline cold-boot matrix cell. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tials Replaces the hand-rolled jq parse / fetch / switch / token-spliced push URL with a conditional checkout of the release PR branch using the App token: credentials flow through checkout's audited persistence, and the refresh step shrinks to uv lock + commit + plain push (still App-authored, so the PR's CI triggers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AlexanderZ-Band
force-pushed
the
fix/acp-client-adapter-doesnt-fall-back-to-band-histor-INT-1095
branch
from
July 21, 2026 16:53
298c158 to
d179cbd
Compare
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.
Summary
Fixes INT-1095 and INT-1101. The ACP client adapter relied solely on ACP
session/loadto restore conversation context; whenever the remote agent process was fresh (container restart,,fork-per-connection spawn, crash) the agent started with zero context, even though the Band room transcript is intact on the platform.The fix adopts the existing
opencode/letta"resume-else-replay" pattern, then hardens it through two review rounds. The governing invariant: a freshly created ACP session owes the room a transcript replay; a restored one does not.What changed
Replay fallback (
converters/acp_client.py,integrations/acp/client_adapter.py,integrations/acp/client_runtime.py)replay_messages, via the sharedbuild_replay_messageshelper). Adapter narration events (thought/tool_call/tool_result/task) never replay.session/loadis validated first; on any miss (no persisted id, unavailable, or erroring — protocol errors are now a recoverable load-miss, not a turn-killing exception) the fresh session's first prompt is seeded with the transcript.tools.fetch_room_context, so a respawn never starts amnesiac._get_or_create_sessionreports created-vs-reused;_load_persisted_session's redundant bool contract was deleted.Prompt safety (review-driven)
format_for_llm) and sits last, under a nonce'd[New Message <nonce>]boundary marker the header names — replayed content predates the turn, so it cannot spoof the boundary.Backlog truncation (all adapters,
runtime/formatters.py+preprocessing/default.py)messages_before): backlog messages that accumulated while the agent was offline are pending turns of their own; replaying them exposed future requests and duplicated them when their own turn arrived. Oneshot's swallow-if-seen drain is deliberately untouched (itsseen_idscoupling depends on the LLM having seen the fetched page).Codex consolidation (
adapters/codex.py)"message"type (not inMessageType, no producer anywhere); whitespace-only lines filtered for all four consumers.CI / lockfile
uv.lockcaught up to the released 1.3.0 (the release flow bumps pyproject without regenerating the lock — the source of everyone's recurring dirty-tree noise).uv sync --lockedeverywhere, and the release job refreshesuv.lockon the release-please PR (checked out with the App token so the push triggers the PR's CI). First live exercise will be the next release PR — worth watching that run.Guarding the known rehydration weaknesses
Testing
test_acp_recall_via_room_replay_when_session_load_misses— new, deterministic: freshCOPILOT_HOMEper phase forces the load-miss; phase-2 recall carries a user-stated marker and an agent-produced fact that only the replayed agent side of the transcript can supply.copilot_acpcells: green.Follow-ups filed
Likely also closes INT-623 (L4 Rehydration placeholder).
🤖 Generated with Claude Code