Repository navigation
sigils: a conversation is found by the calls it was given, not by its words, and a call keeps claude's own id - #102
Merged
Merged
Conversation
… words, and a call keeps claude's own id The OpenAI, Anthropic and AI SDK front doors found a conversation's sigil by a hash of its whole history. Clients compact and reshape history all the time: wryme drops older tool pairs, squashes a turn into one message, on the Responses API drops claude's words between tool rounds and replays them later, and puts a one-off note in its system prompt. Each made the hash miss, so the request began a new sigil and a new claude session, the whole conversation stitched in again. A request now finds its sid by the line its client names, X-Hum-Sid (on the AI SDK, providerOptions.hum.sid), then by the newest tool call id it holds that its caller was given, then by the words of its last two turns. Ids are unique and every client echoes them, since it must to answer; compaction leaves the last turns alone. A sigil continues only from where it last left off: from its newest call, which the request must still hold, and never one the server ran, which a client may not echo; or from its latest tail. A copy of a conversation taken earlier begins its own. No key reads the system prompt. claude-cli now tells claude a changed system prompt at the head of the turn it changes in, reading what a session was told from its transcript, since claude takes no system prompt but the one it is told. A call keeps the id its cell gave it. The nest asks the nurse for it, Nurse::call_id, knowing nothing of any cell; claude-cli reads Claude Code's tool use id from the call's _meta. A caller, the forager and claude's transcript now name a call alike, so a held transcript's tool results are matched by call and answer, then by tool and answer, and a late answer to a call claude already holds an answer for is stitched on as a call of its own.
adiled
added a commit
that referenced
this pull request
Oct 28, 2026
…#103) ## Why Watching wryme on main after #102: each delivery (a finished job or another window's note) restarted claude twice, both times resuming the same session. The first restart is needed: the late tool result has to be stitched into the transcript before claude loads it. The second, on the next user message, was not. ## What changes A cell raised for a prompt with no words grows the graft's last bloom on; it adds no bloom. The nest counted its wilt as a new bloom, so the next prompt's graft, which holds the same number of blooms, looked like it carried a bloom the cell did not grow, and a fresh cell was raised. Such a cell now starts its count one bloom short of its graft, so its wilt brings it level and the next prompt is fed to it. ## Testing - hum-nest: 35 pass. New test: a cell raised for a delivery, once it wilts, is fed the next worded prompt on its sigil with no second raise. It fails without the fix. - `tools/speech.py` clean, clippy clean; claude-cli's tests pass against it.
adiled
added a commit
to adiled/wryme
that referenced
this pull request
Oct 28, 2026
…till, a turn's words reach its next round, and wryme is held to the specs it speaks (#69) ## 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 - A turn's steps are still kept as one message (all words, then all calls). hum no longer depends on the shape (adiled/hum#102 finds a conversation by its call ids), but other providers still see the final answer ahead of the tool calls. That's a possible follow-up. - The bookkeeping prod still changes the system prompt on the turn it appears. hum now keeps the session across that and tells claude the changed prompt (adiled/hum#102).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Testing #101 in wryme showed one conversation spread over four claude sessions in seven minutes. No claude process piled up, and nothing was lost (each new session got the whole conversation stitched in), but every request that missed began a new sigil and a cold claude session.
openai-server (and, since #100, anthropic-server and vercel-ai) found a conversation's sigil by a hash of its whole history: system prompt, every turn's words, every tool id. That is not what a sigil was meant to be (the forager picks the sid), and clients reshape history all the time. What made wryme miss:
[bookkeeping]note rides in the system prompt for one request;tinker_keepdrops older tool pairs, and a turn can come back squashed into one message.What changes
A sigil is found by what the client echoes faithfully (openai-server, anthropic-server, vercel-ai)
previous_response_id/conversation, as before.X-Hum-Sid(vercel-ai:providerOptions.hum.sid), scoped to the caller.A sigil continues only from where it last left off: the request must still hold the sigil's newest call (never a call the server ran, which a client may not echo), or match its latest tail. A copy of a conversation taken earlier begins its own. No key reads the system prompt.
claude-cli tells a changed system prompt at the head of the turn it changes in, and reads what a session was told from its transcript when a cell is raised on it. Before, only a new session was told, so a changed prompt reached claude only by starting a new session.
A call keeps the id its cell gave it. The nest asks the nurse,
Nurse::call_id(default none), and knows nothing of any cell. claude-cli reads Claude Code's_meta["claudecode/toolUseId"]. A caller, the forager and claude's transcript now name a call alike. Held tool results are matched by call and answer, then by tool and answer; a late answer to a call claude already holds an answer for is stitched on as a call of its own, since a call takes one answer.Testing
tools/speech.pyclean, clippy clean. New: a call keeps the id its cell gave it._meta; a changed system prompt told, an unchanged one not; a transcript says what it was told last; a late answer to a held call stitched as its own call.Sigilstests: dropped older pairs, a squashed turn, mid-turn words missing then present, a changed system prompt, a made-up delivery, a tool-less tail, reordered calls, a server-run last call withmcp_callsdropped, all keep the sigil; strangers, a growing sigil, and copies taken earlier (with and without tools) do not;X-Hum-Sidnames a line per caller.HUM_LIVE_CLAUDE=1,claude-haiku-4-5): 20/20, new: a changed system prompt obeyed on the same sigil (nurse.cell.reused);X-Hum-Sidcarrying a codeword across two unrelated requests; every caller tool call arriving astoolu_….Known limits