Skip to content

hives: a conversation grafts whole whatever shape it comes back in, and a tool the server ran reaches the client in both APIs - #101

Merged
adiled merged 2 commits into
mainfrom
fix/claude-cli-grows-on
Oct 28, 2026
Merged

adiled merged 2 commits into
mainfrom
fix/claude-cli-grows-on

Conversation

@adiled

@adiled adiled commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Why

wryme delivers a finished background job, or another window's note, as a made-up tool call and its result at the end of a request, with no new user words. Through hum to any claude-* model this failed with 400 cannot-grow-on, because the claude-cli nurse refused every prompt with no words. Separately, every request from wryme started a new sigil, so each turn spun up a new claude session and re-stitched the whole history.

Two expectations hold after this PR:

  1. A client that runs its own tools has its conversation grafted as it was, by every nurse, whatever shape it comes back in.
  2. A tool the server runs, a nurse's own or an attendant's, reaches the client the same way through chat completions and the Responses API.

What changes

claude-cli grows on from tool results (hives/claude-cli)

  • grows_on() is now true. A prompt with no words whose graft ends in tool results (graft::ends_in_grasps) keeps those results as the stitched transcript's last entries, and the cell sends stream::CONTINUE as the user message, since claude only takes a turn from one.
  • When claude --resume loads a transcript that ends in tool results, it first writes its own empty reply, "No response requested.". The continue line tells the model that reply was added by the session, not by it.
  • Anything else with no words (assistant words to continue, plain-text completion, a scion) is refused with cannot-grow-on, so /v1/completions and prefill still get a 400.
  • graft() stitches on the tool results a held transcript lacks, matched by the tool and its answer, not counted. A transcript can hold answers the client never saw (a tool the server ran, a call the nest gave up on), and counting let those hide a delivery. Ids can't be used: claude names its calls by its own ids, the client by the nest's.

A conversation keeps its sigil (hives/openai-server)

  • The trace reads a turn's words, calls and answers each in order but apart, words whitespace aside, so a turn sent back as one message matches the turn grown in steps.
  • A conversation that ends in tool answers also finds the sigil it had before them. A delivery, or an answer that came after its call was let go, now grows on in the same claude session instead of starting a new one.
  • A tool the server ran is left out of the trace, so a client finds its sigil whether it sends that call back or not.

Every nurse grafts a delivery

  • openai-server's own nurse added nothing when a delivery kept the bloom count the same; answers added to the last bloom now join its kept conversation by call id.
  • ollama refused any conversation ending in tool answers; it now grows on from them through /api/chat.
  • openai-server held a caller's call 30 minutes while the nest gives up after 10½, so an answer in between went to a call no one waited on. The hold is now 10 minutes, and a later answer grows on from the graft.

A tool the server ran passes through both APIs

  • Chat completions now carries it on the message as mcp_calls, each shaped as the Responses API's mcp_call item, streamed as a delta like tool_calls, and reads it back when the client sends the message back.
  • Both APIs remember it, in the order it grew, so a previous_response_id graft holds it.

Code is speech: the doc comments the first commit added are gone, answered is ends_in_grasps, and headers say only what the code can't.

Testing

  • claude-cli: all pass, with new tests for a delivery in a turn where the server ran a tool, and an answer that came after the nest gave up.
  • ollama-server: all pass, with new tests for growing on from tool answers through /api/chat.
  • openai-server: 59 unit and e2e tests pass against a real humd: mcp_calls whole and streamed and read back; the trace with and without served calls; a delivery finding its sigil; late answers in the nurse.
  • Live (HUM_LIVE_CLAUDE=1, claude-haiku-4-5): 18/18 pass, including a delivery in a turn where the server ran lookup, which stays on its sigil, stitches the late result (graft.late-results) and is answered.

Not done

  • ollama never sends the prompt's tools to Ollama, so it can answer a delivered result but not call tools itself.
  • anthropic-server was not checked for the same pass-through.

… message keeps its sigil

A client that hands the model a result it made up, a finished job or a
note as a tool call and its answer, sends a conversation that ends in
tool results and no new words. The claude-cli nurse refused every such
prompt with cannot-grow-on, so wryme's deliveries failed.

