v0.1.9 — Codex Responses parity · analysis-paralysis fix · MCP coverage · macOS screenshots
Note: this release was re-cut on 2026-05-21 to include the trajectory-recording fixes for subagents (#112, #114, closes #33). If you downloaded
grafffrom the original v0.1.9 binary on 2026-05-20, please re-install — your/tracewon't walk subagent trees correctly.
Highlights
- Codex Responses-API parity round 1–3 (#66 → #106): wire-level parity with upstream
openai/codexfor the chatgpt.com Codex backend across body fields (parallel_tool_calls,client_metadatawith installation/window IDs + W3C trace context), SSE event coverage (output_item.done,reasoning_summary_part.added), structured 429 envelopes (UsageLimitReached/UsageNotIncluded), reactive 401 → token refresh, and proactive refresh-before-expiry. ReadWithoutWriteDetector(#109, closes #27 P0): new request-phase orchestrator hook that catches the analysis-paralysis pattern — re-reading the same files without writing code — and injects a forcing-function reminder.- Parallel agent dispatch — and observability for it (#112, #114, closes #33): the orchestrator has long fanned
Tasktool calls out in parallel viafutures::join_all, but until this release the child agents' work was invisible to/trace. The trajectory recorder now records every dispatched child under the root'sconversation_idwithparent_agent_idlinked to its dispatcher, so a single/trace <root_conversation_id>walks the whole fan-out tree. See Spotlight: parallel agent dispatch below. - macOS screenshot drag-drop (#52, closes #51): TUI now correctly recognises temporary screenshot paths and
file://URLs and attaches them as images instead of pasting raw text. - MCP completeness (#108, closes #26): full content-variant coverage (Audio, ResourceLink, Resource, structuredContent, output_schema/annotations/title metadata) on the rmcp adapter.
- Credentials hardening (#69):
~/forge/credentials.jsonis nowchmod 0o600after writes. - WS protocol pin (#66):
OpenAI-Beta: responses_websockets=2026-02-06header for the chatgpt.com WS upgrade.
Spotlight: parallel agent dispatch
graff has three layers of parallelism for sub-agents, and v0.1.9 closes the missing one (observability). They compose as follows:
Layer 1 — wire-level: the LLM emits multiple tool calls per turn
By default we send parallel_tool_calls: true to OpenAI / Codex / Anthropic, and the per-model supports_parallel_tool_calls capability in forge_domain/src/agent.rs declares which models accept it:
// crates/forge_app/src/dto/openai/request.rs:408
parallel_tool_calls: Some(true), // transformers downgrade if a model
// doesn't support itThis was wired across the Codex Responses backend in #95 (round-2 parity, backported via #100). Without it the model emits one tool call per turn and there's no parallelism to dispatch.
Layer 2 — orchestrator: fan Task calls out, run the rest sequentially
Orchestrator::execute_tool_calls partitions the tool calls the model emitted into Task (dispatch-a-subagent) versus everything else. Task calls run concurrently via futures::join_all; everything else stays sequential so the UI notifier handshake and per-tool hooks behave the same as before:
// crates/forge_app/src/orch.rs:108–135
let (task_calls, other_calls): (Vec<_>, Vec<_>) =
tool_calls.iter().partition(is_task_call);
// record dispatches on parent's trajectory *before* kicking them off
if let Some(recorder) = &self.trajectory_recorder {
for tc in &task_calls {
recorder.record_tool_call(tc).await;
}
}
let task_results = join_all(
task_calls.iter().map(|tc|
self.services.call(&self.agent, tool_context, (*tc).clone())
),
).await;When ≥ 2 tool calls land in one assistant turn the REPL surfaces them with a banner so you can see the batch as a group:
⇉ 3 parallel tool calls (2× Task, read)
Layer 3 — observability: child events under the root's conversation
This is the piece v0.1.9 adds. Before #112/#114, Task dispatch trajectories looked like this: the parent's view of the dispatch (tool_call + tool_result rows for the Task itself) recorded fine, but every child agent's internal tool calls were dropped on the floor because AgentExecutor::execute constructed a fresh ToolRegistry via Services::call(...) with no trajectory repo threaded in.
PR #111/#112 plumbed the repo through ForgeApp::tool_registry, but the orchestrator's actual dispatch path goes via services.call(...) which builds a fresh registry per call through the blanket AgentService::call impl — so the recorder never reached the children in production.
PR #113/#114 (this release) finishes the wiring:
- adds
Services::trajectory_repo()so the blanketAgentService::callimpl can thread the repo into the per-callToolRegistry, and - threads
parent_conversation_idthroughToolCallContextso the child agent's events land under the root'sconversation_id.
What /trace <root_id> now looks like for a parent that fanned out 3 Task calls in parallel:
0 run agent=forge
1 call task agent=forge ⇉ dispatched in parallel
2 call task agent=forge ⇉ dispatched in parallel
3 call task agent=forge ⇉ dispatched in parallel
0 run agent=sage parent=forge
1 call read agent=sage
2 result read agent=sage duration=1ms
3 end agent=sage
0 run agent=grep parent=forge
1 call grep agent=grep
2 result grep agent=grep duration=43ms
3 end agent=grep
0 run agent=read parent=forge
1 call read agent=read
2 result read agent=read duration=2ms
3 end agent=read
4 result task agent=forge duration=7984ms
5 result task agent=forge duration=210ms
6 result task agent=forge duration=87ms
7 end agent=forge
The three children are properly nested under the parent and timestamped independently, so you can see at a glance which fork dominated the latency budget.
Verifying it on your own runs
In the REPL:
/trace 20 # last 20 events on the current conversation
/trace all # whole tree, walks subagent dispatches
Pipe-friendly from the shell (e.g. for grepping or diffing across runs):
graff conversation list # find the root id
graff conversation trace <root_conversation_id> # mirrors /traceDirect against the SQLite store:
-- ~/forge/.forge.db
SELECT seq, kind, agent_id, parent_agent_id
FROM trajectory_events
WHERE conversation_id = '<root_conversation_id>'
ORDER BY id;You should see one root-agent run plus N child-agent runs, each carrying parent_agent_id linked back to its dispatcher, and child rows interleaving with the parent's tool_call/tool_result rows in seq order.
🚀 Features
- feat(orch): ReadWithoutWriteDetector hook for analysis-paralysis loops (closes #27) (#109)
- feat(openai-responses): inject W3C trace context into client_metadata (closes #104) (#106)
- feat(openai-responses): per-model
default_reasoning_level+prefer_websocketsmetadata (closes #102 / #103) (#105) - feat(provider): proactive OAuth refresh-before-expiry on credential load (closes #89) (#99)
- feat(openai-responses): reactive 401 → token refresh + retry on Codex backend (closes #88) (#98)
- feat(openai-responses): parse 429
UsageErrorResponseenvelope from Codex backend (closes #90) (#97) - feat(openai-responses): handle
output_item.done+reasoning_summary_part.addedSSE events (closes #93 / #94) (#96) - feat(openai-responses): send
parallel_tool_calls+ Codexclient_metadatabody fields (closes #91 / #92) (#95) - feat(openai-responses):
x-codex-window-id+ opt-in timing metrics (#83) - feat(openai-responses): per-model
default_verbosity/support_verbosityfor Codex (#81) - feat(providers): add
codex-auto-reviewto Codex catalog (#79) - feat(openai-responses): send Codex identity headers (
originator,x-codex-installation-id) on chatgpt.com requests (#77) - feat(openai-responses): wire
text.verbosityfor gpt-5.x Codex models (#75)
🐛 Bug Fixes
- fix(trajectory): record subagent runs under parent's conversation_id (closes #33) (#112, #114)
- fix(openai-responses): pin Codex Responses WS protocol via
OpenAI-Beta(#66) - fix(security): chmod credentials file to
0o600after write (closes #68) (#69) - fix(openai-responses): Codex header fidelity bugs from deepwiki audit (#85)
- fix: support macOS screenshot image drops (closes #51) (#52)
🧰 Maintenance
- test(mcp): cover Audio + ResourceLink JSON fallback paths (closes #26) (#108)
- fix(ci): enable pixo simd for linux coverage builds (#71)
- fix(test): refresh stale tool-description snapshots (#73, #74)
Closed issues
#6, #9, #11, #26, #27, #33, #51, #65, #68, #88, #89, #90, #91, #92, #93, #94, #102, #103, #104
What's deferred
The OpenAI/Codex parity tracker (#65) closed with these still-open follow-ups for the orchestrator-level work:
- 4 remaining Codex identity headers (
x-codex-turn-state,x-codex-turn-metadata,x-codex-parent-thread-id,x-openai-subagent) — need turn-descriptor + subagent-context propagation /responses/compactand/memories/trace_summarizeendpointsAgentIdentityauth mode
Install
Recommended (POSIX shell installer, auto-detects OS + arch + libc):
curl -fsSL https://github.com/justrach/codegraff/releases/download/v0.1.9/install.sh | shSupported binary downloads
| Platform | graff |
codegraff |
|---|---|---|
| macOS arm64 (Apple Silicon) | graff-aarch64-apple-darwin | codegraff-aarch64-apple-darwin |
| Linux x86_64 (glibc) | graff-x86_64-unknown-linux-gnu | codegraff-x86_64-unknown-linux-gnu |
| Linux x86_64 (musl, static) | graff-x86_64-unknown-linux-musl | codegraff-x86_64-unknown-linux-musl |
| Linux aarch64 (glibc) | graff-aarch64-unknown-linux-gnu | codegraff-aarch64-unknown-linux-gnu |
| Linux aarch64 (musl, static) | graff-aarch64-unknown-linux-musl | codegraff-aarch64-unknown-linux-musl |
| Windows x86_64 (MSVC) | graff-x86_64-pc-windows-msvc.exe | codegraff-x86_64-pc-windows-msvc.exe |
| Windows aarch64 (MSVC) | graff-aarch64-pc-windows-msvc.exe | codegraff-aarch64-pc-windows-msvc.exe |
CodeDB-bundled tarballs for the Linux-x86_64 line are also available as graff-x86_64-unknown-linux-{gnu,musl}-bundle.tar.gz.
Signing / notarization status
| Platform | Status |
|---|---|
| macOS arm64 | Codesigned (Developer ID WWP9DLJ27P, hardened runtime, RFC 3161 timestamp) and notarized via Apple notary service. graff submission a380a8cf-eb76-4bce-a4e9-ced2d7664e04, codegraff submission 7f2fbb30-d2da-4c36-9e39-1b953b1dd34d (both Accepted) |
| macOS x86_64 (Intel) | Not shipped in this release — will be added once CODEDB_LOCAL_APPLE_* GitHub Secrets are configured for CI signing |
| Linux + Windows | Unsigned (binaries are produced via the Multi Channel Release workflow on ubuntu-latest / windows-latest runners) |
Build provenance
- macOS arm64: locally built + signed + notarized on the maintainer's developer workstation from this tag's commit
- Linux + Windows: built by this CI run via the
Multi Channel Releaseworkflow (introduced in #115)
Verification
graff --version # → graff 0.1.9
codesign -dvv $(which graff) # macOS only → Authority=Developer ID Application: Rachit Pradhan (WWP9DLJ27P)
Known gaps
x86_64-apple-darwin(Intel mac) is not in this release — local sandboxed-keychain hiccup mid-build, deferred to a follow-upaarch64-linux-androidbuild failed becausearboard(clipboard library used inforge_main) does not compile on Android — needs a#[cfg(not(target_os = "android"))]guard, tracked as a follow-up