fix(state): avoid WAL unlink race during state.db repair - #101085
JoaoMarcos44 wants to merge 2 commits into
Conversation
…ccess An oversized progressive edit is clipped to one message rather than split: splitting mid-stream moves the editable message id, so the next accumulated -token edit re-splits and the reply duplicates (NousResearch#48648). The clipped edit succeeded, so `edit_message` returned a bare `SendResult(success=True)` and the caller had no way to learn that Telegram stored less than it was given. `GatewayStreamConsumer._send_or_edit` then advanced `_last_sent_text` from the text it *sent*. Every "what has the user already seen" derivation is built on that field — `_visible_prefix()`, `_continuation_text()`, and the turn-final payload recorded by `_mark_skip_redundant_finalize()` and the failed-final-edit guard. With an over-long value all three describe text no API call ever carried: a fallback send skips the un-stored middle of the answer, and `delivered_final_matches()` reconciles the clipped preview against the completed `final_response`, so `gateway/run.py` logs "final delivery already confirmed ... content_delivered=True" and suppresses the corrective send. The user is left with a frozen preview and a stuck cursor. Telegram now returns `raw_response={"stream_preview_truncated": True, "delivered_prefix": <stored text>}` on all three clipped-preview returns (pre-flight clip, saturated dedup, and the reactive message_too_long retry) — the same `delivered_prefix` field the existing `partial_overflow` contract carries on the failure branch. The consumer reads it through `_delivered_text_for()` and tracks what the platform stored rather than what it requested. Adapters that deliver exactly what they were given report nothing and are unaffected. Refs NousResearch#98552, NousResearch#48648, NousResearch#71643, NousResearch#78541, NousResearch#82656. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #101085 — fix(state): avoid WAL unlink race during state.db repair + truncated Telegram preview fix (#98552)
Non-blocking:
Verdict: LGTM. Two leakage fixes in one PR: exclusive-connection WAL restore closes the post-repair split-brain, and delivered-prefix tracking closes the clipped-preview silent-loss. |
|
Salvaged via #101736 (merged; the WAL commit cherry-picked with authorship preserved). The bundled Telegram preview commit was dropped — #98552 was already closed via #100533. Confirmed not redundant: main still reopened state.db after the guard released. Your unit test could only run on non-WAL-reset SQLite builds (never on macOS 3.46), so I added an end-to-end test through |
Summary
Root cause
Journal-mode restoration opened a new connection after the exclusive repair guard had released the live database. In WAL mode, a writer could retain the unlinked WAL inode while that second connection created a fresh state.db-wal path.
Bottleneck scan
The focused repair path remains serialized by _exclusive_repair_db_guard. No additional correctness bottleneck was found in hermes_state.py or the regression test; unrelated checkpoint/VACUUM paths were not changed.
Tests
Limitations
Closes #101064