Skip to content

fix: a failed turn keeps what it delivered, the system prompt holds still, a turn's words reach its next round, and wryme is held to the specs it speaks - #69

Merged
adiled merged 3 commits into
mainfrom
fix/delivery-survives-failed-turns
Oct 28, 2026
Merged

adiled merged 3 commits into
mainfrom
fix/delivery-survives-failed-turns

Conversation

@adiled

@adiled adiled commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Why

Running wryme against hum's claude-cli models showed four wryme-side problems:

  1. Deliveries were lost when a turn failed. A finished job's result or a window's note is recorded on the turn's assistant row as a tool call and result (due_calls). If the request failed, finish_streaming treated the row as an empty reply and deleted it. The item had already been claimed from its queue, so it was gone for good.

  2. The system prompt changed every minute. Open book pages render Last inked: 7m ago into the system prompt on every request, so no two requests had the same system prompt. Prompt caches missed, and hum started a new claude session each turn.

  3. Responses protocol: pages said twice, prod dropped. instructions was the first system message, which is the first page when the station has no prompt of its own. The pages were then added again as input items, so each page appeared twice, and the bookkeeping prod (already taken by api_messages) was dropped. In a warm window, the page items were also stored again on every turn.

  4. Responses, full window: nothing finished was ever carried. The response.output_item.done handler read output_item, a field no server sends (the API's is item, which the output_item.added handler already reads). Reasoning, mcp_call and hosted items were never carried into the next round, and a call the server ran never showed its result. The tests passed only because their fixtures used the same wrong name. The words the model said before a tool call weren't carried at all, so within a turn a stateless endpoint never saw them and the next turn replayed them; to hum the conversation looked edited, and it began a new claude session. Argument deltas read output_item_id alone, where the API sends item_id.

  5. Fixtures agreed with bugs. No test checked wryme against the specs it speaks, so problem 4 hid behind fixtures with the same wrong field name. An audit against both specs turned up more (below).

What changes

  • finish_streaming keeps a turn's rows that hold tool events, so a delivered result or note stays in history after a failed turn. A turn with nothing at all is still dropped.

  • The preamble shows when a page was last inked as a local time (2026-10-28 20:31), so it stays the same until the page is inked again. The book open tool result still shows the age.

  • Responses: instructions is every system message joined (the station prompt, open pages, prod), the same as the chat protocol's system prompt. Pages are no longer added again as input items.

  • Responses: a finished item is read from item. An assistant message is carried into the next round like reasoning, so the model's words before a call stay in the turn. Argument deltas are matched by item_id.

  • Specs, pinned, in spec/: OpenAI's OpenAPI (openai/openai-openapi @ 26a2991) cut to the 392 schemas wryme's clients reach, and Open Responses (openresponses/openresponses @ 8907bc3, 2026-04-24) whole. spec/pull fetches them again.

  • Tests held to them: src/spec.rs (test-only, jsonschema as a native dev-dependency) checks every fixture event, every chat chunk and every request wryme builds (full and warm windows, tools, history, deliveries) against both specs. Fixtures are built by constructors that check themselves; anything beyond both specs is marked beyond_spec. The checker's own tests prove it rejects output_item, output_item_id, a missing sequence_number and summary: "none".

  • What the audit fixed:

    Spec says wryme did Now
    reasoning.summary is auto/concise/detailed Hush sent "none" Hush asks for no summary
    max_tool_calls caps built-in tools only, and is ≥ 1 in Open Responses sent 0 at the tool depth limit, so function calls kept coming tool_choice: "none"
    message items have type: "message" (Open Responses) bare {role, content} typed
    with store: false, a reasoning item needs encrypted_content, and a server id can't be resolved would carry both as received reasoning carried only with its encrypted content; words as a plain assistant message
    MCP argument events are response.mcp_call_arguments.* listened for mcp_call.arguments.* spec names
    Open Responses streams raw reasoning as response.reasoning.delta ignored read as Heart
    a failed mcp_call's error is an object looked up a field that isn't there shown as its words
    a server-run function call is reported under its call_id under its item id call_id
  • Kept on purpose: the Open Responses effort enum lacks minimal and max (Swift and Max stations); Chat reasoning_content, multi_agent items, and function calls the provider ran are extensions beyond both specs.

Testing

  • cargo test: 179 pass, twice. New tests: a_failed_turn_keeps_what_it_delivered (fails without the fix), an_open_page_is_said_the_same_until_inked_again, every_system_message_is_said_once_as_instructions, the_words_said_before_a_call_are_carried_into_the_next_round, and arguments_stream_in_by_their_item_id. Every fixture is now built by constructors checked against the specs.
  • cargo clippy --all-targets and cargo check --target wasm32-unknown-unknown are clean.

Not included

…ds still

A turn that delivers a finished job's result or another window's note
records it on the turn's assistant row as a tool call and its result. When
that turn's request failed, finish_streaming dropped the row as an empty
reply, and the job or note was gone: it had already left its queue. A row
with tool events now survives a turn with no reply.

Open book pages rode in the system prompt with "Last inked: 7m ago", an
age that changed every minute, so no two requests shared a system prompt:
prompt caches missed, and hum began a new claude session each turn. The
preamble now gives the time a page was last inked.

On the Responses protocol the instructions were the first system message,
which is the first open page when a station has no prompt of its own, and
the pages were then added again as input items, so a page was said twice
and a bookkeeping prod was dropped. The instructions are now every system
message, as the chat protocol's system prompt reads them, and pages are
said once. A warm window no longer stores the pages again each turn.
…and the words said before a call reach the next round

The handler for response.output_item.done read `output_item`, a field no
server sends: the API names it `item`, as the handler for
output_item.added already reads it. So in the full window nothing
finished was ever seen. Reasoning, mcp_call and hosted items were never
carried into the next round, a call the server ran never showed its
result, and the tests passed only because their fixtures used the same
wrong name. Argument deltas read `output_item_id` alone, where the API
sends `item_id`.

The words the model said before a tool call were never carried either,
so within a turn a stateless endpoint never saw them, and the next turn
replayed them: the conversation looked edited between the two, and hum
began a new claude session for it. An assistant message item is now
carried into the next round like reasoning.
@adiled adiled changed the title fix: a failed turn keeps what it delivered, and the system prompt holds still fix: a failed turn keeps what it delivered, the system prompt holds still, and a turn's words reach its next round Oct 28, 2026
…and its tests are held to them

The specs ride in spec/, pinned: OpenAI's OpenAPI cut to what wryme's
clients reach, and Open Responses whole; spec/pull fetches them again.
Every test fixture, event and request is checked against them
(src/spec.rs), so a fixture can no longer agree with a bug, as the
`output_item` ones did.

What the specs found:
- Hush asked for a reasoning summary of "none", which no spec allows; it
  now asks for none.
- At the tool depth limit wryme sent max_tool_calls: 0, which caps only
  built-in tools and is below Open Responses' minimum; function calls
  kept coming. It now sends tool_choice: "none".
- Message items went without `type`, which Open Responses requires.
- With store false, a carried reasoning item without encrypted_content,
  or a message item with its server id, cannot be resolved; reasoning
  is carried only with its encrypted content, and words as a plain
  assistant message.
- MCP argument events are response.mcp_call_arguments.*, not
  mcp_call.arguments.*.
- Open Responses streams raw reasoning as response.reasoning.delta.
- A failed mcp_call's error is an object, shown as its words.
- A call the provider ran reports its result under its call_id.
@adiled adiled changed the title fix: a failed turn keeps what it delivered, the system prompt holds still, and a turn's words reach its next round fix: a failed turn keeps what it delivered, the system prompt holds still, a turn's words reach its next round, and wryme is held to the specs it speaks Oct 28, 2026
@adiled
adiled merged commit 752b6b6 into main Oct 28, 2026
@adiled
adiled deleted the fix/delivery-survives-failed-turns branch October 28, 2026 21:48
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.

1 participant