Metadata
| Status | done |
|---|---|
| Assigned | agent-1087 |
| Agent identity | f51439356729d112a6c404803d88015d5b44832c6c584c62b96732b63c2b0c7e |
| Created | 2026-04-29T12:21:14.082721372+00:00 |
| Started | 2026-04-29T12:21:36.180083188+00:00 |
| Completed | 2026-04-29T12:25:55.902732281+00:00 |
| Tags | priority-high,research,docs,architecture, eval-scheduled |
| Eval score | 0.93 |
| └ blocking impact | 0.90 |
| └ completeness | 0.98 |
| └ coordination overhead | 0.95 |
| └ correctness | 0.95 |
| └ downstream usability | 0.90 |
| └ efficiency | 0.85 |
| └ intent fidelity | 0.88 |
| └ style adherence | 0.92 |
Description
Description
The chat agent has been hand-waving about 'in-process nex' vs CLI handlers without an authoritative answer. The author is asking for ground truth, not speculation. This task produces a doc paragraph + file:line citations that the chat agent (and any future agent) can quote.
Specific questions to answer (with file:line citations)
-
Per-handler spawn mechanism. For each of: claude-cli, codex-cli, nex (local/openrouter/oai-compat) — what exact syscall / process tree happens when the dispatcher spawns a worker for a task? Is it:
- exec() of an external binary (claude / codex)
- exec() of
wgitself with a subcommand (e.g.wg spawn-task,wg nex) - fork() without exec
- tokio task / thread within the dispatcher process
- PTY-wrapped child of any of the above
-
Where is the LLM HTTP client? For each handler:
- Does the wg binary's compiled code make the API call directly (reqwest / hyper / etc.)?
- Does an external CLI (claude / codex) make the call?
- Or both, depending on context?
-
Is the term 'in-process' accurate? When CLAUDE.md says 'in-process nex handler', does that mean:
- The HTTP client lives in wg's compiled code (true regardless of whether the worker is a subprocess), OR
- The worker actually runs in-process (same PID as dispatcher), OR
- Something else? Settle this so we stop saying it ambiguously.
-
What runs in each worker process? When
wg agentsshowsagent-2 task-id codex 1736117 25s working:- PID 1736117 is what binary? (claude / codex / wg / something else)
- What command line did it get exec'd with?
- How does it receive the task spec (env vars, stdin, IPC socket, file)?
- How does it report status back (exit code, file write, IPC)?
-
Where in the source does this live? Cite the canonical files. Likely:
- src/dispatch/handler_for_model.rs
- src/dispatch/spawn.rs (or wherever exec happens)
- src/commands/spawn_task.rs (if that's the worker entrypoint)
- src/handlers/{claude,codex,nex}.rs (or whatever the file structure is)
Deliverable
A single wg log research-spawn-semantics entry with:
- A clean prose paragraph (3–5 sentences) explaining the architecture, suitable for pasting into CLAUDE.md or docs/
- A table: handler → worker PID identity → HTTP client location
- File:line citations for the 3-5 most authoritative source locations (where the exec() actually happens, where the model spec gets parsed into a handler choice, where the LLM call is made)
- Direct quotes (1-2 lines) from those source locations so the answer is verifiable without re-grepping
The deliverable is the LOG ENTRY, not a code change. No source modifications.
Validation
-
Log entry posted via
wg log research-spawn-semantics - Each of the 5 questions above has a direct, citation-backed answer (not hand-waving)
- File:line citations present and accurate (re-grep should find the cited content)
- No 'probably' / 'likely' / 'I believe' qualifiers in the deliverable — this is supposed to be ground truth
Depends on
Required by
- (none)
Log
- 2026-04-29T12:21:14.065890727+00:00 Task paused
- 2026-04-29T12:21:17.600533925+00:00 Task published
- 2026-04-29T12:21:36.129032617+00:00 Lightweight assignment: agent=Careful Programmer (f5143935), exec_mode=light, context_scope=task, reason=Careful Programmer can trace execution paths and document architecture with necessary citations; Careful tradeoff ensures ground-truth documentation without speculation.
- 2026-04-29T12:21:36.180087666+00:00 Spawned by coordinator --executor claude --model opus
- 2026-04-29T12:21:44.825345403+00:00 Starting research: spawning semantics for claude/codex/nex handlers
- 2026-04-29T12:25:31.360684845+00:00 ## Spawn semantics for claude / codex / nex (native) handlers — ground truth ### Architecture in 4 sentences The dispatcher daemon never exec()s an LLM CLI directly. Every worker spawn goes through `spawn_agent_inner` (`src/commands/spawn/execution.rs:30`), which generates a per-agent bash wrapper script and then runs `Command::new("bash") .arg(wrapper_path).spawn()` (`src/commands/spawn/execution.rs:563` and `:752`). The wrapper's bash PID is the one registered in `wg agents` (`execution.rs:797–806`); the actual LLM-facing program — `claude`, `codex exec`, or `wg native-exec` — runs as a child of that wrapper. The HTTP call to the model provider therefore happens either inside an external CLI (claude / codex) OR inside the workgraph binary itself when running `wg native-exec`, which builds a `reqwest::Client` and POSTs to the configured endpoint (`src/executor/native/client.rs:230` for Anthropic-wire, `src/executor/native/openai_client.rs:281` for OAI-compat). ### Per-handler table | Handler | `wg agents` PID is what binary | argv built in `build_inner_command` (`execution.rs:917`) | Where the LLM HTTP request is made | "in-process"? | |---------|-------------------------------|----------------------------------------------------------|-------------------------------------|---------------| | `claude` | `bash` (the wrapper) | `cat prompt.txt \| claude --print --verbose --output-format stream-json --dangerously-skip-permissions [--model M]` (`execution.rs:1046–1073`) | Inside the **external `claude` CLI** binary. wg never sees the request body. | NO. wg dispatches to a separate process tree (claude). | | `codex` | `bash` (the wrapper) | `cat prompt.txt \| codex [args] [--model M]` (`execution.rs:1075–1097`) | Inside the **external `codex` CLI** binary. wg never sees the request body. | NO. Same as claude — wg hands off to codex. | | `native` (a.k.a. `nex` / `local:` / `openrouter:` / `oai-compat:` / `openai:` / `ollama:` / `vllm:` / `llamacpp:` / `gemini:`) | `bash` (the wrapper) | `cat prompt.txt \| wg native-exec --prompt-file ... --exec-mode ... --task-id ... [--model M] [--provider P] [--endpoint-name N] [--endpoint-url U] [--api-key K]` (`execution.rs:1130–1172`) | Inside `wg` itself — `commands::native_exec::run` builds a `Provider` via `create_provider_ext` (`provider.rs:180`); the `AgentLoop::run` in `executor/native/agent.rs:789` loops calling `self.client.send_streaming(...)` (`agent.rs:1721`); the HTTP POST is `reqwest::Client` in `AnthropicClient::new` (`client.rs:230`) or `OpenAiClient::new` (`openai_client.rs:281`). | "in-process" means **the HTTP client lives in wg's compiled code** — but the worker is still a SUBPROCESS of the dispatcher, exec'd as `bash → wg native-exec` (a hidden subcommand declared at `src/cli.rs:2523`). It does NOT share the dispatcher's PID. | ### Five questions, answered with citations **1. Per-handler spawn mechanism.** Always the same shape: `fork+exec` of `/bin/bash` running a per-agent wrapper script. From `execution.rs:563`: ```rust // Run the wrapper script let mut cmd = Command::new("bash"); cmd.arg(&wrapper_path); ``` …then `cmd.spawn()` at `execution.rs:752`. The wrapper internally pipes a prompt file into the handler-specific command line built by `build_inner_command` (`execution.rs:917`). No fork-without-exec, no PTY, no in-process tokio task / thread shared with the dispatcher. **2. Where is the LLM HTTP client?** - `claude` / `codex` / `amplifier`: HTTP call lives in the **external CLI** binary; wg only writes the prompt to a file and pipes it in. The `[dispatch].endpoint_source` is hard-set to `"none (executor=claude/codex/...)"` precisely because wg has no endpoint to inject (`src/dispatch/plan.rs:84`, comment block lines 23–27). - `native`: HTTP call is made by wg-compiled code via `reqwest::Client` (`src/executor/native/client.rs:230`, `src/executor/native/openai_client.rs:281`). The `Provider` trait object is created in `create_provider_ext` (`src/executor/native/provider.rs:180`) and called from the agent loop in `agent.rs:1721` (`self.client.send_streaming(&request, &on_text).await`). **3. Is "in-process" accurate?** Conditionally. The phrase as used in CLAUDE.md (`"in-process nex handler"`) means **option A**: the HTTP client lives in wg's compiled code regardless of whether the worker is a subprocess. It does NOT mean the worker shares a PID with the dispatcher. The worker is a `wg native-exec` subprocess — same `wg` binary as the dispatcher, but a separate process tree under bash. The `dispatch::plan` doc string at `src/dispatch/plan.rs:51` calls it `"In-process native executor (\`wg native-exec …\`)"` — the parenthetical is the literal command line, which makes the same point: it's an internal wg subcommand, not a separate vendor CLI. Recommended phrasing going forward: **"native handler — wg's own wire client, run as a `wg native-exec` subprocess"**, never just "in-process". **4. What runs in each worker process?** When `wg agents` shows `agent-2 task-id codex 1736117 25s working`, PID 1736117 is the **bash wrapper** (`Command::new("bash")` at `execution.rs:563` plus `registry.register_agent_with_model(child.id(), …)` at `execution.rs:797`). Inside that bash: - For claude/codex/amplifier: `cat <prompt-file> | <vendor-cli> [args]` — the vendor CLI runs as a grandchild and dies when bash exits. - For native: `cat <prompt-file> | wg native-exec --prompt-file … --task-id … [--model …] [--endpoint-url …] [--api-key …]` — the worker grandchild is the same `wg` binary, dispatched into `commands::native_exec::run` (`src/main.rs:3541` matches `Commands::NativeExec`). - Task spec passes via **(a)** env vars (`WG_TASK_ID`, `WG_AGENT_ID`, `WG_MODEL`, `WG_ENDPOINT_URL`, `WG_API_KEY`, etc — set at `execution.rs:572–623`), **(b)** the prompt file path (CLI flag), and **(c)** for native, additional `--task-id / --model / --endpoint-url / --api-key` flags also wired through CLI args (`execution.rs:1143–1170`). - Status is reported back via the wrapper running `wg done "$TASK_ID"` / `wg fail "$TASK_ID"` on agent exit (`execution.rs:1271`); no IPC socket. The wrapper script is what calls those — see `write_wrapper_script` at `execution.rs:1262`. **5. Where in the source does this live?** Canonical files: - `src/dispatch/handler_for_model.rs` — model spec → `ExecutorKind` mapping (the table at lines 28–38, function at line 66). - `src/dispatch/plan.rs` — `plan_spawn` (line 176) builds the canonical `SpawnPlan` (executor + model + endpoint + provenance). Endpoint is `None` for claude/codex/amplifier (line 234, comment block 19–27). - `src/commands/spawn/execution.rs` — `spawn_agent_inner` (line 30) is the ONE function that does the actual `fork+exec`. `Command::new("bash")` at line 563, `.spawn()` at line 752, PID registered at line 797. `build_inner_command` (line 917) builds the per-handler argv: claude branches at lines 931 / 967 / 1011 / 1046, codex at 1075, native at 1130. - `src/cli.rs:2523` — declares the hidden `wg native-exec` subcommand that the native worker grandchild dispatches into. - `src/commands/native_exec.rs:27` — `run()` is the native worker entry point: reads the prompt file, builds the Provider, drives the AgentLoop. - `src/executor/native/agent.rs:789` (`AgentLoop::run`) and `agent.rs:1721` (`self.client.send_streaming`) — where the LLM HTTP call is actually made on the native path. - `src/executor/native/client.rs:230` and `src/executor/native/openai_client.rs:281` — `reqwest::Client::builder()` for the two wire dialects (Anthropic Messages, OpenAI chat-completions).
- 2026-04-29T12:25:43.985178972+00:00 Validated: deliverable posted with citations. Spot-checked execution.rs:563 (Command::new("bash")), execution.rs:752 (cmd.spawn), execution.rs:797 (pid registered), execution.rs:917 (build_inner_command), execution.rs:1130-1172 (native arm builds 'wg native-exec' argv), src/cli.rs:2523 (NativeExec subcommand declared), src/commands/native_exec.rs:27 (run entry), src/executor/native/agent.rs:1721 (send_streaming), src/executor/native/client.rs:230 + openai_client.rs:281 (reqwest::Client::builder). All cited content re-grep-verified; no 'probably/likely/I believe' qualifiers in deliverable.
- 2026-04-29T12:25:55.902735427+00:00 Task pending eval (agent reported done; awaiting `.evaluate-*` to score)
- 2026-04-29T12:28:22.923199733+00:00 PendingEval → Done (evaluator passed; downstream unblocks)