Skip to content

fix: make multi-writer artifacts readable, and stop destroying codex error causes - #270

Merged
saucam merged 1 commit into
mainfrom
fix/live-run-blackboard-slot-and-codex-errors
Jul 29, 2026
Merged

fix: make multi-writer artifacts readable, and stop destroying codex error causes#270
saucam merged 1 commit into
mainfrom
fix/live-run-blackboard-slot-and-codex-errors

Conversation

@saucam

@saucam saucam commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Two defects found by the first live end-to-end run of a collaborative session with real models: a mixed fleet (claude opus 4.8 / sonnet 5, codex gpt-5.6-sol) where an orchestrator drove a cross-vendor panel.

Neither was reachable from the test suite as written. One of them a test was actively enforcing.


1. blackboard_read could not address a multi-writer artifact

findings is a MULTI_WRITER kind — each reviewer writes its own slot. So blackboard_index correctly reported findings in slot review, and blackboard_read had no slot parameter, read the singleton slot, and answered "No findings has been written on this goal yet."

The index advertised something the reader could not fetch.

Live: the orchestrator saw the artifact, tried six times, was told it didn't exist, concluded "Blackboard still unreadable on a third attempt", and abandoned the board — routing around the entire typed-handoff mechanism the feature exists for.

read now takes an optional slot, and a slotless read that finds nothing points at the slots that do exist rather than claiming absence. The asymmetry with write is unchanged and deliberate: §4 gives write no slot so a writer cannot name a peer's and overwrite it. The service always supported read(kind, slot) — only the tool schema was missing it.

A test was enforcing the bug

blackboard-mcp.test.ts asserted that no tool accepts a slot — stricter than the design, which forbids it on write only. That over-assertion is why reads were slotless, and it would have rejected this fix. It now states the invariant per tool: no tool takes a goal/session id (scope rides the token), write never takes a slot, read does.

2. Codex errors arrived as [object Object]

The error notification carries an object; the handler did (params.message ?? params.error) as string then String(...). Every codex failure reported the literal [object Object] — the cause destroyed at the one place whose job was to report it.

Live: a codex role-child failed twice. Nothing in the transcript, audit log, or daemon log said why, and the orchestrator could only report "identical bare worker turn ended in error, no diagnostic detail."

describeCodexError now handles a direct string, a bare string error, a JSON-RPC-shaped {code, message}, and falls back to bounded JSON — tolerating shape rather than asserting one, since a wrong assumption costs the whole diagnostic, which is what happened.

With it, the same run reported:

codex: {"type":"error","status":400,"error":{"type":"invalid_request_error",
  "message":"The 'gpt-5.6-sol' model requires a newer version of Codex.
             Please upgrade to the latest app or CLI and try again."}}

The codex failure itself is environmental — the installed CLI (client 0.145.0) predates the model its own cache advertises. Not a codeoid bug, and now diagnosable in one read instead of not at all.


Verification

Daemon suite 2172 → 2191. Both fixes mutation-checked: removing slot from the read schema fails the multi-writer read test; reverting the error extraction fails three of the five codex cases.

The codex error path had no test at all before this — which is how [object Object] shipped. It has five now, including one asserting that string never appears for any input shape.

What the live run confirmed working

Not part of this fix, but it's the first time any of it ran against real models:

  • an orchestrator on opus chose fleet_panel itself and fanned one brief to two children on different vendors
  • the R3 gate fired per dispatch with the cost roll-up attached, accumulating across dispatches: $0$0.68$1.39
  • the barrier behaved exactly as designed, twice — group_waiting done=1/2 then group_joined members=2 failed=1: it absorbed the first completion, did not fire early, and joined on all-terminal with the failed member reported rather than hanging on it
  • after the slot fix, the reviewer's findings read back through the orchestrator's mount, closing the write-then-read handoff

🤖 Generated with Claude Code

…error causes

Both found by the first live end-to-end run of a collaborative session with
real models — a mixed fleet (claude opus 4.8 / sonnet 5, codex gpt-5.6-sol)
where an orchestrator drove a cross-vendor panel. Neither was reachable from
the test suite as written.

## blackboard_read could not address a multi-writer artifact

`findings` is a MULTI_WRITER kind: each reviewer writes its own slot. So
`blackboard_index` correctly reported `findings` in slot `review` — and
`blackboard_read` had no `slot` parameter, read the singleton slot, and
answered "No findings has been written on this goal yet."

Live consequence: the orchestrator saw the artifact in the index, tried to
read it six times, was told it did not exist, concluded "Blackboard still
unreadable on a third attempt", and abandoned the board — routing around
the entire typed-handoff mechanism the feature exists for.

The index advertised something the reader could not fetch.

`read` now takes an optional `slot`, and a slotless read that finds nothing
POINTS AT the slots that do exist instead of claiming the artifact is
absent. The asymmetry with `write` is deliberate and unchanged: §4 gives
`write` no slot so a writer cannot name a peer's and overwrite it. The
service always supported `read(kind, slot)`; only the tool schema was
missing it.

### A test was enforcing the bug

`blackboard-mcp.test.ts` asserted that NO tool accepts a `slot` — stricter
than the design, which forbids it on `write` only. That over-assertion is
why reads were slotless, and it would have rejected this fix. It now states
the invariant per tool: no tool takes a goal or session id (scope rides the
token), `write` never takes a slot, `read` does.

## codex errors arrived as "[object Object]"

The `error` notification carries an object, and the handler did
`(params.message ?? params.error) as string` then `String(...)` — so every
codex failure reported the literal `[object Object]`. The cause was
destroyed at the one place whose job was to report it.

Live consequence: a codex role-child failed twice with `Error: [object
Object]`. Nothing in the transcript, the audit log, or the daemon log said
why, and the orchestrator could only report "identical bare `worker turn
ended in error`, no diagnostic detail".

`describeCodexError` now handles a direct string, a bare string error, a
JSON-RPC-shaped `{code, message}`, and falls back to bounded JSON — chosen
to tolerate shape rather than assert one, because a wrong assumption here
costs the whole diagnostic, which is exactly what happened.

With it, the same run reported:

    codex: {"type":"error","status":400,"error":{"type":"invalid_request_error",
      "message":"The 'gpt-5.6-sol' model requires a newer version of Codex.
                 Please upgrade to the latest app or CLI and try again."}}

The codex failure itself is environmental — the installed CLI (client
0.145.0) predates the model, which its own cache still advertises. Not a
codeoid bug, and now diagnosable in one read instead of not at all.

## Verification

Daemon suite 2172 to 2191. Both fixes mutation-checked: removing `slot`
from the read schema fails the multi-writer read test, and reverting the
error extraction fails three of the five codex cases.

The codex error path had NO test before this — which is how `[object
Object]` shipped. It has five now, including one that asserts the string
`[object Object]` never appears for any input shape.

## What the live run confirmed working

Not part of this fix, but it is the first time any of it ran with real
models, so worth recording:

- an orchestrator on opus chose `fleet_panel` itself and fanned one brief to
  two children on different vendors
- the R3 gate fired per dispatch with the cost roll-up attached, and the
  roll-up accumulated across dispatches ($0 to $0.68 to $1.39)
- the barrier behaved exactly as designed, twice:
  `group_waiting done=1/2` then `group_joined members=2 failed=1` — it
  absorbed the first completion, did not fire early, and joined on
  all-terminal with the failed member reported rather than hanging on it
- after the slot fix, the reviewer's `findings` artifact read back through
  the orchestrator's mount, closing the write-then-read handoff
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@saucam
saucam merged commit b24a313 into main Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants