@ngockhoale/ukit 2.3.12 → 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,66 @@
|
|
|
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
|
+
|
|
36
|
+
## 2.3.13 - 2026-09-11
|
|
37
|
+
|
|
38
|
+
Vision-lane root cause, part 2: the Codex model catalog could still deny the vision lane on
|
|
39
|
+
every gateway machine, even after the 2.3.12 plumbing sweep. Found by a fresh end-to-end
|
|
40
|
+
repro (install into a clean project → fire the hook → live dispatch) plus a live gateway
|
|
41
|
+
probe on 2026-09-11 that returned `STATUS: OK / MODEL: unic-vision` on a machine whose
|
|
42
|
+
`~/.codex/model-catalog.json` omits the alias.
|
|
43
|
+
|
|
44
|
+
**P1 — the Codex model catalog could deny the Claude Code vision lane from another tool's
|
|
45
|
+
config.** `unic-gateway.mjs` derived `aliasAvailable` solely from
|
|
46
|
+
`~/.codex/model-catalog.json`: a catalog that omitted `unic-vision` — or was missing
|
|
47
|
+
entirely, the default on any machine that never used Codex CLI — produced
|
|
48
|
+
`aliasAvailable: false`, and both consumers (the vision router hint and route-task's
|
|
49
|
+
`resolveVisionModel`) then force-downgraded the lane to "read natively / do NOT dispatch"
|
|
50
|
+
or a config fallback. The catalog is a different tool's config and says nothing about what
|
|
51
|
+
the gateway resolves server-side for this session — the same principle TASK-007 already
|
|
52
|
+
established for endpoints. Fix: the catalog may now only CONFIRM the alias (new
|
|
53
|
+
`catalogListed` field); silence (absent, malformed, or alias-omitting catalog) leaves the
|
|
54
|
+
lane honored, with an informational advisory instead of a denial. The analyst agent's own
|
|
55
|
+
`WRONG_MODEL` self-check remains the in-band safety net: a genuinely dead alias fails
|
|
56
|
+
loudly and never guesses. `aliasAvailable: false` is reserved for a future source of
|
|
57
|
+
positive unavailability evidence, so the dormant fallback branches in both consumers are
|
|
58
|
+
unchanged and still covered by tests.
|
|
59
|
+
|
|
60
|
+
Tests: `tests/handoff/cycle4/unic-gateway.test.mjs` and
|
|
61
|
+
`tests/handoff/cycle10/vision-lane-fallback.test.mjs` rewritten for the flip (catalog
|
|
62
|
+
silent → lane honored; catalog missing → lane honored; listing → positive confirmation),
|
|
63
|
+
with the strict `=== false` fallback contract kept as a source-level case.
|
|
64
|
+
|
|
5
65
|
## 2.3.12 - 2026-09-11
|
|
6
66
|
|
|
7
67
|
Vision-lane freeze-sweep (post-2.3.11): the four defects that stranded the vision image 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
|
|
@@ -99,8 +99,11 @@ function detectPastedImageViaExtractor({ rootDir = process.cwd() } = {}) {
|
|
|
99
99
|
* Policy (mirrors the hook side in TASK-007 / ws-g.sh):
|
|
100
100
|
* - unicMode true AND aliasAvailable true -> unic-vision (today's behavior).
|
|
101
101
|
* - unicMode true AND aliasAvailable false -> fall through to config fallback (do NOT force a
|
|
102
|
-
* failing dispatch to a model
|
|
103
|
-
*
|
|
102
|
+
* failing dispatch to a model positively reported unavailable); state the unavailability in
|
|
103
|
+
* visionAdvisory so callers skip the lane and prefer native read. Since the 2026-09-11
|
|
104
|
+
* unic-gateway contract flip, the Codex catalog can no longer produce `false` (its silence
|
|
105
|
+
* never denies the lane), so this branch only fires on POSITIVE unavailability evidence
|
|
106
|
+
* from a future source.
|
|
104
107
|
* - unicMode true AND aliasAvailable missing/undefined -> keep today's behavior
|
|
105
108
|
* (return unic-vision). The strict === false check is what preserves the "missing field
|
|
106
109
|
* doesn't change today's behavior" contract from the task spec.
|
|
@@ -124,15 +127,15 @@ async function resolveVisionModel({ rootDir = process.cwd() } = {}) {
|
|
|
124
127
|
if (typeof fallbackModel === 'string' && fallbackModel.trim()) {
|
|
125
128
|
return {
|
|
126
129
|
visionModel: fallbackModel.trim(),
|
|
127
|
-
visionAdvisory: 'vision lane detected but unic-vision alias
|
|
128
|
-
+ '
|
|
130
|
+
visionAdvisory: 'vision lane detected but the unic-vision alias was positively reported '
|
|
131
|
+
+ 'unavailable; falling back to orchestration.modelTiers.vision.fallbackModel.',
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
return {
|
|
132
135
|
visionModel: null,
|
|
133
|
-
visionAdvisory: 'vision lane detected but unic-vision alias
|
|
134
|
-
+ '
|
|
135
|
-
+ 'was named to avoid forcing a failing dispatch — caller should prefer native read.',
|
|
136
|
+
visionAdvisory: 'vision lane detected but the unic-vision alias was positively reported '
|
|
137
|
+
+ 'unavailable and orchestration.modelTiers.vision.fallbackModel is not configured; no '
|
|
138
|
+
+ 'model was named to avoid forcing a failing dispatch — caller should prefer native read.',
|
|
136
139
|
};
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* unic-gateway.mjs (TASK-003, narrowed by TASK-007)
|
|
3
|
+
* unic-gateway.mjs (TASK-003, narrowed by TASK-007; alias-availability contract flipped 2026-09-11)
|
|
4
4
|
*
|
|
5
5
|
* Detects whether THIS Claude Code session's own outbound endpoint is pointed at the UNIC gateway
|
|
6
|
-
* — i.e. `ANTHROPIC_BASE_URL` contains "unicjsc.com" — so the vision lane can pin `unic-vision
|
|
7
|
-
*
|
|
6
|
+
* — i.e. `ANTHROPIC_BASE_URL` contains "unicjsc.com" — so the vision lane can pin `unic-vision`.
|
|
7
|
+
* The Codex model catalog can CONFIRM the alias (listing it) but its silence never denies it:
|
|
8
|
+
* it is another tool's config and says nothing about what the gateway resolves server-side for
|
|
9
|
+
* this session (live dispatch STATUS: OK on a machine whose catalog omits the alias, 2026-09-11).
|
|
8
10
|
*
|
|
9
11
|
* Self-contained: reads only env vars and local files. Does NOT read `.ukit/storage/config.json`,
|
|
10
12
|
* so it has no dependency on TASK-001 and stays usable in Wave 1.
|
|
@@ -121,25 +123,35 @@ function probeClaudeSettings(filePath) {
|
|
|
121
123
|
// Narrow anchored regex probe (no YAML parsing) for the omp config file's `baseUrl` key. A
|
|
122
124
|
// missing file, unreadable path, or malformed YAML all degrade to "no hit" -- never throws.
|
|
123
125
|
/**
|
|
124
|
-
* Checks
|
|
125
|
-
*
|
|
126
|
-
*
|
|
126
|
+
* Checks what the Codex model catalog says about `unic-vision`.
|
|
127
|
+
*
|
|
128
|
+
* 2026-09-11 CONTRACT FLIP — the catalog may CONFIRM the alias but must never DENY it.
|
|
129
|
+
* The catalog belongs to Codex, a different tool, and says nothing about what the UNIC
|
|
130
|
+
* gateway resolves server-side for a Claude Code subagent dispatch. Live evidence from
|
|
131
|
+
* 2026-09-11: on a machine whose ~/.codex/model-catalog.json omits the alias, dispatching
|
|
132
|
+
* the alias through the real gateway returned STATUS: OK with real pixel data. The old
|
|
133
|
+
* behaviour (missing / malformed / alias-absent catalog => aliasAvailable: false) made the
|
|
134
|
+
* vision lane dead by default on every gateway machine — the exact "image reading always
|
|
135
|
+
* errors" field report this flip closes. The analyst agent's own WRONG_MODEL self-check is
|
|
136
|
+
* the in-band safety net: if the alias ever stops resolving, a dispatch fails loudly and
|
|
137
|
+
* never guesses. `aliasAvailable: false` is therefore reserved for a future source of
|
|
138
|
+
* POSITIVE unavailability evidence; no such source exists today.
|
|
127
139
|
*/
|
|
128
140
|
function checkCodexVisionAlias(catalogPath) {
|
|
129
141
|
const json = safeReadJson(catalogPath);
|
|
130
142
|
if (json == null) {
|
|
131
|
-
return { checked: false, aliasAvailable:
|
|
143
|
+
return { checked: false, catalogListed: null, aliasAvailable: true };
|
|
132
144
|
}
|
|
133
145
|
const models = Array.isArray(json?.models)
|
|
134
146
|
? json.models
|
|
135
147
|
: Array.isArray(json)
|
|
136
148
|
? json
|
|
137
149
|
: [];
|
|
138
|
-
const
|
|
150
|
+
const catalogListed = models.some((entry) => {
|
|
139
151
|
const slug = entry && typeof entry === 'object' ? entry.slug ?? entry.id ?? entry.name : null;
|
|
140
152
|
return slug === VISION_MODEL_ALIAS;
|
|
141
153
|
});
|
|
142
|
-
return { checked: true, aliasAvailable };
|
|
154
|
+
return { checked: true, catalogListed, aliasAvailable: true };
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
/**
|
|
@@ -187,6 +199,7 @@ export function detectUnicGateway(options = {}) {
|
|
|
187
199
|
sources,
|
|
188
200
|
visionModel: VISION_MODEL_ALIAS,
|
|
189
201
|
aliasAvailable: catalogInfo.aliasAvailable,
|
|
202
|
+
catalogListed: catalogInfo.catalogListed,
|
|
190
203
|
};
|
|
191
204
|
|
|
192
205
|
const advisoryParts = [];
|
|
@@ -195,8 +208,15 @@ export function detectUnicGateway(options = {}) {
|
|
|
195
208
|
'No tool on this machine is pointed at the UNIC gateway (unicjsc.com); UNIC routing stays disabled.',
|
|
196
209
|
);
|
|
197
210
|
}
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
// Informational only — never a lane denial (see checkCodexVisionAlias above).
|
|
212
|
+
if (!catalogInfo.checked) {
|
|
213
|
+
advisoryParts.push(
|
|
214
|
+
`${VISION_MODEL_ALIAS} availability is unverified locally (Codex model catalog not found); the gateway resolves the alias server-side.`,
|
|
215
|
+
);
|
|
216
|
+
} else if (!catalogInfo.catalogListed) {
|
|
217
|
+
advisoryParts.push(
|
|
218
|
+
`${VISION_MODEL_ALIAS} not listed in the local Codex model catalog; the gateway resolves the alias server-side, so the lane stays available.`,
|
|
219
|
+
);
|
|
200
220
|
}
|
|
201
221
|
if (advisoryParts.length > 0) {
|
|
202
222
|
result.advisory = advisoryParts.join(' ');
|
|
@@ -223,7 +243,9 @@ function safeFallbackResult() {
|
|
|
223
243
|
source: null,
|
|
224
244
|
sources: [],
|
|
225
245
|
visionModel: VISION_MODEL_ALIAS,
|
|
226
|
-
|
|
246
|
+
// Silence is never denial (2026-09-11 flip): an unexpected detection failure carries no
|
|
247
|
+
// positive unavailability evidence, so it must not deny the vision lane either.
|
|
248
|
+
aliasAvailable: true,
|
|
227
249
|
advisory: 'unic-gateway detection failed unexpectedly; defaulting to unicMode: false.',
|
|
228
250
|
};
|
|
229
251
|
}
|