@ngockhoale/ukit 2.3.13 → 2.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.3.14 - 2026-09-11
|
|
6
|
+
|
|
7
|
+
Vision-lane root cause, part 3 — the decisive one: the extractor could not see images in REAL
|
|
8
|
+
Claude Code transcripts at all. Found by reproducing the failure against the user's own real
|
|
9
|
+
session transcripts after they insisted the bug lived "right at the image" — grep proved the
|
|
10
|
+
transcripts contained `"type":"image"` while every extraction returned `imageCount: 0`. The
|
|
11
|
+
2.3.12/2.3.13 suites were green because the fixtures modeled an invented transcript shape.
|
|
12
|
+
|
|
13
|
+
**P1 — real transcript image envelopes were invisible to the extractor.** Verified against
|
|
14
|
+
real transcripts from four projects, Claude Code stores images in THREE shapes:
|
|
15
|
+
|
|
16
|
+
- flat: `message.content[i] = { type: 'image', source: { type: 'base64', … } }` — the only
|
|
17
|
+
shape the pre-fix parser handled;
|
|
18
|
+
- pasted: a top-level `attachment` envelope (`type: 'attachment'`,
|
|
19
|
+
`attachment.prompt[]` = content blocks) — every real pasted image lives here, NOT in
|
|
20
|
+
`message.content`; the pre-fix parser extracted nothing from any pasted image;
|
|
21
|
+
- tool-returned: `message.content[i] = { type: 'tool_result', content: [ { type: 'image',
|
|
22
|
+
… } ] }` — screenshots coming back from tools nest one level down; also invisible.
|
|
23
|
+
|
|
24
|
+
Fix: `extractImageBlocks()` now walks a bounded collector (depth ≤ 3) across
|
|
25
|
+
`message.content[]`, nested `content[]` arrays inside blocks, and the top-level
|
|
26
|
+
`attachment.prompt[]` / `attachment.content` envelopes. Same-base64 payloads across
|
|
27
|
+
envelopes dedupe by sha as before. Real-data validation after the fix: the same four
|
|
28
|
+
transcripts now extract valid images (PNG 2278×2154 from the paste envelope, JPEG 1400×884
|
|
29
|
+
from a tool_result, PNG/JPEG from the flat shape), and a live analyst dispatch read the
|
|
30
|
+
real pasted screenshot.
|
|
31
|
+
|
|
32
|
+
Tests: `tests/handoff/cycle10/extract-image-contract.test.mjs` gains three cases whose
|
|
33
|
+
fixtures copy the real envelope structures verbatim (RED pre-fix: `imageCount 0`), plus a
|
|
34
|
+
cross-envelope dedupe pin. All cycle10 + cycle4 vision suites green.
|
|
35
|
+
|
|
5
36
|
## 2.3.13 - 2026-09-11
|
|
6
37
|
|
|
7
38
|
Vision-lane root cause, part 2: the Codex model catalog could still deny the vision lane on
|
package/package.json
CHANGED
|
@@ -381,25 +381,64 @@ function readLines(filePath) {
|
|
|
381
381
|
return raw.split('\n').filter((line) => line.trim().length > 0);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Deepest content-blocks nesting the collector walks. 1 covers the flat
|
|
386
|
+
* message.content[] / attachment.prompt[] arrays; 2 covers the real-world
|
|
387
|
+
* tool_result nesting (message.content[i].content[]); 3 is a cheap safety
|
|
388
|
+
* bound — deeper structures are not a Claude Code shape and are ignored.
|
|
389
|
+
*/
|
|
390
|
+
const MAX_BLOCK_WALK_DEPTH = 3;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Collects base64 image blocks from one content-blocks array. Real Claude
|
|
394
|
+
* Code transcripts nest images in more than one place (verified against real
|
|
395
|
+
* session transcripts 2026-09-11 — the pre-fix parser only saw the flat
|
|
396
|
+
* shape and extracted NOTHING from real paste/tool-image traffic):
|
|
397
|
+
* A. message.content[i] = { type: 'image', source: { type: 'base64', … } }
|
|
398
|
+
* C. message.content[i] = { type: 'tool_result', content: [ { type: 'image', … } ] }
|
|
399
|
+
* Blocks that carry their own `content` array (tool_result etc.) are walked
|
|
400
|
+
* one level deeper, bounded by MAX_BLOCK_WALK_DEPTH.
|
|
401
|
+
*/
|
|
402
|
+
function collectImageBlocksFromContent(content, lineIndex, blocks, depth) {
|
|
403
|
+
if (!Array.isArray(content) || depth > MAX_BLOCK_WALK_DEPTH) return;
|
|
404
|
+
for (const block of content) {
|
|
405
|
+
if (!block || typeof block !== 'object') continue;
|
|
406
|
+
if (block.type === 'image') {
|
|
407
|
+
const source = block.source;
|
|
408
|
+
if (!source || source.type !== 'base64') continue;
|
|
409
|
+
const mediaType = source.media_type;
|
|
410
|
+
const data = source.data;
|
|
411
|
+
if (typeof mediaType !== 'string' || typeof data !== 'string' || !data) continue;
|
|
412
|
+
blocks.push({ mediaType, data, lineIndex });
|
|
413
|
+
} else if (Array.isArray(block.content)) {
|
|
414
|
+
collectImageBlocksFromContent(block.content, lineIndex, blocks, depth + 1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
384
419
|
/**
|
|
385
420
|
* Parses each JSONL line independently. A malformed line is skipped via its
|
|
386
421
|
* own try/catch and never throws out to the caller.
|
|
422
|
+
*
|
|
423
|
+
* Envelope shapes collected per line (real-transcript evidence 2026-09-11):
|
|
424
|
+
* A/C. the API message envelope — flat image blocks, plus tool_result
|
|
425
|
+
* blocks that nest theirs one level down (see the collector above).
|
|
426
|
+
* B. pasted images arrive in a TOP-LEVEL attachment envelope, NOT in
|
|
427
|
+
* message.content: attachment.prompt[] is the content-blocks array
|
|
428
|
+
* (attachment.content accepted defensively for the same reason).
|
|
429
|
+
* The same payload appearing in two envelopes (attachment + the later user
|
|
430
|
+
* message) dedupes by sha in selectImages().
|
|
387
431
|
*/
|
|
388
432
|
function extractImageBlocks(lines) {
|
|
389
433
|
const blocks = [];
|
|
390
434
|
for (let i = 0; i < lines.length; i += 1) {
|
|
391
435
|
try {
|
|
392
436
|
const obj = JSON.parse(lines[i]);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (!source || source.type !== 'base64') continue;
|
|
399
|
-
const mediaType = source.media_type;
|
|
400
|
-
const data = source.data;
|
|
401
|
-
if (typeof mediaType !== 'string' || typeof data !== 'string' || !data) continue;
|
|
402
|
-
blocks.push({ mediaType, data, lineIndex: i });
|
|
437
|
+
collectImageBlocksFromContent(obj?.message?.content, i, blocks, 1);
|
|
438
|
+
const attachment = obj?.attachment;
|
|
439
|
+
if (attachment && typeof attachment === 'object') {
|
|
440
|
+
collectImageBlocksFromContent(attachment.prompt, i, blocks, 1);
|
|
441
|
+
collectImageBlocksFromContent(attachment.content, i, blocks, 1);
|
|
403
442
|
}
|
|
404
443
|
} catch {
|
|
405
444
|
// malformed line: skip and continue
|