Metadata
| Status | done |
|---|---|
| Assigned | agent-961 |
| Agent identity | f51439356729d112a6c404803d88015d5b44832c6c584c62b96732b63c2b0c7e |
| Model | claude:opus |
| Created | 2026-04-28T21:54:23.609586152+00:00 |
| Started | 2026-04-28T22:02:12.782645228+00:00 |
| Completed | 2026-04-28T22:15:38.415092753+00:00 |
| Tags | bug,tui,chat, eval-scheduled |
| Eval score | 0.90 |
| └ blocking impact | 0.90 |
| └ completeness | 0.95 |
| └ coordination overhead | 0.95 |
| └ correctness | 0.90 |
| └ downstream usability | 0.92 |
| └ efficiency | 0.85 |
| └ intent fidelity | 0.89 |
| └ style adherence | 0.95 |
Description
Description
User reports: in the TUI new-chat dialog, selecting 'codex' as the executor/model launches a claude session instead. The selection is being dropped or the wrong handler is dispatched.
Symptom 2 (likely same code path): when launching the new chat, the TUI flips focus back to the previous chat first, then later switches to the newly-launched chat once it's ready. Jarring two-step focus transition.
Goal
Find the root cause(s) for both symptoms. Likely candidates to investigate:
- The new-chat dialog handler in the TUI — where it reads the selected executor/model and what it actually passes to chat-launch / IPC.
- IPC: NewCoordinator / launch-chat command — is the model field being honored, or is dispatcher.model / coordinator.model overriding?
- Focus state machine: where does the TUI decide which chat tab is active after launch? Is there a default 'switch to most recent done chat' that fires before the new chat is ready?
- Recent commit 9817f36dd (tui-chat-bar) and fix-tui-new are recent and may be relevant.
Validation
- Root cause identified for symptom 1 (wrong launcher selected). Specific file + function + line range.
- Root cause identified for symptom 2 (focus jumps to prev chat then to new chat). Specific file + function + line range.
- Confirmed whether one fix or two are needed.
- Reproduction steps documented (so the fix task can verify).
- Diagnosis posted as task log entries (wg log) — not a separate file.
Depends on
Required by
Log
- 2026-04-28T21:54:23.549729692+00:00 Task paused
- 2026-04-28T21:57:19.084945024+00:00 Task published
- 2026-04-28T22:02:12.782650788+00:00 Spawned by coordinator --executor claude --model opus
- 2026-04-28T22:02:28.826837838+00:00 Starting diagnosis. Inspecting recent commits 9817f36dd (tui-chat-bar) and fix-tui-new for relevant code paths.
- 2026-04-28T22:05:05.032584401+00:00 Traced launcher flow: launch_from_launcher (state.rs:12904) builds args, runs CommandEffect::CreateCoordinator. IPC writes coordinator-state-N.json with executor_override/model_override (ipc.rs:1525-1530). Supervisor at coordinator_agent.rs:744-790 reads overrides each iteration; correctly layered through plan_spawn.
- 2026-04-28T22:09:34.171677976+00:00 Confirmed model selection bug: spawn_task::resolve_handler at src/commands/spawn_task.rs:208 calls plan_spawn(task, config, env_executor.as_deref(), None) — passes None as default_model. Supervisor sets WG_MODEL but spawn_task doesn't read it. The chat task in graph has no model field. So plan_spawn falls back to config.coordinator.model (likely 'claude:opus' default). Result: launcher's model selection is silently dropped at the spawn-task layer.
- 2026-04-28T22:14:15.942371735+00:00 Found additional path: config.agent.model defaults to 'claude:opus' (src/config.rs:3369). When spawn_task subprocess re-runs plan_spawn without WG_MODEL, model falls through to step 5 ([agent].model fallback) = 'claude:opus'. So user-selected model is silently replaced by claude:opus regardless of executor.
- 2026-04-28T22:14:44.004468934+00:00 === DIAGNOSIS: SYMPTOM 1 — wrong executor/model === ROOT CAUSE A (model is silently dropped at spawn-task layer): File: src/commands/spawn_task.rs Function: resolve_handler Lines: 207-208 Problem: WG_MODEL env var is set by the supervisor (coordinator_agent.rs:818-820) but resolve_handler ONLY reads WG_EXECUTOR_TYPE; it never sources WG_MODEL. The call is plan_spawn(task, &config, env_executor.as_deref(), None). The fourth arg (default_model) is hardcoded None. Effect: chat tasks have no model field in graph.jsonl (create_chat_in_graph at ipc.rs:1466-1492 does not set Task.model). So plan_spawn cascade hits step 5 — config.agent.model fallback — which defaults to 'claude:opus' via default_model() at src/config.rs:3369. The user's launcher selection is never read by the worker subprocess. Provenance evidence: the SpawnPlan provenance line emitted by spawn_task at line 213 will show 'model_source = [agent].model (fallback)' rather than tracing back to launcher selection. ROOT CAUSE B (model picker may default-select claude model after executor swap): File: src/tui/viz_viewer/state.rs Function: filter_models_for_executor + refresh_model_filter_for_executor Lines: 1082-1103, 1115-1128 Problem: filter_models_for_executor only REORDERS models (compatible-first), never filters out incompatible ones. When user selects 'codex' executor, the model picker is rebuilt and selection resets to 0. If the registry lacks any openai/gpt models, position 0 may still be a 'claude:*' model. So user thinks they picked a codex-compatible model but actually submits codex+claude:opus. EXECUTOR FLOW IS CORRECT (for the lazy-spawn path): - launch_from_launcher (state.rs:12930-12932) always passes --executor. - create_chat_in_graph (ipc.rs:1525-1530) writes coordinator-state-N.json with executor_override='codex'. - Lazy-spawn at mod.rs:2520-2540 reads state.executor_override and passes it to CoordinatorAgent::spawn. - subprocess_coordinator_loop reads state.executor_override on every iteration (coordinator_agent.rs:744-757) so the executor self-corrects each loop. - WG_EXECUTOR_TYPE=codex is set on the subprocess (line 814). - spawn_task::resolve_handler reads WG_EXECUTOR_TYPE → plan.executor = Codex → dispatch_codex. So the actual executor process IS codex-handler, not claude-handler. The 'launches a claude session' user perception is most likely the user observing the model spec 'claude:opus' (from the silent fallback) appearing in handler logs/output and concluding it's a claude session, when in fact codex-handler is running with a Claude-flavored model name (which the codex CLI either errors on or substitutes silently). ALSO RELEVANT (lower priority — boot path bug): File: src/commands/service/mod.rs:2168-2174 At daemon boot, enumerate_chat_supervisors_for_boot calls CoordinatorAgent::spawn with daemon_cfg.executor / daemon_cfg.model — NOT per-coord state.executor_override. The supervisor loop self-corrects on first iteration, but the synchronous CoordinatorAgent::spawn does call plan_spawn at coordinator_agent.rs:364 with the wrong executor for the 'uses_subprocess' decision and the is_claude_available() check at line 386.
- 2026-04-28T22:15:04.042155551+00:00 === DIAGNOSIS: SYMPTOM 2 — focus jumps to previous chat then to new chat === ROOT CAUSE: Launcher closes synchronously, but coordinator-creation IPC is async — there is a visible window where the previous chat is on-screen. File 1: src/tui/viz_viewer/state.rs Function: launch_from_launcher Lines: 12904-12950 Behavior: - line 12909: self.input_mode = InputMode::Normal — closes launcher visually IMMEDIATELY. active_coordinator_id is unchanged (still points at the chat the user was on before opening launcher). - line 12950: self.exec_command(args, CommandEffect::CreateCoordinator) fires the 'wg service create-coordinator' subprocess in a background thread (exec_command at line 9631-9667 std::thread::spawn). - Frames between launcher-close and IPC-completion render the previous chat (no state change happened locally). User sees the OLD chat 'flip in' visually. File 2: src/tui/viz_viewer/state.rs Function: drain_commands → CommandEffect::CreateCoordinator branch Lines: 9756-9788 Behavior on success (after IPC returns): - line 9761: self.force_refresh() - line 9762: self.switch_coordinator(cid as u32) ← FOCUS JUMPS HERE - line 9763: self.right_panel_tab = RightPanelTab::Chat Net visible sequence: launcher closes → ~previous-chat visible (1 to N frames, depending on IPC latency: graph load + write coord-state file + return) → drain_commands fires → switch_coordinator(new_cid) → new chat visible. The two-step transition the user reports is exactly this. Why latency is non-trivial: the IPC roundtrip in handle_create_coordinator (ipc.rs:1438-1533) does: 1. Load full graph from .workgraph/graph.jsonl 2. Read config 3. Compute next_id (scans graph + chat dirs) 4. Build new Task 5. modify_graph (re-load + atomic-write graph) 6. Write coordinator-state-N.json 7. Record launcher_history entry Plenty of disk I/O — easily a few frames of UI. NOT THE CAUSE (ruled out): - sync_active_tabs_from_graph (state.rs:13165) only switches if active_coordinator_id is dropped — doesn't apply here since the user's chat still exists. - check_new_task_focus (state.rs:5659) skips dot-prefixed tasks (notify_new_task_focus at commands/mod.rs:209 not invoked for .chat-N — see ipc.rs:1284-1287 explicit '!task_id.starts_with(".")' guard). - sync_coordinator_from_selection only fires on user-initiated selection changes.
- 2026-04-28T22:15:27.870587722+00:00 === ONE FIX OR TWO? === TWO distinct fixes needed. The two symptoms share NO code path — they happen to co-occur because both are triggered by the same user action (Launch in the new-chat dialog), but the mechanisms are independent: - Symptom 1 (executor/model): bug is in the spawn-task subprocess, two layers below the TUI. Fix: src/commands/spawn_task.rs:208 — read WG_MODEL and pass it as default_model to plan_spawn. Optionally also fix filter_models_for_executor to FILTER (not just reorder) to avoid silent claude-model selection on codex executor. - Symptom 2 (focus jump): bug is purely in the TUI command-effect handler. Fix candidates: (A) Show a placeholder/loading state for the new chat tab BEFORE the IPC returns: append an 'optimistic' tab and switch to it immediately, replace its id with the real cid when IPC returns. (B) Don't close the launcher until IPC completes (set input_mode=Normal in drain_commands instead of in launch_from_launcher; show a 'Creating chat...' spinner in the launcher pane during the wait). (C) Hide the chat panel entirely (right_panel_tab = some neutral state) until the new chat is ready. (B) is closest to existing UX patterns; (A) is the cleanest visually. === REPRODUCTION STEPS (for the fix-tui-new task) === Symptom 1: 1. Fresh install: rm -rf .workgraph; wg init (defaults give config.coordinator.executor=None, model=None). 2. Start daemon: wg service start. 3. Launch TUI: wg tui. 4. Press '+' or Ctrl+N to open the launcher. 5. With launcher open, navigate to Executor section (Tab), select 'codex' (j/k keys). 6. Optionally pick a specific codex/openai-prefixed model (if registry has any). If registry only has claude:* models, the picker default-selects claude:opus — that itself is the symptom-1b path. 7. Press Enter. 8. Send a message in the new chat to trigger lazy-spawn of the supervisor. 9. Inspect: cat .workgraph/service/coordinator-state-{N}.json — confirm executor_override='codex'. 10. Check daemon log .workgraph/service/daemon.log for the line '[spawn_task] .chat-{N}: SpawnPlan executor=codex (from agency.effective_executor), model=claude:opus (from [agent].model (fallback)), endpoint=...' — this is the smoking gun: model_source = '[agent].model (fallback)' instead of tracing back to the user's launcher selection. 11. Confirm dispatch_codex was called by looking for 'wg codex-handler' in the daemon's spawned-process log; the model passed is 'claude:opus' regardless of what the user picked. Symptom 2: 1. Have at least one existing chat (e.g., chat-0, the bootstrap chat). 2. With chat-0 active and visible in the right panel. 3. Press '+' to open launcher. 4. Pick any executor/model. 5. Press Enter. 6. Observe: launcher closes IMMEDIATELY → chat-0 (the previous chat) is visible for ~100ms-500ms → focus snaps to the new chat. === SUMMARY === - Symptom 1: spawn_task.rs:208 ignores WG_MODEL → user model dropped → falls back to claude:opus default; appears as a claude session. - Symptom 2: launch_from_launcher closes launcher synchronously, IPC roundtrip is async, drain_commands does switch_coordinator only after IPC returns → visible two-step focus transition. - Two fixes needed. No shared code path.
- 2026-04-28T22:15:38.415098353+00:00 Task pending eval (agent reported done; awaiting `.evaluate-*` to score)
- 2026-04-28T22:19:00.610305931+00:00 PendingEval → Done (evaluator passed; downstream unblocks)