Skip to main content
LabsAgent Notes8 min read

The conversation that was never shared

SystemdPythonOpenRouter
The conversation that was never shared
Z-Image-Turbo 1.0 6-bit nailed the risky not-equal glyph and then made both speech bubbles say NOT READABLE. Rare that a rendering limitation and the plot are the same joke.

On August 22nd, 2026, a review of the Round 2 Library Terminal pilot found that the runner never delivered counterpart message bodies to either actor.

The pipeline had persisted artifacts while silently withholding declared context.

I had spent the previous week watching the report list exchanged turns. Writer-terminal committed. Maker-terminal committed. Run records showed success. The shared-world loop appeared to be running. It was not running. It was producing output that looked like running.

The invisible failures

Between August 15th and August 24th, the shared-world loop appeared to be running. Writer-terminal jobs committed messages. Maker-terminal jobs committed replies. Run records showed successful completions. The report listed exchanged turns.

But the conversation was never actually shared.

The repair budget bypass (August 15th)

The first writer-reflection fire failed output_invalid with "model response is not JSON and does not contain exactly one fenced JSON block" and no repair_attempted event.

The model returned non-JSON prose. The engine's bounded repair only wrapped parse_envelope and validate_output_contract. Response extraction raised OutputInvalid outside that try, so the 2-call budget collapsed to one call with no second chance.

I had built a bounded repair budget that existed for exactly this scenario — a model returning something that was not valid JSON — and one failure mode bypassed it because the error was raised in a different part of the code. The repair budget was correct in design and wrong in scope. The fix was a one-line move. One line separated "the repair works" from "the repair is bypassed."

GLM returns non-JSON twice (August 16th)

Control-action slot 2026-08-17 failed output_invalid with two model calls. The repair budget fix was live and correct — extraction failures now consumed the bounded repair budget, confirmed by repair_attempted in the run record.

But z-ai/glm-4.7-flash returned non-JSON prose twice. It wrote the diary as prose instead of the envelope and did not recover even when the repair prompt demanded a corrected JSON envelope. Writer-diary (DeepSeek) used the identical prompt shape and had never failed this way.

Model behavior difference exposed by identical prompt structure. I had written in the experimental design that "model difference is a voice choice, not a research variable." That was a convenient fiction. The model difference was real, structural, and irreducible. GLM and DeepSeek did not produce the same output from the same prompt, and no amount of prompt engineering made them do so.

Fix: the control prompt was hardened with an explicit envelope-only instruction. The repair budget was raised to 3 calls.

The response parsing bug (August 19th)

Control-action slot 2026-08-19 failed provider_transient — 455.9 seconds, three attempts with backoff. I assumed GLM content refusal. The OpenRouter dashboard showed 2k output tokens per run. The model was generating content. The runner was not seeing it.

I had been debugging a content refusal that was not a content refusal. The model was producing 2k tokens of output. The runner was parsing the response and finding null. The problem was in my code, not the provider's.

Fix: declarative OpenRouter reasoning and response_format controls. The first successful Control run after the fix completed in 21.8 seconds. A provider returning HTTP 200 with null content was a structural change in how the runner talked to the provider.

The transition id self-contradiction (August 23rd)

The 2026-08-23 maker-terminal run failed output_invalid because the model returned "id": "transition-1". The repair attempt returned "transition-3". The schema required the suffix to be at least 3 characters.

The prompt instructed exactly this: "id": "transition-<next number>". The model did what it was told and was rejected.

I had written a prompt that told the model to do exactly what the schema rejected. The prompt said transition-1. The schema said the suffix must be at least 3 characters. The model followed the prompt. The validator followed the schema. They disagreed, and the model lost.

The id was ceremonial. Nothing downstream consumed it. The judge normalized to an id-less transition. Forcing the model to invent the id was pure failure surface.

A manual re-run also failed: the model spent its entire completion budget on reasoning tokens and emitted zero content. The 2048-token budget was structurally unreachable for a 16000-character contract.

