Skip to content

v0.1.9 — Codex Responses parity · analysis-paralysis fix · MCP coverage · macOS screenshots

Choose a tag to compare

@justrach justrach released this 20 May 19:07
cf5bb63

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 graff from the original v0.1.9 binary on 2026-05-20, please re-install — your /trace won't walk subagent trees correctly.

Highlights

  • Codex Responses-API parity round 1–3 (#66#106): wire-level parity with upstream openai/codex for the chatgpt.com Codex backend across body fields (parallel_tool_calls, client_metadata with 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 Task tool calls out in parallel via futures::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's conversation_id with parent_agent_id linked 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.json is now chmod 0o600 after writes.
  • WS protocol pin (#66): OpenAI-Beta: responses_websockets=2026-02-06 header 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 it

This 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 blanket AgentService::call impl can thread the repo into the per-call ToolRegistry, and
  • threads parent_conversation_id through ToolCallContext so the child agent's events land under the root's conversation_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 /trace

Direct 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_websockets metadata (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 UsageErrorResponse envelope from Codex backend (closes #90) (#97)
  • feat(openai-responses): handle output_item.done + reasoning_summary_part.added SSE events (closes #93 / #94) (#96)
  • feat(openai-responses): send parallel_tool_calls + Codex client_metadata body 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_verbosity for Codex (#81)
  • feat(providers): add codex-auto-review to 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.verbosity for 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 0o600 after 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/compact and /memories/trace_summarize endpoints
  • AgentIdentity auth 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 | sh

Supported 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 Release workflow (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-up
  • aarch64-linux-android build failed because arboard (clipboard library used in forge_main) does not compile on Android — needs a #[cfg(not(target_os = "android"))] guard, tracked as a follow-up