You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(images): nine review findings from #90, and the multi-image gap
Eleven comments. Two (the agent branch dropping images, and the generic .x handler stealing
the image ×) were already fixed in commits later than the reviewed diff. The other nine were
all real.
THE METER WAS UNDERCOUNTING. estimateMsgTokens takes a modelId to pick the resolution tier —
4,784 visual tokens on high-res against 1,568 on standard — and BOTH compaction call sites
omitted it. So every screenshot was costed at a third of what a Claude 4.7+ model is charged,
by the very module whose contract says it must never under-count. meterModel() resolves it
the same way currentContextLimit does, gateway and BYOK alike.
THE SEND PATH HAD TWO HOLES. Normalization is async and a placeholder chip carries no bytes,
so sending mid-decode posted an attachment with undefined media_type and base64 — refused
host-side, and the image vanished from a message the user watched themselves attach. doSend
now waits on in-flight work and refuses a surviving placeholder outright. Separately, the
model was only checked at ATTACH time: switch models between attaching and sending and the
images went to a model that cannot read them. Re-checked at send, refusing without
discarding anything typed or attached.
NEW CHAT INHERITED THE TRAY. The reset handler cleared the log and the context chips and
never touched pendingImages, so the next conversation opened holding the last one's
screenshots and stored them under the new session.
THE NOT-ATTACHED COUNT COULD GO NEGATIVE: it subtracted the whole tray from the batch size.
Four attached, cap five, drop two, and it claimed "-3 not attached". Counted from the
position in the batch now.
THE VISION GATE IGNORED THE PROVIDER. The registry enumerates vision providers — anthropic,
openai, openrouter declare it; ollama and `custom` deliberately do not — and the gate read
only the model id, so `custom` (an arbitrary user-supplied endpoint) was handed images
whenever the model NAME looked right. Both halves must agree now, as supportsToolsForModel
already required.
I1 FIXED ONE LOOP AND LEFT THE OTHER. The assistant branch still dropped unknown blocks
silently — the same bug, one branch over. It throws now; `thinking` is an explicit,
documented drop rather than a fall-through, because an OpenAI-shaped request has nowhere to
put it.
"DELETED WITH THEM" WAS NOT TRUE. Sessions are append-only and trash() only writes a
lifecycle event, so nothing ever removed a stored image — and refsIn, which I wrote for
exactly this, had no caller. There is a real sweep now, run on session seal, with an AGE
FLOOR: a normal chat writes media whose refs are never persisted anywhere, so
unreferenced-means-delete would break the open conversation. The comment says what the code
does now.
AND THE FEATURE GAP: several images are introduced by name ("Image 1:", "Image 2:") per the
vision guidance, so a question — and every follow-up turn — can refer to them. Only when
there is more than one; labelling a lone screenshot is noise.
TWO OF MY OWN GUARDS PASSED ON BROKEN CODE while verifying this, both from matching text
rather than behaviour: a commented-out resetImages() still satisfied /resetImages\(\)/, and
`inflightImages.size` appears twice in doSend so disabling the gate left the name present.
Fixed by stripping comments before matching, and by asserting the gate's POSITION relative
to the send plus the presence of the await. That is the fourth instance of this family this
session.
Bypass-verified, each by reverting: the undercount; the negative count; the wait deleted and
the await removed; no model re-check; New Chat inheriting; custom taking images; the
assistant loop silent again; the sweep removed; the labels dropped.
30 tests in imageAttach (was 21), 38 suites green.
0 commit comments