From 0e1e2f3febadb0a089a7d66788088357caeb225b Mon Sep 17 00:00:00 2001 From: Saksham-Bhardwaj Date: Sun, 26 Apr 2026 23:11:31 +0530 Subject: [PATCH 1/2] docs+test: document Codex support and cover CLI dispatch The Codex adapter, .codex/hooks.json, and Codex UI registration already existed in the repo, but Codex was still listed as roadmap-only and the CLI boundary had no test asserting AGENTS_OBSERVE_AGENT_CLASS=codex stamps Codex metadata correctly. - README: tagline, hook setup section, "How it works" line, and ROADMAP updated to reflect Codex as a supported agent class. - test/hooks/scripts/observe_cli.test.mjs: new test covering the codex dispatch path through observe_cli. Verified locally: npm run test:scripts (219 passing) and the server suite (240 passing). Live smoke test posted Codex SessionStart, PreToolUse, PostToolUse, and Stop events end-to-end into the API. --- README.md | 33 +++++++++++++++++++---- test/hooks/scripts/observe_cli.test.mjs | 35 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c11b74a8..8c16fdf3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Agents Observe -Real-time observability dashboard for Claude Code agents. +Real-time observability dashboard for Claude Code and Codex agents. Includes powerful filtering, searching, and visualization of multi-agent sessions. @@ -120,7 +120,9 @@ just dev See [justfile](./justfile) for additional commands. -### 2. Configure Claude Code hooks +### 2. Configure agent hooks + +#### Claude Code Copy the hooks from `.claude/settings.json` into your project's settings.in this repo into your target project's Claude Code settings: @@ -129,6 +131,26 @@ Copy the hooks from `.claude/settings.json` into your project's settings.in this Update the `$CLAUDE_PROJECT_DIR` paths to point to your agents-observe install location. +#### Codex + +Codex support uses Codex hooks plus the `codex` agent adapter in +`hooks/scripts/lib/agents/codex.mjs`. This repo already includes a local +Codex hook config at `.codex/hooks.json` and enables hooks in +`.codex/config.toml`. + +For this repo, start the server and then run Codex from the repo root: + +```bash +just start-local +codex +``` + +For another project, add equivalent Codex hook config in that project's +`.codex/hooks.json`, pointing the command at this checkout's +`hooks/scripts/hook.sh`, and set `AGENTS_OBSERVE_AGENT_CLASS=codex`. +Set `AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop` if you want a dashboard +notification when a Codex turn completes. + **Environment variables set in the config:** A few commonly used ones: @@ -152,7 +174,7 @@ just health just test-event ``` -Navigate to **** (dev) or **** (Docker). You should see the test event appear. Start a Claude Code session in your configured project and events will stream in automatically. +Navigate to **** (dev) or **** (Docker). You should see the test event appear. Start a Claude Code or Codex session in your configured project and events will stream in automatically. ## Standalone Commands @@ -191,6 +213,7 @@ app/ hooks/ hooks.json # Plugin hook definitions scripts/ # CLI, MCP server, and shared libs +.codex/ # Repo-local Codex hook config for local testing skills/ # /observe skills scripts/ # Release tooling test/ # Integration tests @@ -208,7 +231,7 @@ package.json # Version metadata and workspace scripts ## How it works -**Hooks** fire on every Claude Code event (tool calls, prompts, stops, subagent lifecycle). `observe_cli.mjs` reads the raw event from stdin and dispatches through `hooks/scripts/lib/agents/.mjs` — each agent class's `buildHookEvent()` builds the envelope (project metadata plus agent-class-aware flags like `meta.isNotification` / `meta.clearsNotification`) and the CLI POSTs it to the server. If the server needs additional data (like the session's human-readable slug), it responds with a request — the hook reads it from the local transcript file and sends it back. +**Hooks** fire on every Claude Code or Codex event (tool calls, prompts, stops, subagent lifecycle). `observe_cli.mjs` reads the raw event from stdin and dispatches through `hooks/scripts/lib/agents/.mjs` — each agent class's `buildHookEvent()` builds the envelope (project metadata plus agent-class-aware flags like `meta.isNotification` / `meta.clearsNotification`) and the CLI POSTs it to the server. If the server needs additional data (like the session's human-readable slug), it responds with a request — the hook reads it from the local transcript file and sends it back. **Server** receives raw events, extracts structural fields (type, tool name, agent ID), stores agent metadata (name, description, type, parentage), and saves everything in SQLite. Events are forwarded to WebSocket clients subscribed to the relevant session — each browser tab only receives events for the session it's viewing. The server tracks session status (active/stopped) but does not track agent status. @@ -253,7 +276,7 @@ Run `just db-reset` to delete the SQLite database and start fresh (stops the ser ## ROADMAP -- [ ] Add support for Codex +- [x] Add support for Codex - [ ] Add support for OpenClaw - [ ] Add support for pi-code agents diff --git a/test/hooks/scripts/observe_cli.test.mjs b/test/hooks/scripts/observe_cli.test.mjs index 883e1829..ae803bd0 100644 --- a/test/hooks/scripts/observe_cli.test.mjs +++ b/test/hooks/scripts/observe_cli.test.mjs @@ -151,6 +151,41 @@ describe('observe_cli', () => { } }) + it('dispatches Codex hook payloads when AGENTS_OBSERVE_AGENT_CLASS=codex', async () => { + const mock = mockApiHandler({ + 'POST /api/events': { status: 201, body: { ok: true } }, + }) + const { server, url } = await startMockServer(mock.handler) + + try { + await runCli(['hook'], { + stdin: JSON.stringify({ + session_id: 'codex-session-1', + hook_event_name: 'PreToolUse', + tool_name: 'Bash', + tool_use_id: 'tool-1', + tool_input: { command: 'echo codex' }, + }), + env: { + AGENTS_OBSERVE_API_BASE_URL: `${url}/api`, + AGENTS_OBSERVE_AGENT_CLASS: 'codex', + AGENTS_OBSERVE_PROJECT_SLUG: 'codex-project', + }, + }) + await new Promise((r) => setTimeout(r, 200)) + const eventReq = mock.received.find((r) => r.url === '/api/events') + const parsed = JSON.parse(eventReq.body) + expect(parsed.agentClass).toBe('codex') + expect(parsed.hookName).toBe('PreToolUse') + expect(parsed.sessionId).toBe('codex-session-1') + expect(parsed.agentId).toBe('codex-session-1') + expect(parsed.payload.tool_name).toBe('Bash') + expect(parsed._meta.project.slug).toBe('codex-project') + } finally { + server.close() + } + }) + it('sets flags.clearsNotification on UserPromptSubmit events', async () => { const mock = mockApiHandler({ 'POST /api/events': { status: 201, body: { ok: true } }, From 957d97d0bc568ed95fc35cad524b194214d83b01 Mon Sep 17 00:00:00 2001 From: Saksham Bhardwaj Date: Mon, 27 Apr 2026 11:19:06 +0530 Subject: [PATCH 2/2] fix: improve codex hook metadata --- .codex/hooks.json | 21 +++++++--- README.md | 25 ++++++++---- app/client/src/agents/default/index.tsx | 10 ++++- docs/ENVIRONMENT.md | 4 +- hooks/scripts/lib/agents/codex.mjs | 30 ++++++++++----- test/hooks/scripts/lib/agents/codex.test.mjs | 40 ++++++++++++++++++-- test/hooks/scripts/observe_cli.test.mjs | 5 +++ 7 files changed, 106 insertions(+), 29 deletions(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index 8956dab2..65bc27c4 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"", + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"", "statusMessage": "Starting Agents Observe" } ] @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" } ] } @@ -29,7 +29,18 @@ "hooks": [ { "type": "command", - "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" + } + ] + } + ], + "PermissionRequest": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" } ] } @@ -39,7 +50,7 @@ "hooks": [ { "type": "command", - "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" } ] } @@ -49,7 +60,7 @@ "hooks": [ { "type": "command", - "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" + "command": "AGENTS_OBSERVE_LOG_LEVEL=trace AGENTS_OBSERVE_AGENT_CLASS=codex bash \"$(git rev-parse --show-toplevel)/hooks/scripts/hook.sh\"" } ] } diff --git a/README.md b/README.md index 8c16fdf3..2beddb47 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,9 @@ Update the `$CLAUDE_PROJECT_DIR` paths to point to your agents-observe install l Codex support uses Codex hooks plus the `codex` agent adapter in `hooks/scripts/lib/agents/codex.mjs`. This repo already includes a local Codex hook config at `.codex/hooks.json` and enables hooks in -`.codex/config.toml`. +`.codex/config.toml`. The bundled `.codex/hooks.json` is repo-local: it +uses `$(git rev-parse --show-toplevel)` and should only be copied within +this checkout. For this repo, start the server and then run Codex from the repo root: @@ -146,10 +148,19 @@ codex ``` For another project, add equivalent Codex hook config in that project's -`.codex/hooks.json`, pointing the command at this checkout's -`hooks/scripts/hook.sh`, and set `AGENTS_OBSERVE_AGENT_CLASS=codex`. -Set `AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS=Stop` if you want a dashboard -notification when a Codex turn completes. +`.codex/hooks.json`, pointing the command at this checkout with an +absolute path: + +```json +{ + "type": "command", + "command": "AGENTS_OBSERVE_AGENT_CLASS=codex bash /absolute/path/to/agents-observe/hooks/scripts/hook.sh" +} +``` + +Codex `PermissionRequest` events trigger dashboard notifications by +default. Set `AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS` only if you want to +override that behavior. **Environment variables set in the config:** @@ -231,9 +242,9 @@ package.json # Version metadata and workspace scripts ## How it works -**Hooks** fire on every Claude Code or Codex event (tool calls, prompts, stops, subagent lifecycle). `observe_cli.mjs` reads the raw event from stdin and dispatches through `hooks/scripts/lib/agents/.mjs` — each agent class's `buildHookEvent()` builds the envelope (project metadata plus agent-class-aware flags like `meta.isNotification` / `meta.clearsNotification`) and the CLI POSTs it to the server. If the server needs additional data (like the session's human-readable slug), it responds with a request — the hook reads it from the local transcript file and sends it back. +**Hooks** fire on Claude Code or Codex events (tool calls, prompts, stops, subagent lifecycle where supported). `observe_cli.mjs` reads the raw event from stdin and dispatches through `hooks/scripts/lib/agents/.mjs` — each agent class's `buildHookEvent()` builds the event envelope with identity fields, project/session hints under `_meta`, and behavior flags such as `startsNotification` or `clearsNotification`. The CLI POSTs that envelope to the server. If the server needs additional data (like the session's human-readable slug), it responds with a request — the hook reads it from the local transcript file and sends it back. -**Server** receives raw events, extracts structural fields (type, tool name, agent ID), stores agent metadata (name, description, type, parentage), and saves everything in SQLite. Events are forwarded to WebSocket clients subscribed to the relevant session — each browser tab only receives events for the session it's viewing. The server tracks session status (active/stopped) but does not track agent status. +**Server** validates the envelope, stores the opaque payload in SQLite, applies `_meta` creation hints, and uses flags to update session state. Events are forwarded to WebSocket clients subscribed to the relevant session — each browser tab only receives events for the session it's viewing. The server tracks session status (active/stopped) but does not track agent status. **Client** fetches events via REST API on initial load, then receives real-time updates via WebSocket (events are appended to the local cache — no refetching). All agent state (status, event counts, timing) is derived from the event stream. Tool events are deduped client-side (PreToolUse + PostToolUse merged into a single row). The emoji icon mapping and summary generation are editable config files. diff --git a/app/client/src/agents/default/index.tsx b/app/client/src/agents/default/index.tsx index 59979aa5..41e63c70 100644 --- a/app/client/src/agents/default/index.tsx +++ b/app/client/src/agents/default/index.tsx @@ -24,8 +24,14 @@ function deriveStatus(_event: RawEvent, _grouped: RawEvent[]): EventStatus | nul } export function processEvent(raw: RawEvent, ctx: ProcessingContext): { event: EnrichedEvent } { - const turnId = ctx.getCurrentTurn(raw.agentId) - const payloadToolUseId = (raw.payload as Record).tool_use_id + const payload = raw.payload as Record + const payloadTurnId = payload.turn_id + const turnId = typeof payloadTurnId === 'string' ? payloadTurnId : ctx.getCurrentTurn(raw.agentId) + // Some agent classes carry tool_use_id on the payload under that exact + // key; the default processor surfaces it as the groupId for Pre/Post + // pairing. Reads from payload rather than a top-level field because the + // server no longer promotes tool_use_id to a column. + const payloadToolUseId = payload.tool_use_id const toolUseId = typeof payloadToolUseId === 'string' ? payloadToolUseId : null const toolName = deriveToolName(raw) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 9471c8d1..ad51d837 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -17,9 +17,9 @@ per-user behavior. | Variable | Default | Purpose | | --- | --- | --- | -| `AGENTS_OBSERVE_AGENT_CLASS` | `claude-code` | Which agent class the CLI dispatches through: `claude-code`, `codex`, or anything else (falls back to the `unknown` lib). | +| `AGENTS_OBSERVE_AGENT_CLASS` | `claude-code` | Which agent class the CLI dispatches through: `claude-code`, `codex`, or anything else (falls back to the default lib). | | `AGENTS_OBSERVE_PROJECT_SLUG` | *(unset)* | Override the project slug the CLI reports on each event. | -| `AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS` | *(unset — defaults to `Notification`)* | Comma-separated hook events that trigger the notification bell. Empty string (`""`) disables bells entirely. Claude Code's `Notification` hook fires by default; Codex has no equivalent, so Codex users must opt in (e.g. set to `Stop` to fire on turn end). See [spec-configurable-notification-events.md](./plans/spec-configurable-notification-events.md). | +| `AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS` | *(unset — agent-class default)* | Comma-separated hook events that trigger the notification bell. Empty string (`""`) disables bells entirely. Claude Code defaults to `Notification`; Codex defaults to `PermissionRequest`. See [spec-configurable-notification-events.md](./plans/spec-configurable-notification-events.md). | | `AGENTS_OBSERVE_ALLOW_LOCAL_CALLBACKS` | `all` | Comma-separated allowlist of server-initiated callbacks the CLI will execute. `all` permits every known handler. | | `AGENTS_OBSERVE_API_BASE_URL` | *(derived from `AGENTS_OBSERVE_SERVER_PORT`)* | Full URL of the server API (e.g. `http://remote:4981/api`). Overrides the auto-started local Docker server. | | `AGENTS_OBSERVE_LOG_LEVEL` | `warn` | CLI log level: `error`, `warn`, `info`, `debug`, `trace`. | diff --git a/hooks/scripts/lib/agents/codex.mjs b/hooks/scripts/lib/agents/codex.mjs index d777c398..d7667fe3 100644 --- a/hooks/scripts/lib/agents/codex.mjs +++ b/hooks/scripts/lib/agents/codex.mjs @@ -1,12 +1,20 @@ // hooks/scripts/lib/agents/codex.mjs -// Codex hook lib. Composes default.mjs and overrides agentClass only — -// Codex hook payloads use the same identity-field shape as Claude -// (session_id, agent_id, hook_event_name, cwd, transcript_path), so the -// default lib's extraction works without further overrides. +// Codex hook lib. Composes default.mjs, overrides agentClass, and uses +// PermissionRequest as Codex's default notification event. Codex hook +// payloads use the same identity-field shape as Claude (session_id, +// agent_id, hook_event_name, cwd, transcript_path), so the default lib's +// extraction works without further overrides. import { readFileSync } from 'node:fs' import { defaultLib } from './default.mjs' +const CODEX_DEFAULT_NOTIFICATION_EVENTS = ['PermissionRequest'] + +function codexNotificationConfig(config) { + if (config?.notificationOnEvents !== undefined) return config + return { ...config, notificationOnEvents: CODEX_DEFAULT_NOTIFICATION_EVENTS } +} + export function buildEnv(config) { return defaultLib.buildEnv(config) } @@ -14,11 +22,13 @@ export function buildEnv(config) { /** * Build the event envelope for a Codex hook payload. * - * Codex does NOT have Claude's UserPromptSubmit/SessionEnd hooks, so we - * never set flags.clearsNotification or flags.stopsSession. Notification - * opt-in is handled by the default lib via - * AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS. If future Codex versions add - * equivalent semantic events, set the flags here. + * Notification semantics: Codex's PermissionRequest is the point where + * the agent is blocked on user approval, so it is the default dashboard + * notification event. Users can override this with + * AGENTS_OBSERVE_NOTIFICATION_ON_EVENTS. + * + * Codex does not have Claude's UserPromptSubmit/SessionEnd hooks, so this + * adapter does not set flags.clearsNotification or flags.stopsSession. * * @param {object} config * @param {object} log @@ -26,7 +36,7 @@ export function buildEnv(config) { * @returns {{ envelope: object, hookEvent: string, toolName: string }} */ export function buildHookEvent(config, log, payload) { - const result = defaultLib.buildHookEvent(config, log, payload) + const result = defaultLib.buildHookEvent(codexNotificationConfig(config), log, payload) result.envelope.agentClass = 'codex' return result } diff --git a/test/hooks/scripts/lib/agents/codex.test.mjs b/test/hooks/scripts/lib/agents/codex.test.mjs index 8d6dd5dc..a2314ba1 100644 --- a/test/hooks/scripts/lib/agents/codex.test.mjs +++ b/test/hooks/scripts/lib/agents/codex.test.mjs @@ -144,6 +144,31 @@ describe('codex.buildHookEvent', () => { expect(envelope._meta?.session?.transcriptPath).toBe('/tmp/sess.jsonl') }) + it('preserves real Codex hook payload fields on the opaque payload', () => { + const { envelope } = buildHookEvent(config, makeLog(), { + session_id: 'cdx-sess-1', + hook_event_name: 'PreToolUse', + cwd: '/repo', + model: 'gpt-5.5', + turn_id: 'turn-1', + tool_name: 'Bash', + tool_use_id: 'tool-1', + tool_input: { command: 'echo codex' }, + transcript_path: '/Users/me/.codex/sessions/2026/04/27/session.jsonl', + }) + expect(envelope.agentClass).toBe('codex') + expect(envelope.hookName).toBe('PreToolUse') + expect(envelope.sessionId).toBe('cdx-sess-1') + expect(envelope.agentId).toBe('cdx-sess-1') + expect(envelope.payload.model).toBe('gpt-5.5') + expect(envelope.payload.turn_id).toBe('turn-1') + expect(envelope.payload.tool_name).toBe('Bash') + expect(envelope.payload.tool_use_id).toBe('tool-1') + expect(envelope._meta?.session?.transcriptPath).toBe( + '/Users/me/.codex/sessions/2026/04/27/session.jsonl', + ) + }) + it('does not set Claude-only flags (clearsNotification, stopsSession, resolveProject)', () => { for (const hook_event_name of [ 'UserPromptSubmit', @@ -174,7 +199,16 @@ describe('codex.buildHookEvent', () => { }) describe('notificationOnEvents opt-in', () => { - it('default config: no events fire startsNotification (apart from "Notification")', () => { + it('default config: PermissionRequest fires startsNotification', () => { + const { envelope } = buildHookEvent(config, makeLog(), { + hook_event_name: 'PermissionRequest', + session_id: 'cdx-1', + }) + expect(envelope.flags?.startsNotification).toBe(true) + expect(envelope.flags?.clearsNotification).toBeUndefined() + }) + + it('default config: Stop does not fire startsNotification', () => { const { envelope } = buildHookEvent(config, makeLog(), { hook_event_name: 'Stop', session_id: 'cdx-1', @@ -200,10 +234,10 @@ describe('codex.buildHookEvent', () => { expect(envelope.flags?.startsNotification).toBeUndefined() }) - it('empty list suppresses startsNotification on Notification events', () => { + it('empty list suppresses startsNotification on PermissionRequest events', () => { const optOut = { ...config, notificationOnEvents: [] } const { envelope } = buildHookEvent(optOut, makeLog(), { - hook_event_name: 'Notification', + hook_event_name: 'PermissionRequest', session_id: 'cdx-1', }) expect(envelope.flags?.startsNotification).toBeUndefined() diff --git a/test/hooks/scripts/observe_cli.test.mjs b/test/hooks/scripts/observe_cli.test.mjs index ae803bd0..853af950 100644 --- a/test/hooks/scripts/observe_cli.test.mjs +++ b/test/hooks/scripts/observe_cli.test.mjs @@ -162,6 +162,8 @@ describe('observe_cli', () => { stdin: JSON.stringify({ session_id: 'codex-session-1', hook_event_name: 'PreToolUse', + model: 'gpt-5.5', + turn_id: 'turn-1', tool_name: 'Bash', tool_use_id: 'tool-1', tool_input: { command: 'echo codex' }, @@ -179,7 +181,10 @@ describe('observe_cli', () => { expect(parsed.hookName).toBe('PreToolUse') expect(parsed.sessionId).toBe('codex-session-1') expect(parsed.agentId).toBe('codex-session-1') + expect(parsed.payload.model).toBe('gpt-5.5') + expect(parsed.payload.turn_id).toBe('turn-1') expect(parsed.payload.tool_name).toBe('Bash') + expect(parsed.payload.tool_use_id).toBe('tool-1') expect(parsed._meta.project.slug).toBe('codex-project') } finally { server.close()