Skip to content

Agent-pane Claude (and Codex) sessions don't trigger wt-agent-hooks — registry stays uninitialized #48

Description

@DDKinger

📌 Updated 2026-05-25: a lighter phased approach (presence-only first cut, full ACP translation as follow-up) is proposed in this comment. The body below documents the original full-Plan-C analysis for reference.


Agent-pane Claude (and Codex) sessions never trigger wt-agent-hooks — session registry stays uninitialized

Summary

When the agent pane CLI is set to Claude (or Codex), wt-agent-hooks events never fire for those sessions. The SessionRegistry therefore receives no SessionStarted / ToolStarting / ToolCompleted updates, and downstream UI surfaces (tab status badge, F2 list) miss state changes that work correctly for agent-pane Copilot / Gemini.

For Copilot and Gemini in agent pane, hooks fire normally and everything works. The root cause is structural and specific to adapter-based CLIs.

Reproduction

  1. Open an agent pane with Claude as the CLI.
  2. Send a prompt; let it run a tool call.
  3. Observe:
    • A row is appended to %LOCALAPPDATA%\IntelligentTerminal\agent-pane-sessions.jsonl (good — the wta-side bookkeeping records it).
    • No corresponding line appears in %LOCALAPPDATA%\IntelligentTerminal\logs\hook-trace.log for that session id (no ENTER, no OK, no ERROR).
  4. Switch to a normal tab, run claude directly, send a prompt: hook-trace.log now records ENTER + OK lines as expected.

Concrete example from my machine (today):

agent-pane-sessions.jsonl (today):
  04bf17d7-55ed-4f50-b26e-3a98021a2899
  ab05fa1c-bb10-4838-a625-49a5a2b7351e

hook-trace.log (today, cli=claude entries):
  sessId=27152147  ← the *tab* Claude, not agent-pane

The two agent-pane session IDs do not appear in hook-trace.log at all. Same machine, same install, same wt-agent-hooks plugin — works in tab, never fires in agent pane.

Root cause

Agent-pane Claude does not run the real claude CLI binary. The agent registry profile (tools/wta/src/agent_registry.rs:95-112) sets:

AgentProfile {
    id: "claude",
    ...
    acp_launch_command: "npx -y @zed-industries/claude-code-acp",
    ...
}

with the explanatory comment:

"Claude CLI itself doesn't speak ACP. We launch the Zed-maintained adapter via npx; npm-installed claude shim implies node/npx are present, so this works whenever delegate mode does."

So in agent pane we run:

wta.exe --owner-tab-id "{...}" --agent "npx -y @zed-industries/claude-code-acp"
└─ cmd /c npx.cmd -y @zed-industries/claude-code-acp
    └─ node @zed-industries/claude-code-acp/dist/index.js   ← the "Claude" agent pane talks to
        └─ node @anthropic-ai/claude-agent-sdk/cli.js       ← driven via SDK, NOT Claude Code CLI

The @anthropic-ai/claude-code binary (the one that loads ~/.claude/plugins/...) never runs.

Inspecting the Zed adapter source (@zed-industries/claude-code-acp v0.16.2):

  • It does call the Claude Agent SDK with settingSources: ["user", "project", "local"] (dist/acp-agent.js:777), so a user putting hook definitions directly into ~/.claude/settings.json would have them honored.
  • It does not pass plugins: to the SDK (grep -i 'plugins\s*:' dist/ returns no matches). The SDK's plugin-loading machinery (@anthropic-ai/claude-agent-sdk/sdk.d.ts:721-734) is only invoked when the host explicitly passes a SdkPluginConfig[] — there is no auto-discovery of ~/.claude/plugins/installed_plugins.json.

Result: wt-agent-hooks is installed via Claude's plugin marketplace mechanism, so the adapter never loads it. Codex has the same architecture (agent_registry.rs:113-128: acp_launch_command: "npx -y @zed-industries/codex-acp") and presumably the same problem.

Why this matters

WTA has two parallel paths that populate SessionRegistry:

  1. Hook path: wt-agent-hooks → PowerShell → wtcli send-eventapp.rs::handle_send_event (lines 370-440) → reg.apply(SessionEvent::*).
  2. ACP path: agent process → session/update notification → client.rs::session_notification (line 1491) → AppEvent::ToolCall → consumed in app.rs:2924-2954 to update agent-pane UI state onlySessionRegistry is never touched.

So for agent-pane Claude/Codex, the registry receives zero events from either path. UI surfaces that read from the registry (tab status badge, F2 list, status persistence) display incorrect or missing state.

Proposed fix — Plan C: synthesize hook events from ACP traffic in wta

