No worker code changed. This is the evidence from the 2026-08-12 16:39 and 17:38 runs, and where each finding now lives. The fourth session's four identity changes all work on real panels. Panel 7's two wrong bindings are gone. The lead going unassigned there is correct and was measured, not assumed: face_detect finds one face on the whole panel at conf 0.599, nothing else above 0.056 even at a 0.04 threshold, and the crop shows him drawn from behind. Two decisions, both closed: a roster name is a guess so it never reaches detection, and merged_into is exactly one hop deep. Two caveats, both open: detection can order a bbox backwards (1 in 117), and identity coverage has fallen on every run since the gate landed (70 -> 61 -> 50). Coverage is the thing to settle next, and not by reading the number. identity_labels already holds 145 rows of ground truth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.6 KiB
HANDOFF, 2026-08-12 (fifth session)
Live state is in NEXT.md. This file is only what this session did. The previous handoff is in
JOURNAL.md.
Asked
"go ahead" on the GPU cycle the fourth session left staged. Then "what do we do now". Then go ahead on the fixes its evidence asked for, and run it again.
Result
Two full GPU cycles ran. All four of the fourth session's identity changes are now proven on real panels. Four more fixes were written on top, all orchestrator-side, all deployed. Coverage fell twice and that is the open question.
| cycle | detections | assignments | coverage | registry | named |
|---|---|---|---|---|---|
| baseline (13:11, pre-change) | 110 | 77 | 70% | 8 | 1 |
| run A (16:39-16:45) | 110 | 67 | 61% | 16 | 2 |
| run B (17:38-17:44) | 117 | 59 | 50% | 20 | 2 |
Vision is non-deterministic, so detection counts move between runs. Each cycle is ~6 minutes: vision ~3m50s, identity ~1m25s, reconcile ~50s.
First, a correction the session started with
The vision/identity/reconcile timestamps in /job/status are UTC. The git log is local, UTC+4. The run
that looked like a completed rerun was the pre-change baseline: it finished 13:17 local, and
_mark_has_face was not committed until 19:07. The fourth session's handoff was right that nothing had
run.
Run A: the four staged changes, verified
| change | verdict |
|---|---|
has_face stamp |
on all 110 detections, 67 true / 43 gated (39%) |
| extras gate | panel 7's two wrong bindings gone |
| NONE mints | 16 characters where 8 existed |
| non-destructive merge | 7 rows carry merged_into, 9 assignments stamped merged_from: |
Panel 7, the worked example:
person_1 Seonho 1.00 -> -- none -- has_face=False
person_2 character_f7a4fd 0.00 -> character_519d2b 0.00 has_face=True
person_4 character_d72710 0.94 -> -- none -- WRONG binding removed
person_5 Seonho 1.00 -> -- none -- WRONG binding removed
person_1 going unassigned is correct, and this was measured, not assumed. Ran face_detect.detect_faces
on p006.png directly: one face on the whole panel, conf 0.599, inside person_2. Swept the threshold to
0.04 and nothing else appears above 0.056. The crop shows the lead drawn from behind at his desk, back of
the head and headphones. Detector right, gate right, lead unidentifiable in that panel.
person_1's box now frames the lead. The old "empty window mullion" note was written against pre-fix
boxes and is retired.
Run A's new defect: a roster hint named the wrong man
Verified against the art, three real people:
character_92129ac7"Lim Seonho", 22 assignments. The p010 caption readsLIM SEONHO (29), yellow plaid shirt, headphones, matching the roster's description. Correct.character_556aef60, 25 assignments, unnamed. The woman with short black hair and pearls, the roster's second character, whom the roster itself calls "Unknown". Correct.character_dbadfff7"Seonho", 15 assignments. A different man, glasses, dark clothing, wearing the lead's roster name.
p020 assigned Seonho to person_1 and Lim Seonho to person_3, two people in one panel.
What was changed, and where the cause was
Roster hints no longer seed detection. Deleted the two lines at service.py:882 that appended
_roster_char_hints to known_characters. build_detect_prompt drops any hint without a name. Passing a
nameless hint would have contributed nothing, so removing the seeding was the only real option.
Names now reach detection from registry rows only, which carry embeddings and were named from an in-panel
caption or address. The roster still feeds roster_cast in run_stage_dialogue, where names match
against speech rather than faces.
merge_characters keeps merged_into one hop deep. Two halves, one per direction, and the second was
missed on the first attempt:
- resolve the keeper to its chain root before merging (a bounded 64-step walk, the cap only so a cycle cannot hang reconcile)
- repoint the loser's own dependents to the keeper when the loser is retired
The keeper walk alone does not work. Run B still produced 477c1894 -> a92d9df4 -> 4fb94c15. At merge
time that pair was fine. The chain formed later, when a row that was already somebody's keeper was itself
retired.
reset_registry deletes name_claims for the manga's panels and reports the count. Confirmed live:
run B's reset reported name_claims: 5, the orphans that had pointed at character_afa7623b since two
resets earlier.
audit_registry.py is in the repo. It had only ever been docker cp'd, so every rebuild dropped it.
Dockerfile has COPY . ., so it is baked now and that trap is gone.
Run B: what the fixes did
The registry split is fixed. "Lim Seonho" came back as ONE row holding 25 assignments. That answers the open risk from before the run: dropping the roster hint did not split the lead across the panels before his caption. "Seonho" fell from 15 assignments to 1.
Two things run B surfaced:
- A second naming mechanism, untouched by the fix. Even with no roster hint, p011 and p026 emit
name: "Seonho"onperson_2from in-panel text. Detection reads a name off the panel and attaches it to the wrong body. That is what keeps the glasses man named, now at 1 assignment instead of 15. - One degenerate bbox.
p007 person_1is[226, 417, 130, 551], x1 > x2. One in 117. It crops to nothing, so that detection can never enroll or match._bbox_to_pixelsclamps each coordinate but never orders the corners.
Not done
- The degenerate-box guard. It is a worker change (
worker_vision.py:_bbox_to_pixels), so it needs a vision worker restart and a third GPU cycle to prove. Not started, nothing half-edited. - The coverage trend. 70% -> 61% -> 50%. Part is the
has_facegate, which is stable at ~39% of detections across both runs. Framed against face-bearing detections only, run B assigned 59 of 72, or 82%. Nothing establishes whether the rest is correct abstention or lost cast. Settle that number before trusting the registry. - Nothing downstream re-ran. The job is parked at
dialogue waitingwith every stage below it cleared. No clip or chapter has been rebuilt against the new cast.
Checks
/usr/bin/ssh kami@192.168.1.104 "cd /home/kami/docker-apps/manga-infra/orchestrator && python3 -m pytest -q --ignore=test_api.py" # 118 passed, was 116
./check_stale.sh # exit 0, all 9 workers current
Both new merge tests were confirmed to fail with their fix disabled, then the fix was restored and the
suite re-run. The deployed container was verified by parsing its source, not grepping it. The first
attempt gave a false negative, matching _roster_char_hints inside the comment that explains its removal.
Next command
The third cycle, after adding the corner-ordering guard to _bbox_to_pixels and restarting the vision
worker. Watch coverage against 50% and the degenerate box count against 1.
cd /home/kami/Programs/n8n-worker && ./check_stale.sh # must exit 0
J=778297bc-e7ce-439d-91b5-8a027060d17f; M=ef105a86-4b7e-4ac4-b45c-b7d83b8f5b5e
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/characters/reset -H 'Content-Type: application/json' -d '{\"manga_id\":\"$M\",\"confirm\":true}'"
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/stage/clear -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"vision\"}'"
# restart the identity worker here, see the trap below
for S in vision identity reconcile; do /usr/bin/ssh kami@192.168.1.104 "curl -s --max-time 5400 -X POST http://127.0.0.1:9090/stage/run -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"$S\"}'"; done
/usr/bin/ssh kami@192.168.1.104 "docker exec manga-orchestrator python3 /app/audit_registry.py 7c944dd4-e972-42c7-ba60-9f6939548e80 6"
Traps confirmed or found
kill $(pgrep -f "worker_identity:app")kills the shell running it. The pattern matches the calling command line. It killed this session's own script mid-way, so thesend-keysrelaunch never fired and the identity worker sat dead. Restart it with the twotmux send-keyslines only, then poll/health.- Verify deployed code by parsing it, not by grepping it. A comment explaining a removal contains the name of the thing removed.
- A test that cannot fail proves nothing. Both merge tests were run with their fix disabled first.
- Panel-7 line numbers move between runs, because vision is non-deterministic and
local_idis assigned top-to-bottom per run. In run B the lead isperson_5, notperson_1. Compare by bbox, not by id.