You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Codex→Claude replies silently lost when the Claude session is idle (no ack, no queue fallback)
Summary
When Codex replies to Claude, the message is delivered as a fire-and-forgetnotifications/claude/channel JSON-RPC notification. If the Claude Code session is idle at that moment, the notification is lost. Because the loss is silent — no transport error is thrown — the bridge's fallback queue never fires, so get_messages also returns empty. The reply is unrecoverable: it is neither rendered in-band nor retrievable via pull.
Net effect: Claude→Codex works reliably, Codex→Claude is lossy whenever Claude is idle.require_reply: true does not help — Codex emits the reply, but delivery rides the same unreliable channel.
This is consistent with the known idle-channel variability the agent-bridge README already documents. On this machine it presented at the severe end: 100% loss across three reproductions, with the message not even displayed — see "Environment-specific severity" below.
Claude Code 2.1.201, launched with --dangerously-load-development-channels (verified present) + OAuth
Codex app-server / TUI 0.142.5
macOS (arm64); multi-pair v3 layout; both sides run under a cmux wrapper
Daemons healthy; dev-channels flag present; no stale killed sentinel involved
Root cause
src/claude-adapter.ts:240-268 — every Codex→Claude message is delivered exactly one way:
awaitthis.server.notification({method: "notifications/claude/channel",params: {...}});this.log(`Pushed notification: ${message.id}`);// logged on WRITE success}catch(e){this.queueFallbackMessage(message);}// only on a transport WRITE error
Three facts combine into the symptom:
It's a JSON-RPC notification — fire-and-forget. The promise resolves the instant the bytes hit the stdio pipe. There is no ack from Claude Code, so "Pushed notification" means "handed to the pipe," not "Claude received/rendered it."
notifications/claude/channel is experimental and idle-unreliable. The project README notes: "channel pushes to a fully idle session have known upstream variability." Two upstream Claude Code issues (both verified on GitHub 2026-07-05 — see References) describe this: #44380"Channel messages don't wake idle sessions" (OPEN; symptom: displays but not auto-processed) and #61797"MCP notifications silently dropped when delivered to idle session via --channels" (CLOSED as stale, not fixed; symptom: "not in the pane, not in the transcript" — the closer match to this report). The maintainer's docs (docs/07-…v3.md:80) also record a 2026-06-14 local test that refuted #44380 (idle Claude did auto-respond then), so the accurate framing is "known variability/flakiness" — this machine (CC 2.1.201 + cmux) sits at the severe, 100%-loss end of that variability.
The fallback queue only catches write failures. A silent idle-drop throws no exception → queueFallbackMessage never runs → get_messages stays pending=0. The message is gone with no trace. Messages are also deduped by id (rememberDelivery), so they are never re-pushed, and the previously reliable AGENTBRIDGE_MODE=pull (Claude actively polls get_messages) was removed. Only the budget-resume path has ack+retry (ack_resume); ordinary Codex replies do not.
Why "idle" is the trigger
Mid-turn (Claude actively stepping): channel notifications are injected between model steps → works. Normal interactive back-and-forth therefore works.
Fully idle (turn ended, or waiting on a backgrounded shell that doesn't keep the agent loop alive): the push is only displayed-or-dropped upstream → silently lost.
Environment-specific severity (possibly a separate, sharper upstream report)
The maintainer's 2026-06-14 test showed idle push sometimes works (and #44380 only claims idle messages "display but aren't auto-processed"). On this machine the failure was stronger on both axes: it was 100% reproducible (repros A/B/C below) and the message did not even display — a harder failure than #44380 describes. That gap suggests an environment-specific aggravator layered on top of the generic upstream variability — most likely Claude Code 2.1.201 and/or the cmux wrapper both sessions run under. This may warrant its own, narrower upstream report: "channel notification to an idle session is dropped entirely (not rendered, not processed)."
Reproductions with instanceId logs (two independent pairs, same machine)
Filed per the request in the closing note of #64: "如在 push-only 架构下复现类似症状,请带 instanceId 日志开新 issue" ("if you reproduce similar symptoms under the push-only architecture, open a new issue with instanceId logs").
In each repro the daemon logs a successful push and then a get_messages call on the same live adapter instance= id returns empty — the reply never surfaces in Claude. All three used force-forward-reply-required (so STATUS buffering is not involved). Timestamps are UTC (2026-07-05T…), abbreviated to HH:MM:SS.mmm.
Repro A — pair main-a5bb7620, pong (len=4), instance 0c430e4d:
Not a buffering artifact. All three carry the force-forward-reply-required tag → the message bypassed STATUS buffering and was meant to deliver immediately.
Never entered the fallback queue.dropped=0, oversized=0 and pending=0 → no transport write error was thrown, so queueFallbackMessage never ran and the message is unrecoverable — the exact push-only gap.
Reproduces on two independent pairs, one of them (main-8abffcff, Repro C) verified by a human with shell access reading the daemon log directly — the push is logged ✅ server-side while the reply never reaches the model.
Additionally, status.json for the affected pair at drop time showed the daemon still connected to the Codex app-server (appServerUrl ws://127.0.0.1:4520, turnPhase: idle, TUI attached) — rules out a bridge disconnect. Together these place the drop client-side, after a successful push — not a bridge or connection failure.
Raw excerpts live in ~/Library/Application Support/AgentBridge/pairs/{main-a5bb7620,main-8abffcff}/agentbridge.log.
Relationship to prior (closed) issues — we read #29 and #64
This is not a fresh discovery; it is a regression of an already-reported-and-closed bug. We read the full threads of both:
get_messages 返回空但消息实际已在队列中 / get_messages returns empty while messages exist in queue #64 — "get_messages returns empty while messages exist in queue" (closed superseded 2026-06-10). Its closing note states the pull delivery mode was removed wholesale in commit 6b1e948 ("feat!: 删除可配置 pull 投递模式,push 唯一化"), replaced by push-only + per-message fallback — and explicitly invites this report: "如在 push-only 架构下复现类似症状,请带 instanceId 日志开新 issue."
The regression, stated plainly: the pull-default fallback that closed #29 (PR #61) was later deleted by 6b1e948 to make delivery push-only — because pull "could not wake an idle session, which silently broke the budget RESUME chain" (src/claude-adapter.ts:7-8). That architectural trade re-opened #29's exact failure mode for ordinary Codex→Claude messages, but with no pull fallback left to recover them. #29 still reads as "fixed" and #64 as "superseded," so no open issue currently tracks this — which is why it looks unreported. Related earlier duplicates in the same family (all closed): #18, #32.
Impact
Any workflow where Claude dispatches to Codex and then waits (ending its turn, or backgrounding the wait) can silently lose Codex's reply.
No recovery path exists once dropped: not re-pushed (dedup), not queued (no write error), pull mode removed.
Misleading signal: get_messagespending=0 reads as "nothing to fetch / delivered fine," when the message may have been lost.
Proposed fix
Extend the ack+retry+queue-fallback treatment (currently resume-only, via ack_resume) to all Codex→Claude messages:
Queue-on-no-ack. On timeout (not just on transport write error), call queueFallbackMessage(message) so get_messages can retrieve it. This is the single highest-value change — it turns a silent loss into a recoverable pull.
Docs: clarify that "Pushed notification" is a write-success log, not a delivery confirmation, and that pending=0 does not imply successful delivery.
A fully complete cure requires an upstream Claude Code change (reliable processing of idle channel messages), but (1)–(3) make the bridge robust against the current upstream behavior without waiting on it — and do not depend on whether #44380/#61797 are ever fixed.
Expected behaviour
A Codex reply produced while Claude is idle should either be delivered in-band or remain retrievable via get_messages — it must never be silently lost (i.e. no case where the daemon logs Pushed ✅ yet get_messages returns pending=0 forever).
Implementation specifics are the maintainer's call, not prescribed here — e.g. the ack transport and timeout, whether to reinstate an opt-in AGENTBRIDGE_MODE=pull vs. an internal no-ack queue, and how any of this coexists with the resume-wake requirement that motivated the push-only move (6b1e948).
Workarounds today
Keep the receiver active/foreground while awaiting Codex (mid-turn injection works); do not end the turn or background the wait.
If a reply is dropped, it is unrecoverable via the bridge — the human must relay it, or Claude must re-trigger Codex once active. Tailing the per-pair agentbridge.log (Codex → Claude … (N chars)) is a usable observability workaround but not real in-band delivery.
Claude Code #44380 / #61797 — verified on GitHub 2026-07-05: #44380 "Channel messages don't wake idle sessions (--channels plugin)" is OPEN (created 2026-04-06; macos, area:mcp; symptom: displays but not processed). #61797 "MCP notifications silently dropped when delivered to idle session via --channels" is CLOSED as stale/inactive — not fixed (created 2026-05-23; macos, area:mcp; symptom: "not in the pane, not in the transcript"; its reporter independently built the same ack+retransmit-on-no-ack broker workaround proposed here). #61797 is the closer match to this report; #44380 is related. All three upstream reporters are macOS + a terminal multiplexer (tmux upstream, cmux here).
Codex→Claude replies silently lost when the Claude session is idle (no ack, no queue fallback)
Summary
When Codex replies to Claude, the message is delivered as a fire-and-forget
notifications/claude/channelJSON-RPC notification. If the Claude Code session is idle at that moment, the notification is lost. Because the loss is silent — no transport error is thrown — the bridge's fallback queue never fires, soget_messagesalso returns empty. The reply is unrecoverable: it is neither rendered in-band nor retrievable via pull.Net effect: Claude→Codex works reliably, Codex→Claude is lossy whenever Claude is idle.
require_reply: truedoes not help — Codex emits the reply, but delivery rides the same unreliable channel.This is consistent with the known idle-channel variability the agent-bridge README already documents. On this machine it presented at the severe end: 100% loss across three reproductions, with the message not even displayed — see "Environment-specific severity" below.
Environment
55120e8), installed correctly--dangerously-load-development-channels(verified present) + OAuthkilledsentinel involvedRoot cause
src/claude-adapter.ts:240-268— every Codex→Claude message is delivered exactly one way:Three facts combine into the symptom:
"Pushed notification"means "handed to the pipe," not "Claude received/rendered it."notifications/claude/channelis experimental and idle-unreliable. The project README notes: "channel pushes to a fully idle session have known upstream variability." Two upstream Claude Code issues (both verified on GitHub 2026-07-05 — see References) describe this: #44380 "Channel messages don't wake idle sessions" (OPEN; symptom: displays but not auto-processed) and #61797 "MCP notifications silently dropped when delivered to idle session via --channels" (CLOSED asstale, not fixed; symptom: "not in the pane, not in the transcript" — the closer match to this report). The maintainer's docs (docs/07-…v3.md:80) also record a 2026-06-14 local test that refuted #44380 (idle Claude did auto-respond then), so the accurate framing is "known variability/flakiness" — this machine (CC 2.1.201 + cmux) sits at the severe, 100%-loss end of that variability.queueFallbackMessagenever runs →get_messagesstayspending=0. The message is gone with no trace. Messages are also deduped by id (rememberDelivery), so they are never re-pushed, and the previously reliableAGENTBRIDGE_MODE=pull(Claude actively pollsget_messages) was removed. Only the budget-resume path has ack+retry (ack_resume); ordinary Codex replies do not.Why "idle" is the trigger
Environment-specific severity (possibly a separate, sharper upstream report)
The maintainer's 2026-06-14 test showed idle push sometimes works (and #44380 only claims idle messages "display but aren't auto-processed"). On this machine the failure was stronger on both axes: it was 100% reproducible (repros A/B/C below) and the message did not even display — a harder failure than #44380 describes. That gap suggests an environment-specific aggravator layered on top of the generic upstream variability — most likely Claude Code 2.1.201 and/or the cmux wrapper both sessions run under. This may warrant its own, narrower upstream report: "channel notification to an idle session is dropped entirely (not rendered, not processed)."
Reproductions with instanceId logs (two independent pairs, same machine)
Filed per the request in the closing note of #64: "如在 push-only 架构下复现类似症状,请带 instanceId 日志开新 issue" ("if you reproduce similar symptoms under the push-only architecture, open a new issue with instanceId logs").
In each repro the daemon logs a successful push and then a
get_messagescall on the same live adapterinstance=id returns empty — the reply never surfaces in Claude. All three usedforce-forward-reply-required(so STATUS buffering is not involved). Timestamps are UTC (2026-07-05T…), abbreviated toHH:MM:SS.mmm.Repro A — pair
main-a5bb7620,pong(len=4), instance0c430e4d:Repro B — pair
main-a5bb7620,[IMPORTANT] pong(len=16,require_reply: true), instanceb236ca8a:Repro C — pair
main-8abffcff(a different, independent pair),PONG …(len=27,require_reply: true), instanceb169a139:What these instanceId logs prove — each preempts a likely reviewer objection:
get_messagesshare one liveinstance=id in every repro (A0c430e4d, Bb236ca8a, Cb169a139) — rules out a stale/dead MCP connection and multi-instance misrouting (get_messages 返回空但消息实际已在队列中 / get_messages returns empty while messages exist in queue #64's candidate root-cause--dangerously-load-development-channelsin Claude cannot be used in third-party platform or api-key #1).force-forward-reply-requiredtag → the message bypassed STATUS buffering and was meant to deliver immediately.dropped=0, oversized=0andpending=0→ no transport write error was thrown, soqueueFallbackMessagenever ran and the message is unrecoverable — the exact push-only gap.main-8abffcff, Repro C) verified by a human with shell access reading the daemon log directly — the push is logged✅server-side while the reply never reaches the model.Additionally,
status.jsonfor the affected pair at drop time showed the daemon still connected to the Codex app-server (appServerUrl ws://127.0.0.1:4520,turnPhase: idle, TUI attached) — rules out a bridge disconnect. Together these place the drop client-side, after a successful push — not a bridge or connection failure.Raw excerpts live in
~/Library/Application Support/AgentBridge/pairs/{main-a5bb7620,main-8abffcff}/agentbridge.log.Relationship to prior (closed) issues — we read #29 and #64
This is not a fresh discovery; it is a regression of an already-reported-and-closed bug. We read the full threads of both:
server.notification(), no ack). It was closed with: "Fixed by PR fix: default auto mode to pull to prevent silent message loss #61 — auto 模式默认改为 pull,push 失败时 fallback 到 pull 队列,静默丢失问题已解决" — i.e. the fix was making pull the default and falling back to the pull queue, which is reliable for an idle session.6b1e948("feat!: 删除可配置 pull 投递模式,push 唯一化"), replaced by push-only + per-message fallback — and explicitly invites this report: "如在 push-only 架构下复现类似症状,请带 instanceId 日志开新 issue."The regression, stated plainly: the pull-default fallback that closed #29 (PR #61) was later deleted by
6b1e948to make delivery push-only — because pull "could not wake an idle session, which silently broke the budget RESUME chain" (src/claude-adapter.ts:7-8). That architectural trade re-opened #29's exact failure mode for ordinary Codex→Claude messages, but with no pull fallback left to recover them. #29 still reads as "fixed" and #64 as "superseded," so no open issue currently tracks this — which is why it looks unreported. Related earlier duplicates in the same family (all closed): #18, #32.Impact
get_messagespending=0reads as "nothing to fetch / delivered fine," when the message may have been lost.Proposed fix
Extend the ack+retry+queue-fallback treatment (currently resume-only, via
ack_resume) to all Codex→Claude messages:notifications/claude/channelas needing acknowledgement; if no ack arrives within a short timeout, consider the push undelivered. (Note: an idle Claude that drops the notification will also not send the ack — that's fine; the absence of an ack is exactly the detection signal.--dangerously-load-development-channelsin Claude cannot be used in third-party platform or api-key #1 is just the primitive that makes feat: add dual-mode message transport (Channel Push + Tool Pull) #2 fire.)queueFallbackMessage(message)soget_messagescan retrieve it. This is the single highest-value change — it turns a silent loss into a recoverable pull.get_messagesto drain unacked messages (optionally reinstate an opt-inAGENTBRIDGE_MODE=pullfor idle-heavy setups). Real robustness comes from feat: add dual-mode message transport (Channel Push + Tool Pull) #2 + feat: add smart message filtering with marker protocol (v1.1) #3;--dangerously-load-development-channelsin Claude cannot be used in third-party platform or api-key #1 alone does nothing without them."Pushed notification"is a write-success log, not a delivery confirmation, and thatpending=0does not imply successful delivery.A fully complete cure requires an upstream Claude Code change (reliable processing of idle channel messages), but (1)–(3) make the bridge robust against the current upstream behavior without waiting on it — and do not depend on whether #44380/#61797 are ever fixed.
Expected behaviour
A Codex reply produced while Claude is idle should either be delivered in-band or remain retrievable via
get_messages— it must never be silently lost (i.e. no case where the daemon logsPushed ✅yetget_messagesreturnspending=0forever).Implementation specifics are the maintainer's call, not prescribed here — e.g. the ack transport and timeout, whether to reinstate an opt-in
AGENTBRIDGE_MODE=pullvs. an internal no-ack queue, and how any of this coexists with the resume-wake requirement that motivated the push-only move (6b1e948).Workarounds today
agentbridge.log(Codex → Claude … (N chars)) is a usable observability workaround but not real in-band delivery.References
src/claude-adapter.ts:240-268(delivery + fallback logic);:7-8and:150-155(pull mode removed, push-only always used)ack_resume(resume-only ack path)docs/07-…v3.md:80— maintainer's 2026-06-14 local test that refuted #44380 (idle Claude auto-responded)6b1e948, invites this report), Push mode silently drops messages — no fallback to pull queue when channel delivery fails #18 / Auto mode push delivery may silently drop messages when channel API is unavailable #32 (earlier duplicates in the same family, closed). Fix that closed Push notifications silently dropped when Claude Code is not actively listening on channel #29 (pull fallback) no longer exists in current code.stale/inactive — not fixed (created 2026-05-23; macos, area:mcp; symptom: "not in the pane, not in the transcript"; its reporter independently built the same ack+retransmit-on-no-ack broker workaround proposed here). #61797 is the closer match to this report; #44380 is related. All three upstream reporters are macOS + a terminal multiplexer (tmux upstream, cmux here).