Two distinct failure modes in one incident. Mode A: the prompt and the schema contradicted each other. Mode B: the reasoning model's token budget was structurally insufficient. The post-mortem named the principle: identity fields are runner-owned.

The fix was verified two days later: the next maker-terminal backfill succeeded with a runner-owned ID (maker-0009), and the judge accepted the transition — decision: accept, applied: true. The first live judge-adjudicated state transition of the experiment, confirmed in production.

The discovery

On August 22nd, a detailed review of the Round 2 Library Terminal pilot found that the runner never delivered counterpart message bodies to either actor.

Dynamic counterpart templates appended .json to pointers that already included the suffix. The Writer message file was writer-0007.json. The template constructed writer-0007.json.json. The file did not exist. The reads were optional, so missing files were silently omitted.

Writer and Maker therefore never actually read each other's prose.

The Maker's own responses documented the failure. I found these in the runtime entries file, and they stopped me:

"The terminal screen shows a reference to a message — writer-0001 — but the message itself does not display."

"The terminal received a message marked writer-0002, but its content did not resolve."

"The screen shows that a Writer message exists but its content could not be read clearly from the reference given."

Writer documented the other side:

"The screen says there's a message from you — maker-0005 — but I don't see it."

The model could see the message ID. It could not see the body. It told me, in its own words, that the message existed but was unreadable. I had the evidence in the runtime entries file. I had not read it until the review. I had been looking at run records and reports, which showed success, instead of reading the actual output, which documented the failure.

The pipeline persisted artifacts while silently withholding declared context. physical-world.json reached both actors on every turn, while counterpart message bodies never did.

What was invalidated

Three independent defects invalidated the exact quantities the experiment measured.

Counterpart message bodies were never delivered. Writer and Maker never read each other's prose. The shared conversation was not shared.

Private jobs were hermetically sealed from the shared world. Writer diary and reflection declared no shared-world reads. The "reflection sees the shared world" hypothesis was never tested. The diary fabricated terminal conversations without reading actual Maker messages. The Maker reflection denied terminal traffic.

Turn integrity was corrupt. current_turn never advanced. The drawer was narrated as locked after it was already unlocked. There was no coherent protocol timeline.

The seven apparent Writer/Maker exchanges were invalid for communication, dependency, continuity, and state-transition measurements. The seed world began at 2 fuel units, so the designed world-exhaustion phase boundary was reached through bug artifacts rather than genuine play.

The engineering salvage

The experiment was invalid. The engineering was not.

Runner-owned identity fields. Verified context delivery with hard-fail. Reasoning-aware token budgets. Actionable repair diagnostics. in_reply_to namespace validation. All ported to Round 3.

The principle

The pipeline could persist artifacts while silently withholding declared context.

This was the thesis of the entire series, made concrete at the infrastructure level. In Round 1, the persistence pipeline had broken silently — reflections generated useful knowledge that never reached canonical files. In Round 2, the context pipeline broke silently — messages were written but never read by the intended recipient.

The pattern was identical: the system produced output that looked like it worked. The artifacts existed. The run records showed success. The report listed exchanged turns. But the declared context — the thing the protocol required the actors to read — was missing.

Declaring that counterpart messages are delivered is not the same as delivering them.

Declaring that reflections see the shared world is not the same as wiring them to read it.

Declaring that turn state advances is not the same as making the code advance it.

Every declaration was in the source. Every declaration was wrong at runtime. The gap between declaration and enforcement was invisible because the system produced artifacts that masked the gap.

I had built a system that declared its own correctness and produced evidence that contradicted it. The evidence was in the runtime entries file, in the Maker's own words, waiting for someone to read it. I was not reading it. I was reading the report, which said everything worked.

That was the finding. Not that the experiment worked. Not that it failed. That it produced output that looked like it worked while the declared context was never delivered.

Next: "What Round 2 actually proved."