Translate ACP session_notification and new_session events into SessionEvent variants and feed them into SessionRegistry via a new AppEvent::SyntheticSessionEvent(SessionEvent). Treat ACP as a third ingress path equivalent to the hook path.

Why not just fix the hook side

Two simpler alternatives were considered and rejected:

  • Plan A — install hooks via ~/.claude/settings.json instead of plugin: Works for Claude only (adapter does honor settingSources), doesn't help Codex, conflicts with users' own settings.json edits.
  • Plan B — have wta push hooks to the adapter via ACP _meta.claudeCode.options.hooks: Adapter-specific, doesn't generalize to other CLIs that might use different adapter patterns in the future.

Plan C generalizes to any ACP agent (current adapter-based ones + any future ones) without depending on third-party adapter implementation details.

Scope (this issue)

  • Claude (agent pane) only. Codex is structurally the same but CliSource enum (agent_sessions.rs:34-40) doesn't have a Codex variant; adding it is a separate ripple-change ticket.
  • Native ACP agents (Copilot, Gemini) get is_agent_pane && is_adapter_based gate set to false → no change for them; their existing hook path keeps working as-is.

Key design points (validated against the code)

  1. Adapter detection must use canonical agent_id, NOT agent_cmd string parsing. WT passes --agent-id (TerminalPage.cpp:1829-1840) and main.rs reads it (main.rs:1671-1684), but it isn't currently threaded into run_acp_client (main.rs:1626-1635). After resolve_agent_cmd (app.rs:5925-5940) the cmd string can be a fully-resolved absolute path that fails resolve_agent_id_from_cmd's prefix matcher (agent_registry.rs:216-232). Thread the id explicitly.

  2. Per-session in-flight HashSet<tool_call_id> to avoid premature Idle. SessionEvent::ToolStarting/ToolCompleted are tool-id-less (agent_sessions.rs:123-124), but ACP runs concurrent tool calls each with its own id. Emit registry ToolStarting only on 0→1, ToolCompleted only on 1→0. Otherwise a completion for tool A flips the session Idle while tool B is still running.

  3. SessionOrigin::AgentPane stamping on the app-side handler. The hook path stamps origin in app.rs:449-460. A raw reg.apply(ev) leaves origin as Unknown, breaking the SessionStopped(reason="complete") lifecycle at agent_sessions.rs:373-390. Add an apply_with_agent_pane_origin helper that mirrors the hook path.

  4. Pane GUID from wta's own WT_SESSION env. wta runs as the agent pane's process; ConPTY sets WT_SESSION for every child (ConptyConnection.cpp:56-62; matches TerminalPage.cpp:4794-4809). Read once in main.rs, thread through to ACP runner.

  5. Key equality verification first, dedup later. Hook path keys by agent_session_id from hook JSON; synthetic path keys by ACP session_id. If they differ for the same logical Claude session, hook + synthetic events create competing rows for the same pane (and drop_synthetic_for_pane at agent_sessions.rs:589-600 only cleans pane:-prefixed keys). Add tracing::info! for one-shot manual verification before deciding whether dedup is needed.

Files

File Change
tools/wta/src/main.rs thread WT_SESSION + canonical agent_id into ACP runner
tools/wta/src/protocol/acp/client.rs new params; per-session in-flight set; emit synthetic events in session_notification + after new_session
tools/wta/src/app.rs new AppEvent::SyntheticSessionEvent variant + apply_with_agent_pane_origin helper
tools/wta/src/agent_registry.rs pub fn is_adapter_based(profile_id: &str) -> bool helper
Tests in client.rs gate + in-flight transitions

Rough size: ~120-180 lines net. No public API changes.

Verification plan

  1. Build, repro: badge stays wrong for agent-pane Claude.
  2. Apply fix, retry: badge transitions Idle → Working → Idle.
  3. Cross-check no regression: agent-pane Copilot/Gemini still work (no double events).
  4. Cross-check no regression: tab Claude still works (hook path still updates registry, synthetic events not emitted).
  5. Inspect wta-main.log for acp_synthetic_hook trace entries during the test.

Follow-up tickets (not this issue)

  • Add CliSource::Codex to agent_sessions.rs:34-40 + cascade through parse/filter/history/resume so Codex agent-pane gets the same coverage.
  • If key-equality verification (point 5) shows ACP id != hook id for Claude, implement dedup (extend drop_synthetic_for_pane or maintain an alias map).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-BugSomething is broken or behaves incorrectlyIssue-FeatureNew capability or enhancement requestNeeds-TriageAwaiting first review by the team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions