[Bug]: Codex image items using fileId are dropped before reaching image projection #35851
Send to my agentSummary#
Codex can identify a generated image with a durable file reference instead of inline image data. T3 rejects that valid completion event before its image handler runs, so the existing image projection cannot receive the saved image path. An isolated run through the real typed client reproduced the dropped event while the equivalent legacy payload reached its handler.
Before submitting#
- Related issues: listed under Additional context; focused open and closed issue/PR searches found no exact duplicate.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/server
Observed behavior#
An in-memory peer sent a valid current item/completed image payload with fileId, savedPath, and no result. The typed client did not call its registered item/completed handler. An otherwise equivalent control with result: "" reached the same handler. The callback failure was reproduced directly; the downstream loss of the image projection is source-derived, not a live UI observation.
Reproduction#
- Use the pinned checkout with its Effect dependency installed.
- Feed the client the legacy inline-result control and then the current file-reference image completion, followed by a barrier notification.
- Observe that only the control image ID reaches the registered completion handler.
The final script below reproduced the failure in one run and its control passed. An earlier exploratory combined probe omitted the image envelope timestamp, which also rejected its image control; that invalid fixture was corrected before the standalone reproduction recorded here. No live Codex turn or T3 state was used.
Save the script as repro-image.sh and run sh repro-image.sh /path/to/checkout. Prerequisites are Node.js with TypeScript stripping and the checkout's pinned Effect dependency installed. The script creates disposable state under mktemp, uses the production in-memory stdio and typed client, and removes its state on exit without editing the checkout. Exit 1 reproduces the bug, 0 means both payloads deliver or decode correctly, and 2 means setup or the control failed.
Reproduction script: repro-image.sh
#!/bin/sh
set -u
if [ "$#" -ne 1 ]; then echo "Usage: sh repro.sh /path/to/checkout" >&2; echo "RESULT: inconclusive"; exit 2; fi
repo=$(CDPATH= cd -- "$1" && pwd) || { echo "RESULT: inconclusive"; exit 2; }
scratch=$(mktemp -d) || { echo "RESULT: inconclusive"; exit 2; }
trap 'rm -rf "$scratch"' EXIT HUP INT TERM
mkdir -p "$scratch/home" "$scratch/tmp" "$scratch/codex" "$scratch/config" "$scratch/data" "$scratch/cache" "$scratch/t3"
export HOME="$scratch/home" TMPDIR="$scratch/tmp" CODEX_HOME="$scratch/codex" XDG_CONFIG_HOME="$scratch/config" XDG_DATA_HOME="$scratch/data" XDG_CACHE_HOME="$scratch/cache" T3CODE_HOME="$scratch/t3"
cd "$repo/packages/effect-codex-app-server" || { echo "RESULT: inconclusive"; exit 2; }
node --version
git --version
git rev-parse HEAD
node --input-type=module - <<'JS'
import * as Effect from 'effect/Effect';
import * as Deferred from 'effect/Deferred';
import * as Queue from 'effect/Queue';
import * as Fiber from 'effect/Fiber';
import * as Exit from 'effect/Exit';
import * as Logger from 'effect/Logger';
import { createRequire } from 'node:module';
import * as Client from './src/client.ts';
import { makeInMemoryStdio } from './src/_internal/stdio.ts';
const require=createRequire(import.meta.url);
console.log('Effect '+require('effect/package.json').version);
try {
const failed=await Effect.runPromise(Effect.scoped(Effect.gen(function*(){
const io=yield* makeInMemoryStdio();
const client=yield* Client.make(io.stdio);
const base={completedAtMs:1,threadId:'thread-1',turnId:'turn-1',item:{type:'imageGeneration',id:'image-new',status:'completed',fileId:'file-1',savedPath:process.env.TMPDIR+'/generated.png',revisedPrompt:null}};
const delivered=[];
const done=yield* Deferred.make();
yield* client.handleServerNotification('item/completed',p=>Effect.sync(()=>delivered.push(p.item.id)));
yield* client.handleUnknownServerNotification(()=>Deferred.succeed(done,undefined).pipe(Effect.asVoid));
const frames=[
{method:'item/completed',params:{...base,item:{...base.item,id:'image-control',result:''}}},
{method:'item/completed',params:base},
{method:'repro/barrier',params:{}}
];
yield* Queue.offer(io.input,new TextEncoder().encode(frames.map(f=>JSON.stringify(f)).join('\n')+'\n'));
yield* Deferred.await(done);
console.log('Delivered image IDs: '+JSON.stringify(delivered));
if(!delivered.includes('image-control'))throw new Error('Legacy result control failed');
return !delivered.includes('image-new');
})).pipe(Effect.provide(Logger.layer([]))));
process.exitCode=failed?1:0;
} catch(error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode=2; }
JS
status=$?
case "$status" in
0) echo "RESULT: expected behavior"; exit 0 ;;
1) echo "RESULT: defect reproduced"; exit 1 ;;
*) echo "RESULT: inconclusive"; exit 2 ;;
esac
Expected behavior#
Both supported image representations must reach the completion handler. The Codex 0.167.2 ItemCompletedNotification JSON schema defines the image variant with an anyOf requiring either fileId or result; result is not universally required. The same contract exists in the 0.166.0 export. Local exports from Codex CLI 0.167.2 agree in both default and --experimental mode. The existing T3 image handler reads savedPath to retain generated images.
Impact and scope#
Impact: P2 - A bounded class of valid generated-image completions is discarded before the existing image projection can run.
This affects the file-reference representation, not the legacy inline-result control. The reproduction exercises the production typed client and an inert in-memory peer; it does not generate an image or launch the UI. NOT_ENOUGH_INFO: the frequency of this representation in real image-generation sessions and a live UI workaround were not measured; an authenticated image-generation trace would establish them.
Version#
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93. Contract comparison: Codex generator baseline 0.159.0, managed baseline 0.166.0, installed/current stable codex-cli 0.167.2. The behavior run used the T3 source and Effect 4.1.3, not a live Codex process.
Runtime and platform#
macOS 27.2, arm64; Node.js v26.12.1; Effect 4.1.3; Git 2.56.0. No frontend browser or desktop shell was launched.
Interface#
Library: effect-codex-app-server typed client, reached from the server Codex provider.
Earliest failure stage#
Integration or transport
Relevant output or evidence#
The exact final script printed the following output and exited 1:
v26.12.1
git version 2.56.0
59e64eef881632a23f89c4138bcf37f23d92bf93
Effect 4.1.3
Delivered image IDs: ["image-control"]
RESULT: defect reproduced
packages/effect-codex-app-server/src/_generated/schema.gen.ts#L33960-L33974 requires result: Schema.String. packages/effect-codex-app-server/src/client.ts#L180-L203 drops a known notification after decode failure rather than forwarding it to its handler. apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts#L5498-L5519 is the existing image completion path that receives no callback in the reproduction.
Analysis or suspected cause#
Source evidence: the generator pins upstream 0.159.0 in packages/effect-codex-app-server/scripts/generate.ts#L16-L19, and these checked-in constraints retain the older representation. Later Codex exports broaden the valid inputs. This establishes the specific incompatible constraint, not untouched ancestry for every generated file; local generator/client adaptations are present.
Additional context#
- https://github.com/pingdotgg/t3code/issues/15420 — covers the former absence of image handling in the V2 adapter; this reproduction fails earlier at decoding even though that handler now exists.
- https://github.com/pingdotgg/t3code/pull/34020 — adds image tool activity after delivery to the adapter; its changed files do not cover the decoder requirement.
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced
juliusmarminge closed this
Sign in to join this conversation.