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
A claude --continueresume on the tmux transport can come up with a live REPL but an unbound MCP identity — the agent's X-Agent-Name is never re-established at the shared :8890 gateway, so every MCP tool call (pinky-self, pinky-messaging, pinky-memory) returns 404 / "Agent not found". The agent is alive and talking but blind to its own tools. Worse, a plain daemon/agent restart re-resumes the same orphaned transcript and reproduces the blank-identity state forever, until a forced-fresh launch (no --continue) is issued by hand.
This is the third sibling of the tmux-relaunch failure family:
Same machinery as #612 (force_fresh_context_once retry); new predicate: "is the resumed REPL alive and correctly bound?" instead of just "is it alive?".
Observed (Barsik, prod Mac Mini, 2026-06-02)
MCP blackout hit mid-session (not just at boot): all pinky MCP tools started 404-ing on a live session.
Every plain restart re-ran claude --continue and re-orphaned the session.
The remediation that worked: POST /agents/{name}/streaming/restart — disconnect, clear the persisted resume handle, set force_fresh_context_once, relaunch fresh. A fresh launch re-binds identity correctly.
It also tripped the restart guard (last save_my_context was 2h+ stale) — and because save_my_context itself routes through the dead MCP, the agent could not refresh its save to satisfy the guard: a deadlock requiring an operator with bump_context_updated_at / bypass.
Root cause
The :8890 MCP gateway is shared across all agents; per-request identity comes only from the X-Agent-Name HTTP header (sourced from each agent's .mcp.json):
shared_mcp.py:AgentNameMiddleware (L141-163) reads x-agent-name into the _current_agent ContextVar. Missing/blank header → ContextVar stays "".
make_agent_name_resolver / LazyAgentName.resolve() (L41-137) falls back to closure_agent_name, which is "" in shared-SSE mode.
Why a --continue resume comes up unbound but a fresh launch does not:
A fresh launch fires SessionStart on a virgin transcript (agent_registry.py hook generator L980-1040; settings matcher .* L1805-1818) and the in-REPL MCP client does a clean cold bind, re-sending X-Agent-Name.
A --continueresume re-loads the prior conversation; the resumed CC process appears to re-establish its SSE/MCP clients without re-sending X-Agent-Name (or SessionStart does not re-fire the same way), so the gateway never re-learns identity → _current_agent stays blank → 404. (This exact CC resume semantics is the open question below.)
On plain restart, on_startup (api.py:8460-8473) reads the persisted main_resume handle and calls _start_streaming_session(resume_id=...)withoutforce_fresh_context_once; tmux_session.py:_build_claude_cmd (L1801-1803) then sets use_continue = _has_prior_transcript() and not force_fresh — i.e. it re-resumes purely because a transcript exists on disk. The cwd transcript is the real latch; clearing the DB handle alone does not heal tmux — only force_fresh_context_once does.
Likely trigger: the recent SDK→tmux transport migration left an SDK-origin transcript in Barsik's cwd (the canonical #612 trigger). Here the REPL survives the resume but comes up identity-unbound — a case #612's has-session liveness check does not catch.
Is it a known CC/tmux issue?
Partly. #606 documents a real CC regression family — stateful-MCP reconnect loops (2.1.147–2.1.152), session-resume memory, and "background sessions running on stale daemon before upgrade" (fixed 2.1.153) — the last of which directly intersects our update_and_restart + background-tmux flow. However, production now runs 2.1.160 (> 2.1.153), so those specific upstream bugs are patched. This orphan is therefore most likely the PinkyBot-side resume/identity-rebind gap, not an unpatched CC bug. (Web research in progress to confirm CC --continue SessionStart / .mcp.json re-read semantics — will update.)
Open questions (being resolved)
Does CC emit SessionStart on a --continue resume, or only on fresh sessions? If only fresh, that alone explains why resumes never re-open the readiness gate / re-bind identity (smoking gun).
On --continue, does CC re-read .mcp.json and re-send X-Agent-Name, or restore MCP client state from the transcript without re-sending headers? If the latter, a resume routes blank independent of the on-disk config.
Confirm the 404 originates at the :8890 gateway (blank X-Agent-Name) vs the /transport/transcript-path hook endpoint (api.py:5238, 404s when agents.get(name) is None).
Proposed fix (multi-pronged)
Primary — identity-verify launch hook (sibling of #612). In tmux_session.py:_spawn_tmux_repl / connect(), after a --continue spawn, add _verify_repl_identity_bound() (the identity analog of #612's _verify_repl_survived_launch liveness poll): confirm the agent's MCP identity re-bound within a grace window (e.g. the gateway saw X-Agent-Name == self.agent_name, and/or SessionStart re-POSTed transcript-path for this name). If unbound → kill, set force_fresh_context_once=True, relaunch once fresh, re-verify; fail to DEAD if still unbound. Needs a small side-channel in shared_mcp.py to record last-seen X-Agent-Name per agent (no such signal exists today).
Boot-loop self-heal.api.py:on_startup (L8460): set force_fresh_context_once when the prior session was orphaned/blank (e.g. a last-shutdown manifest flag), so a plain restart self-heals instead of re-resuming.
Fail-closed env.tmux_session.py:_build_repl_env (L1861) / connect — refuse to spawn (or transition to BOOT_FAILED) when self.agent_name is blank, so a nameless session can never launch and route as blank. Guard agent_name non-empty at _start_streaming_session (L2604) / _ensure_streaming_session (L2747) too.
Gateway observability.shared_mcp.py:AgentNameMiddleware (L152) — emit a structured signal / reject (logged 400) on blank X-Agent-Name instead of silently 404-ing per tool, so the daemon can correlate the blackout to a specific resumed session and auto-trigger the fallback.
Secondary — restart-guard deadlock.api.py:_build_restart_guard (L1754) / restart_streaming_session (L6984): when the agent's MCP self-tools are unreachable (so it cannot refresh save_my_context), the staleness guard must not hard-block — downgrade to warn or add a reason="tools_unreachable" bypass, and/or thread an explicit force/bypass_guard param via a new RestartStreamingRequest model (none exists today). Mirror #613's force_restart(bypass_guard=True) / bump_context_updated_at.
Summary
A
claude --continueresume on the tmux transport can come up with a live REPL but an unbound MCP identity — the agent'sX-Agent-Nameis never re-established at the shared:8890gateway, so every MCP tool call (pinky-self, pinky-messaging, pinky-memory) returns 404 / "Agent not found". The agent is alive and talking but blind to its own tools. Worse, a plain daemon/agent restart re-resumes the same orphaned transcript and reproduces the blank-identity state forever, until a forced-fresh launch (no--continue) is issued by hand.This is the third sibling of the tmux-relaunch failure family:
--continueforce_fresh_context_onceheal (open)Same machinery as #612 (
force_fresh_context_onceretry); new predicate: "is the resumed REPL alive and correctly bound?" instead of just "is it alive?".Observed (Barsik, prod Mac Mini, 2026-06-02)
claude --continueand re-orphaned the session.POST /agents/{name}/streaming/restart— disconnect, clear the persisted resume handle, setforce_fresh_context_once, relaunch fresh. A fresh launch re-binds identity correctly.save_my_contextwas 2h+ stale) — and becausesave_my_contextitself routes through the dead MCP, the agent could not refresh its save to satisfy the guard: a deadlock requiring an operator withbump_context_updated_at/ bypass.Root cause
The
:8890MCP gateway is shared across all agents; per-request identity comes only from theX-Agent-NameHTTP header (sourced from each agent's.mcp.json):shared_mcp.py:AgentNameMiddleware(L141-163) readsx-agent-nameinto the_current_agentContextVar. Missing/blank header → ContextVar stays"".make_agent_name_resolver/LazyAgentName.resolve()(L41-137) falls back toclosure_agent_name, which is""in shared-SSE mode.MemoryStorePool.get_storedb_path_resolver (L241-271) raisesValueErrorfor a blank/unknown agent → surfaced to the agent as 404 "Agent not found". (This is routing-by-name, distinct from Stale PINKY_AGENT_KEY after daemon restart locks agent out of MCP tools (401) #641's 401 auth / stale-PINKY_AGENT_KEYpath.)Why a
--continueresume comes up unbound but a fresh launch does not:SessionStarton a virgin transcript (agent_registry.pyhook generator L980-1040; settings matcher.*L1805-1818) and the in-REPL MCP client does a clean cold bind, re-sendingX-Agent-Name.--continueresume re-loads the prior conversation; the resumed CC process appears to re-establish its SSE/MCP clients without re-sendingX-Agent-Name(orSessionStartdoes not re-fire the same way), so the gateway never re-learns identity →_current_agentstays blank → 404. (This exact CC resume semantics is the open question below.)on_startup(api.py:8460-8473) reads the persistedmain_resumehandle and calls_start_streaming_session(resume_id=...)withoutforce_fresh_context_once;tmux_session.py:_build_claude_cmd(L1801-1803) then setsuse_continue = _has_prior_transcript() and not force_fresh— i.e. it re-resumes purely because a transcript exists on disk. The cwd transcript is the real latch; clearing the DB handle alone does not heal tmux — onlyforce_fresh_context_oncedoes.Likely trigger: the recent SDK→tmux transport migration left an SDK-origin transcript in Barsik's cwd (the canonical #612 trigger). Here the REPL survives the resume but comes up identity-unbound — a case #612's has-session liveness check does not catch.
Is it a known CC/tmux issue?
Partly. #606 documents a real CC regression family — stateful-MCP reconnect loops (2.1.147–2.1.152), session-resume memory, and "background sessions running on stale daemon before upgrade" (fixed 2.1.153) — the last of which directly intersects our
update_and_restart+ background-tmux flow. However, production now runs2.1.160(> 2.1.153), so those specific upstream bugs are patched. This orphan is therefore most likely the PinkyBot-side resume/identity-rebind gap, not an unpatched CC bug. (Web research in progress to confirm CC--continueSessionStart /.mcp.jsonre-read semantics — will update.)Open questions (being resolved)
SessionStarton a--continueresume, or only on fresh sessions? If only fresh, that alone explains why resumes never re-open the readiness gate / re-bind identity (smoking gun).--continue, does CC re-read.mcp.jsonand re-sendX-Agent-Name, or restore MCP client state from the transcript without re-sending headers? If the latter, a resume routes blank independent of the on-disk config.:8890gateway (blankX-Agent-Name) vs the/transport/transcript-pathhook endpoint (api.py:5238, 404s whenagents.get(name)is None).Proposed fix (multi-pronged)
Primary — identity-verify launch hook (sibling of #612). In
tmux_session.py:_spawn_tmux_repl/connect(), after a--continuespawn, add_verify_repl_identity_bound()(the identity analog of #612's_verify_repl_survived_launchliveness poll): confirm the agent's MCP identity re-bound within a grace window (e.g. the gateway sawX-Agent-Name == self.agent_name, and/orSessionStartre-POSTed transcript-path for this name). If unbound → kill, setforce_fresh_context_once=True, relaunch once fresh, re-verify; fail to DEAD if still unbound. Needs a small side-channel inshared_mcp.pyto record last-seenX-Agent-Nameper agent (no such signal exists today).Boot-loop self-heal.
api.py:on_startup(L8460): setforce_fresh_context_oncewhen the prior session was orphaned/blank (e.g. a last-shutdown manifest flag), so a plain restart self-heals instead of re-resuming.Fail-closed env.
tmux_session.py:_build_repl_env(L1861) /connect— refuse to spawn (or transition to BOOT_FAILED) whenself.agent_nameis blank, so a nameless session can never launch and route as blank. Guardagent_namenon-empty at_start_streaming_session(L2604) /_ensure_streaming_session(L2747) too.Gateway observability.
shared_mcp.py:AgentNameMiddleware(L152) — emit a structured signal / reject (logged 400) on blankX-Agent-Nameinstead of silently 404-ing per tool, so the daemon can correlate the blackout to a specific resumed session and auto-trigger the fallback.Secondary — restart-guard deadlock.
api.py:_build_restart_guard(L1754) /restart_streaming_session(L6984): when the agent's MCP self-tools are unreachable (so it cannot refreshsave_my_context), the staleness guard must not hard-block — downgrade to warn or add areason="tools_unreachable"bypass, and/or thread an explicitforce/bypass_guardparam via a newRestartStreamingRequestmodel (none exists today). Mirror #613'sforce_restart(bypass_guard=True)/bump_context_updated_at.Test plan (sketch)
Related
#612 (liveness sibling), #613 (poisoned-transcript sibling), #606 (CC binary / MCP-resume regression context), #623 (shared-MCP per-agent transport auth), #486 (explicit Transport state machine), #543 (SDK→tmux migration validation), #591 (stale saved-context across restarts).
🤖 Opened by Barsik