fix(claude-native): stop the resume seek from skipping a just-injected prompt - #4403
Conversation
|
…me prefix A prompt sent to a resuming claude-native session sometimes never reached the Omnigent DB while still showing in Claude's TUI pane — no error, no warning. `start_at_end=True` means "skip the prefix I just wrote" — it is set iff this launch synthesized a resume transcript from committed Omnigent history (which the DB already has, so forwarding it would duplicate the conversation). But it was implemented as "skip whatever exists when I get around to looking", and those are different things. Seeding requires `transcript_path` from Claude's first hook, and `inject_user_message` waits on the same boot; the two are unordered, so the paste routinely wins. Everything Claude wrote in that window — the user's prompt included — then sat behind the cursor, skipped for the session's lifetime. The prefix length is already known before launch: all three synthesizing paths (`_ensure_local_claude_resume_transcript` on cold resume, `_clone_claude_transcript` for a same-host fork, the items-rebuild for a cross-family fork) return the path they wrote. Measure it there and pass `start_at_offset` through instead of relying on a later `stat`. The skip becomes exactly the prefix regardless of when the forwarder is scheduled, so the race is removed rather than narrowed. `start_at_end` stays for reattach, where nothing was synthesized and a live end-offset is correct — the CLI attach path has no concurrent inject. The offset is clamped to the transcript end so a truncated/replaced file cannot leave the cursor past EOF, and a failed measurement falls back to the old behaviour rather than to 0 (re-forwarding all history is the worse failure). claude-native only: `supervise_forwarder` here is distinct from the same-named codex function, and no other harness forwarder has `start_at_end`. Co-authored-by: Isaac
f80fe8c to
5cf576a
Compare
|
/review |
|
|
🏷️ Doc impact: Internal bugfix to the Claude native transcript forwarder's cursor-seeding logic (adding a measured-prefix offset to avoid skipping boot-window messages); no user-facing surface, integration, or documented behavior changed. Auto-classified on merge. Set the label manually before merging to override. · run |
Related issue
Closes #
Summary
A prompt sent to a resuming claude-native session sometimes never reached the
Omnigent DB — while still showing in Claude's TUI pane. No error, no warning,
nothing in the runner log.
Commit 1 adds diagnostics for the three silent skip paths. It caught the bug on
the first run:
start_at_end=Truemeans "skip the prefix I just wrote" — it is set iff thislaunch synthesized a resume transcript. But it was implemented as "skip
whatever exists when I get around to looking," and those are different things.
Seeding needs
transcript_pathfrom Claude's first hook;inject_user_messagewaits on the same boot. The two are unordered, so the paste routinely wins.
Everything Claude wrote in that ~2.3s window then sits behind the cursor and
is skipped for the session's lifetime.
The record count is the proof. The synthesized prefix is 14 records (bytes
0–5920, all v5 UUIDs — the ones we generate). The seed skipped 21. The extra
7 are live Claude records, and one of them is the user's message.
So the skip was doing two different things at once:
The fix
The prefix length is already known before launch — all three synthesizing
paths return the path they wrote (
_ensure_local_claude_resume_transcriptoncold resume,
_clone_claude_transcriptfor a same-host fork, the items-rebuildfor a cross-family fork). So measure it there and pass
start_at_offsetthrough, instead of relying on a later
stat:The skip becomes exactly the prefix regardless of when the forwarder is
scheduled, so the race is removed rather than narrowed.
ELI5
We rebuild the conversation history into a file, hand it to Claude, and tell the
tailer "start reading after the part I wrote." It was measuring "the part I
wrote" after Claude had already started appending to the same file — so it
skipped the user's new message along with the history. Now we measure the file
before Claude touches it.
Kept deliberately
start_at_endstays for reattach (claude_native.py:2685), where nothingwas synthesized and a live end-offset is the right answer. The CLI attach
path has no concurrent inject, so it is unaffected.
the cursor past EOF (where every later read looks like a stale-cursor reset).
0—re-forwarding all of history would be the worse failure.
source=measured_prefix|transcript_end, so a futuredivergence between the skipped-record count and the prefix is visible.
Other harnesses
claude-native only.
supervise_forwarderhere is a distinct function fromthe same-named codex one (no shared code), and no other harness forwarder has
start_at_endat all — goose / hermes / codex / cursor / kimi all report 0references.
_ensure_state_for_transcriptis claude-only.Test Plan
Branched from latest
main(f9ec924a) — this is a pre-existing bug, not aregression from #4344, so the fix is scoped away from that PR.
pytest tests/test_claude_native_forwarder.py tests/test_claude_native_bridge.py # -> 363 passed, 1 pre-existing failure (see below) pytest tests/runner/Verified against the real transcript of the session that lost a message:
start_at_end)start_at_offset=5920)New coverage:
test_measured_prefix_seed_keeps_a_prompt_injected_during_boot— replays theexact race (prefix written, Claude appends a prompt, then we seed).
Confirmed it fails without the fix (cursor at 305/EOF instead of
186/prefix), so it genuinely guards the regression.
test_measured_prefix_never_seeks_past_the_transcript_end— the clamp guard.production change (4 failed / 1 passed when reverted).
Pre-commit: ruff format + ruff check pass, zero pyrefly errors in the changed
line ranges (the 9 reported are
missing-importin untouched files — optionaldeps absent from this worktree's
.venv, present in CI).Manual verification
The record count is the tell: it should now equal the synthesized prefix
exactly.
Demo
N/A — no visual change. The fix is that a message stops disappearing.
Type of change
Test coverage
Coverage notes
The race itself is covered deterministically by writing the prefix, appending a
record, and then seeding — which is the real ordering, without needing to
schedule a real Claude boot against a real tmux pane. Both new tests were
verified to fail with the production change reverted.
One pre-existing failure, unrelated and present on clean
main:test_claude_native_bridge.py::test_relay_close_keeps_advertisement_owned_by_newer_relay.Also pre-existing on clean
mainin this shell: 4test_claude_native.pyprovider-config tests that read ambient
ANTHROPIC_*env.Changelog
Resuming a claude-native session no longer drops a message sent right after the
session starts.