96bdeeb9cfbf4c76d85c18feedf6e8ee2e1b2b42
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6af1a7e718 |
fix(models): derive turn-completion from turn-journal for advanced-past-pending predicate (re-gate 2026-09-23)
The suppression predicate _transcript_already_advanced_past_pending() required a run-journal terminal 'completed' state, but its caller in _apply_core_sync_or_error_marker already returns early on that same condition. The predicate branch was therefore unreachable dead code: it could never suppress anything, and stale-cancel recovery could still append a duplicate recovered user row, a journal clone and an interruption marker for a turn that actually finished. Derive the completion evidence from the crash-safe turn journal instead: _turn_journal_records_completion() reports True when the exact-stream 'completed' event is the latest terminal state for the pending stream id. The run journal's terminal write is not durable evidence for this path (it is consumed by the caller's early return), so the turn journal is the only still-observable source. A cancelled/crashed turn (interrupted) keeps recovery active, and completion for another stream of the same session never settles the pending one. Also record the same completion event on the Gateway success writeback path, so Gateway-backed runs have the equivalent completion persistence instead of leaving the run journal's lost terminal write unrecoverable. Regression coverage: - test_turn_journal_helper_reports_completed_for_matching_stream - test_turn_journal_helper_rejects_interrupted_turn - test_advances_past_pending_from_turn_journal_without_run_journal_terminal - test_full_recovery_suppresses_duplicates_on_turn_journal_completion - test_full_recovery_appends_rows_on_turn_journal_completion_loss - test_gateway_chat_worker_records_turn_journal_completion Reverting the predicate gate turns the two new CORE regressions red; reverting the Gateway write turns the Gateway regression red. |
||
|
|
5466d09e91 | fix(recovery): map vanished-session KeyError to the skip path in WebUI startup recovery (#7753) | ||
|
|
70646b3502 |
fix(i18n): explicit server tri-state for language + reviewer 4-row table regression (#7730 round 4)
Round-3 removed the `"language": "en"` schema default so fresh installs stopped
reporting a fake explicit English, and dropped the `primary === 'en'` skip.
The maintainer's round-4 gate asked to complete the tri-state shape:
1. `load_settings()` now emits an explicit `None` for the unset case
(`settings.setdefault("language", None)`) so `/api/settings` carries a
real three-way signal instead of omitting the key: `null` = no
preference, "en" = explicitly saved English, any other code = chosen
locale. Stored values (including legacy pre-change English picks) still
win via the merge and are untouched.
2. New `TestReviewerTableServerTriState` regression loads the real
`static/i18n.js` in a Node `vm` sandbox and drives
`resolvePreferredLocale(s.language, localStorage.getItem('hermes-lang'),
_detectBrowserLanguageHint())` — the exact boot.js call shape — for all
four rows of the maintainer's reproduction table:
- `en` + new browser (zh-CN) -> en (was zh under round-2 skip)
- `en` + stale ja + en-US -> en (was ja under round-2 skip)
- `en` + empty + en-US -> en
- `fr` + empty + zh-CN -> fr
Verification:
- `python3 -c "import api.config"` OK
- 15/15 in test_issue7622_initial_locale_from_browser.py (14 + new table test)
- 8/8 in test_language_precedence.py
- 60/60 settings + 13/13 password-env + login-locale/autosave/sprint26
collateral tests pass
- ruff_lint --diff origin/master: 0 new violations
|
||
|
|
221f25dcda |
fix(run-journal): reset folded reason index on timed-flush buffer clear
The ~10 Hz reasoning throttle clears the coalescing buffer before emitting but left _reasoning_buffer_index holding offsets for text that was just removed. Subsequent _strip_reasoning_output_echo cut_to() calls then used raw offsets that no longer correspond to the buffer, so the live-echo strip truncated the buffer at a phantom position and falsely marked the interim as reasoning_echo: true — leaking a truncated copy into both the SSE stream and the durable journal. Fix: call _reasoning_buffer_index.reset() immediately after zeroing the buffer in the timed-flush branch, matching the flush/_flush_reasoning_buffer paths that already keep the folded index in step with the raw text. Adds tests/test_7569_timed_flush_index_reset.py pinning the call site: - revert this fix and both tests fail (verified via revert + re-run). |
||
|
|
5ac3632d58 |
chore(run-journal): drop now-unused echo helper imports from routes
The journal replay matches through _CompactEchoIndex, so _find_compact_echo_suffix_start and _strip_compact_echo_suffix are no longer referenced in api/routes.py. The helpers stay in api/streaming.py as the equivalence oracle the tests compare against. |
||
|
|
dcac71c74e |
fix(run-journal): match echo suffixes through an incremental folded index
Review follow-up for #7569: change 4 of the live-snapshot perf PR replaced one quadratic with another. The window-free backward walk is correct, but it re-walks the raw reasoning transcript's whitespace once per interim event — on a whitespace-heavy transcript that span is megabytes, and the replay went ~55x slower than master on the production shape the gate measured (1MB / 20-interim journal: 0.035s master → 1.922s branch). ## Thinking Path The gate's finding was that the cost is structural, not a tweak: the matcher consults the *whole* accumulated transcript on every interim event, so any raw-text rescan of it is O(interim_count x whitespace_span). The fix is the data-structure change the review asked for — maintain the folded view and the raw cut offsets incrementally, and match/strip from that indexed state instead of rescanning the raw buffer. ## What Changed - `api/streaming.py`: new `_CompactEchoIndex` — an append-only folded index that records, per non-whitespace character, its raw index. `append` folds a chunk once (cost proportional to the chunk, never the buffer), `matches_tail`/`cut_to` compare only the candidate against the folded tail (O(len(candidate)), no raw walk), `reset` drops the state after the raw text is truncated. The old raw helpers (`_find_compact_echo_suffix_start`, `_strip_compact_echo_suffix`) stay as the equivalence oracle. - `api/routes.py::_run_journal_live_snapshot`: reasoning chunks feed the index as they are appended; `reasoning_echo_tail_matches` and `strip_reasoning_echo_tail` consult the index. The strip re-indexes the truncated transcript so later probes match against it. - `api/streaming.py` live path: the same swap for the live-stream echo strip. The three live reasoning buffers (STREAM_REASONING_TEXT, the coalescing buffer, the per-message segments) each carry a mirrored index, fed on every append and re-indexed after a strip. The buffer flush resets the buffer's index with the text it drops, so the index never describes text that is no longer there. - `docs/rfcs/webui-run-state-consistency-contract.md`: the echo-match clause now names the indexed representation and the scaling regression that pins it. ## Why It Matters Removing one quadratic by introducing another in the same hot path is the exact risk the gate flagged — the headline benchmark used a journal shape where the window-free walk's cost does not dominate, so the aggregate number still looked like a win. The index keeps the original 8.9x and adds the scaling guarantee on the shape that was regressing. ## Verification - Measured on the reproduced shape (1MB whitespace span, 20 interim events): 0.972s → 0.049s. Doubling both span and interim count scales ~2x per step (x1 0.041s / x2 0.090s / x4 0.175s) — linear, not quadratic. - Behaviour equivalence: 300 randomized journals (whitespace-heavy, Unicode, chunked, flagged/unflagged interims) rebuilt with the indexed implementation and with the raw walk swapped in — sha256 of the canonical snapshot JSON matched on all 300. - `tests/test_compact_echo_index.py` (new, 6 tests): oracle equivalence over 2000+ randomized cases, chunked-append equivalence, matches_tail/cut_to agreement, reset-after-truncation, and two cost probes that fail the raw walk. - `tests/test_live_snapshot_echo_scan_scaling.py` (new, 4 tests): the proportional-scaling regression the review asked for — verified to FAIL on the pre-fix matcher (14.6x time for 4x rows, the quadratic signature) and pass on the indexed one. - Existing echo/journal suites unchanged and green: 19 items across `test_compact_echo_suffix_linear_scan` / `test_live_snapshot_echo_dedup` / `test_live_snapshot_single_parse`, plus 66 neighbouring items (`test_issue1361_cancel_data_loss`, `test_issue2565_reasoning_accumulation`, `test_issue2713_streaming_segment_flush`, `test_cancel_interrupt`, `test_run_journal`). ## Contract Change None. The matcher's observable behaviour is unchanged (same cut points, same whitespace-insensitive semantics); only its cost profile changes. No config knobs, no API surface. ## Risks - Index drift: if a future writer mutates one of the reasoning buffers without feeding (or resetting) its index, the matcher would silently miss echoes. Mitigated by keeping the index feed adjacent to every existing write site and by the reset-on-flush/reset-on-strip symmetry; the equivalence test would catch a dropped feed as a missed echo. - Memory: the index stores one char plus one int per non-whitespace character. On a whitespace-heavy transcript this is bounded by the folded length, which is what the transcript would cost folded anyway; the raw text is still the larger object. ## Model Used Hermes Agent (step-5-preview) with direct file edits and pytest verification. Cost properties were measured with a purpose-built scaling probe (proportional span x interim count), not a fixed-size fixture; the probe is kept as a regression test rather than a script. ## Related #7569 (this PR), #7310 (slow /api/session under load), #7534 (incremental journal read cache — complementary), #7273/#7291 (metering write-side skip). |
||
|
|
a44a415876 |
test(session): drive the bounded-settle splice decision in a Node VM
The round-3 gate reproduced two data-integrity defects against the shipped functions and noted that the focused adjacent suite stayed green through both reproductions — the existing coverage for this area asserts source text, which pins the diff's *text* rather than its *behaviour*. These tests execute the real decision code instead. ## What Changed `tests/test_7628_recovery_window_integrity.py` (new, 5 tests): the bounded-settle splice decision is extracted verbatim from `static/messages.js` (from the terminal-marker detection through the `S.messages` assignment) together with its transitive pure dependencies, and driven through a Node VM with the paging signals as the only variable. - **Discriminating regression for the splice strategy.** The fixture is a visible window of three turns plus a terminal marker against a durable tail of the last turn with `_messages_truncated=true, _messages_offset=4`. On that shape the correct suffix splice yields `[u-3, a-3, marker]`, while the pre-fix `prefix || suffix` expression splices at the head offset and re-emits `u-2/a-2/u-3/a-3` — the duplication the gate reported. Verified to fail on the pre-fix expression and pass on the current one. - **Paging restore.** `_messagesTruncated` / `_oldestIdx` follow the response's own signals for both the truncated and untruncated shapes, so a successful `/compress` (bounded preflight, full-transcript response) cannot leave the preflight's window state behind. - **Source lock on the strategy selection.** The strategy must be a truncation-gated ternary; a bare `||` between the two comparisons is the defect. - **Source lock on the compress success path.** The paging restore must sit between the response assignment and `renderMessages()`, reading `data.session._messages_truncated` / `_messages_offset` rather than hardcoding `false`. ## Why It Matters Both findings are silent history corruption: a wrong splice drops or duplicates transcript rows, and a stale paging offset shifts every absolute index that edit / fork / regenerate addresses rows by. Neither surfaces as an error, so the regression has to observe the resulting row list. ## Verification - 5/5 pass on the current head. - Reverting only the strategy selection (`prefix || suffix` plus the head-offset splice) fails 2 of the 5 — the behavioural splice test and the source lock — confirming the tests bite rather than restate the diff. - `tests/test_session_default_window.py` + `tests/test_7628_recovery_paging.py` unchanged and green (30 items together with the new file). - `scripts/ruff_lint.py --diff origin/master`: no new violations. ## Contract Change None — tests only. ## Model Used Hermes Agent (step-5-preview). The discriminating fixture was derived by executing both the current and the reverted expression through the same harness and comparing the resulting row lists, not by reading the diff. ## Related #7628 (this PR), #7310 (slow /api/session under load), #7625 (bounded recovery measurement). |