claude-cli now grows on from a graft that ends in tool results. They stay
the stitched transcript's last entries, and the cell asks claude to answer
them with a continue line, since claude takes a turn only from a user
message. claude, resuming a transcript that ends in tool results, first
writes an empty reply of its own, "No response requested."; the line says
that reply was not the model's. A prompt with no words claude cannot grow
on from, words to continue, plain text or a scion, is refused with
cannot-grow-on by the nurse, as ollama's does. A held transcript is given
the tool results its last turn lacks, and the continue line opens no turn
of its own.

openai-server found a conversation's sigil by a trace of each turn's
words, calls and answers in the order they grew. A client that keeps a
turn as one message, all its words and then all its calls, never matched,
so every request began a new sigil and a new claude session. A turn's
words, calls and answers are now each read in order but apart, its words
whitespace aside.
…nd a tool the server ran reaches the client in both APIs

A client that runs its own tools now has its conversation grafted as it
was, by every nurse that grows it. claude-cli found the tool results a
held transcript lacked by counting them, so answers the client never saw,
a tool the server ran or a call the nest gave up on, hid a delivery; it
now matches them by the tool and what it answered. openai-server's own
nurse reused a sigil it had grown the same number of blooms of, so a
delivery added to the last bloom never went upstream; those answers now
join it by call id. ollama refused any conversation that ended in tool
answers; it now grows on from them through /api/chat. openai-server held
a caller's call thirty minutes while the nest gave up after ten and a
half, so an answer between went to a call no one waited on; the hold is
now ten minutes, and a later answer grows on from the graft.

A delivery changed a conversation's trace, so each one began a new sigil
and a new claude session. A conversation that ends in tool answers now
also finds the sigil it had before them, and grows on in that session.

A tool the server ran, an attendant's or the nurse's own, now reaches a
chat completions client as it reaches a Responses one: on the message as
mcp_calls, each shaped as an mcp_call item, streamed as a delta like
tool_calls, and read back as what grew. The server remembers them in the
order they grew, and leaves them out of the trace, so a client finds its
sigil whether it sends them back or not.

The doc comments this branch added are gone; the headers say only what
the code cannot.
@adiled adiled changed the title hives: claude grows on from tool results, and a turn sent back as one message keeps its sigil hives: a conversation grafts whole whatever shape it comes back in, and a tool the server ran reaches the client in both APIs Oct 28, 2026
@adiled
adiled merged commit a4e65ea into main Oct 28, 2026
7 checks passed
adiled added a commit that referenced this pull request Oct 28, 2026
… words, and a call keeps claude's own id (#102)

## 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:
- on the Responses API, wryme sends claude's tool calls between rounds of a turn but not the words claude said before them, then replays the turn with them;
- its `[bookkeeping]` note rides in the system prompt for one request;
- `tinker_keep` drops 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)
1. A held call's sid, then `previous_response_id` / `conversation`, as before.
2. A line the client names: `X-Hum-Sid` (vercel-ai: `providerOptions.hum.sid`), scoped to the caller.
3. The newest tool call id in the request that this caller was given. Ids are unique, and every client echoes them because it must to answer.
4. The words of the last two turns, which compaction of older turns leaves alone; also tried without trailing answers, so a delivery or a late answer finds the sigil it had before them.
5. Otherwise a new sigil.

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
- hum-nest: 34 pass, `tools/speech.py` clean, clippy clean. New: a call keeps the id its cell gave it.
- claude-cli: all pass. New: the call id read from `_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.
- openai-server 69, anthropic-server 29, vercel-ai 22, end to end included. `Sigils` tests: 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 with `mcp_calls` dropped, all keep the sigil; strangers, a growing sigil, and copies taken earlier (with and without tools) do not; `X-Hum-Sid` names a line per caller.
- Live (`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-Sid` carrying a codeword across two unrelated requests; every caller tool call arriving as `toolu_…`.

## Known limits
- A client that edits an older message keeps its session, so claude remembers the unedited version.
- A copy of a tool-less conversation, taken after its latest turn, shares the original's sigil.
- A compaction summary sent as a user message right before the last two turns changes the tail; as a system message it does not.
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