fix(css): mobile responsive layout and dvh viewport fix - #5
Conversation
deboste
commented
Mar 31, 2026
- Use 100dvh with 100vh fallback to fix composer being cut off on mobile browsers where the address bar affects viewport height
- Add comprehensive @media(max-width:640px) rules: topbar wrapping, compact messages, full-width msg-body, smaller chips and buttons, responsive composer, approval cards, tool cards, settings modal
- Use font-size:16px on textarea to prevent iOS/Android auto-zoom on input focus (browsers zoom when font-size < 16px)
- Add .topbar-left class on title wrapper for responsive stacking
- Use 100dvh with 100vh fallback to fix composer being cut off on mobile browsers where the address bar affects viewport height - Add comprehensive @media(max-width:640px) rules: topbar wrapping, compact messages, full-width msg-body, smaller chips and buttons, responsive composer, approval cards, tool cards, settings modal - Use font-size:16px on textarea to prevent iOS/Android auto-zoom on input focus (browsers zoom when font-size < 16px) - Add .topbar-left class on title wrapper for responsive stacking
nesquena
left a comment
There was a problem hiding this comment.
Review: PR #5 — mobile responsive layout and dvh viewport fix
Summary
Clean, well-structured CSS-only PR that addresses real mobile usability issues.
What's Good
-
100dvhwith100vhfallback — Correct progressive enhancement pattern. Browsers that don't supportdvhuse the first declaration; modern mobile browsers use the dynamic viewport height to account for browser chrome. This fixes the composer being cut off on mobile. -
font-size: 16pxon textarea — Smart fix for a well-known iOS/Android issue where browsers auto-zoom on inputs with font-size < 16px. This prevents the jarring zoom-on-focus behavior. -
Comprehensive mobile rules — Covers topbar wrapping, message compaction, composer responsiveness, approval/tool cards, and settings modal. All reasonable choices.
-
.topbar-leftclass — Properly coordinated between HTML and CSS. Theflex: 1 1 100%forces title to take full width on mobile, pushing chips to next line for a stacked layout. -
Limited
!importantusage — Only on chips/buttons where specificity against inline styles is needed. Acceptable.
Tests
Ran full test suite: 201 passed, 23 failed — all 23 failures are pre-existing. No regressions.
Verdict
This is a low-risk CSS/HTML-only change with no logic modifications. Looks good to merge. Approved.
|
@deboste Thanks! |
Features: - Subagent session tree with expand/collapse in sidebar - Unified tool card rendering with diff/terminal/todo views - requestAnimationFrame throttled token streaming - Clarify dialog with threading.Event pattern - Git modal with branch/status/log - Collapsible date groups in session list - Context usage indicator in composer - VS Code Dark+ theme with Font Awesome icons - Workspace explorer with labels, tags, breadcrumbs - Auto-generated session titles via LLM - Unified session storage via SessionDB - Inline tool cards during streaming - Gateway approval polling support - Telegram session indicator Security (audit fixes): - Restore XSS protection in renderMd() with esc() sanitization and SAFE_TAGS - Restore credentials:include on all fetch/EventSource calls (PR nesquena#3) - Restore mobile responsive CSS (PR nesquena#5) - Fix sendCompress() calling undefined sendMessage() -> send() - Fix db.add_message() -> db.append_message() - Remove hardcoded su -l user, use HERMES_DESKTOP_USER env var - Preserve upstream security controls (path traversal, headers, body limit, loopback guard) - Restore SESSION_DIR/SESSION_INDEX_FILE exports - Remove unused _write_session_index imports
Reviewed: CSS/HTML only, security audited, test suite passes (201/201 relevant tests). Clean mobile responsive fix.
- Title/badge/source: 2025 → 2026, sources updated to Chatbot Arena/BenchLM - Overall: Opus 4.6 (#1, 1504 Arena Elo), Gemini 3.1 Pro (#2), GPT-5.4 (#3), Sonnet 4.6 (#4), DeepSeek V3.2 (#5, replaces DeepSeek R1) - Coding: Opus 4.6 (#1, powers Claude Code/Cursor), GPT-5.4 (#2, SWE-Pro 57.7%), Sonnet 4.6 (#3), Gemini 3.1 Pro (#4), DeepSeek V3.2 (#5) Removed: Sonnet 4.5, Gemini 2.5 Pro, GPT-5.3 Codex - Writing: Opus 4.6 (#1, Mazur 8.53), Gemini 3.1 Pro (#2, Arena CW 1487), Sonnet 4.6 (#3, EQ-Bench CW 1936), GPT-5.4 (#4), Meta Muse Spark (#5) Removed: Llama 4 Maverick (replaced by Meta Muse Spark) - Search: Gemini 3.1 Pro (#1), Grok 4 (#2, live X data), Sonnet 4.6+tool (#3), GPT-5.4 (#4), Gemini 3 Flash Thinking (#5) Added Grok 4 for real-time social/X data - Reasoning: Gemini 3.1 Pro (#1, GPQA 95.45%), GPT-5.4 (#2), Opus 4.6 (#3), Gemini 3 Flash Thinking (#4, best value), DeepSeek V3.2 (#5) Removed: Kimi K2, DeepSeek R1 standalone - Quick picker: all model names updated to 2026 versions - Setup boxes: OpenAI gpt-5-4 names, Google gemini-3-1-pro-preview, self-hosted section updated to DeepSeek V3.2 + Muse Spark
Coding section: - Gemini 3.1 Pro: add Terminal-Bench 78.4% (highest of any frontier model on CLI/DevOps) Score badge updated to show Terminal-Bench rather than SWE-bench Verified - GPT-5.4: note Terminal-Bench 75.1% in description, consolidate pill text - #5 DeepSeek V3.2 → Qwen 3.6-Plus: leads Terminal-Bench at 61.6%, 88.2% GPQA, 1M context, available now on Alibaba Cloud + OpenRouter Writing section (reordered based on EQ-Bench CW scores): - #1 Claude Sonnet 4.6 (1936 EQ-Bench CW — highest, best voice consistency) - #2 Claude Opus 4.6 (Mazur 8.53, IF Arena #1, 1M context for literary depth) - #3 Gemini 3.1 Pro (Arena CW #1 1487, AI-tell avoidance, 2M context) - #4 GPT-5.4 (noted as ~9th on Arena CW, better for structured/commercial writing) - #5 Meta Muse Spark → Kimi K2.5 (/usr/bin/bash.60/.50, ~1700 EQ-Bench CW, live API) Muse Spark removed — no commercial API available yet Reasoning section: - Gemini 3.1 Pro GPQA: 95.45% → 94.1% (more conservative/recent figure, consistent with both agents' data) - Added ARC-AGI-2 77.1% for Gemini 3.1 Pro (#1 on visual reasoning too) - Opus 4.6: added note that Sonnet leads GDPval-AA (1633 Elo #1) for throughput - #5 DeepSeek V3.2 → Qwen 3.6-Plus (88.2% GPQA, 1M context, same model as coding) Quick picker: - Creative writing: Opus → Sonnet 4.6 (EQ-Bench #1, 85% cheaper) - Hard reasoning: 95.45% → 94.1%, add ARC-AGI-2 mention - Budget pick: DeepSeek V3.2 → Gemini 3 Flash Thinking (/usr/bin/bash.50/1M, 89.8% GPQA) Setup boxes: - Self-hosted: Muse Spark → Qwen 3.6-Plus + Gemma 4 26B MoE (Apache 2.0, 82.3% GPQA with 3.8B active params, best edge/self-hosted reasoning) Overall section: unchanged (top 5 still correct per both agents) Search section: unchanged (no new data from either agent)
Reviewed: CSS/HTML only, security audited, test suite passes (201/201 relevant tests). Clean mobile responsive fix.
SHOULD-FIX #1 (renamed-root client cross-alias): drop strict-equality client filter at static/sessions.js:1853. Server-side _profiles_match cross-aliases 'default'-tagged rows to a renamed root 'kinni'; the strict-equality client would reject them, dropping every legacy session for renamed-root users. The server is now solely authoritative for profile scoping. SHOULD-FIX #2 (messaging-source dedupe ordering): _keep_latest_messaging_session_per_source now runs AFTER the profile filter at api/routes.py:2078. Before, it ran on the merged-cross-profile list with profile-blind keys, discarding the older profile's row across profiles before the scope filter — leaving zero rows for any messaging identity the active profile shared with another profile. NIT #3: _projects_migrated flag now set only AFTER successful save_projects. NIT #4: cleaned dead test code in test_is_root_profile_invalidation_drops_stale. NIT #5: _create_profile_fallback's clone_from=='default' literal now routes through _is_root_profile() for parity with the 5 other callsites. +2 regression tests pin the SHOULD-FIX shapes: - test_keep_latest_messaging_runs_after_profile_filter (source-string ordering) - test_static_sessions_js_trusts_server_profile_scoping (no client re-filter) 4173 -> 4175 tests pass. 0 regressions.
…Copilot nesquena#5) manual_repro_wakeup_hang.py sat in the pytest test root with a hardcoded /home/islaliu/.hermes/... venv path in its docstring. pytest never ran it (no test_ functions) but it was confusing. - git mv tests/manual_repro_wakeup_hang.py -> tests/manual/repro_wakeup_hang.py - Strip the operator-specific absolute interpreter path from the docstring; the run instruction is now parametric ("python tests/manual/repro_wakeup_hang.py" from repo root) and fix the sys.path repo-root math for the now-deeper location. - Exclude tests/manual/ from collection via collect_ignore_glob in the existing tests/conftest.py rather than a nested conftest.py: a second conftest module would collide with the suite's top-level 'from conftest import TEST_BASE/TEST_STATE_DIR' imports (no packages / rootdir-relative module names). Verified: pytest --collect-only tests/ collects 6014 tests, 0 collection errors, the manual script no longer surfaces.
…esquena#5 follow-up) Copilot review nesquena#5 'strip local paths' missed one residue: tests/test_wakeup_model_resolve_hang.py:3-4 still embedded the reviewer's machine path /home/islaliu/.hermes/webui/bootstrap-8787.log. Rewrote lines 3-4 to a neutral description ('a live thread-stack capture of a hung wakeup turn') — root-cause explanation itself unchanged. Full re-grep of tests/ + tests/manual/ confirms this was the only remaining residue. 7/7 tests pass.
…Copilot nesquena#5) manual_repro_wakeup_hang.py sat in the pytest test root with a hardcoded /home/islaliu/.hermes/... venv path in its docstring. pytest never ran it (no test_ functions) but it was confusing. - git mv tests/manual_repro_wakeup_hang.py -> tests/manual/repro_wakeup_hang.py - Strip the operator-specific absolute interpreter path from the docstring; the run instruction is now parametric ("python tests/manual/repro_wakeup_hang.py" from repo root) and fix the sys.path repo-root math for the now-deeper location. - Exclude tests/manual/ from collection via collect_ignore_glob in the existing tests/conftest.py rather than a nested conftest.py: a second conftest module would collide with the suite's top-level 'from conftest import TEST_BASE/TEST_STATE_DIR' imports (no packages / rootdir-relative module names). Verified: pytest --collect-only tests/ collects 6014 tests, 0 collection errors, the manual script no longer surfaces.
…esquena#5 follow-up) Copilot review nesquena#5 'strip local paths' missed one residue: tests/test_wakeup_model_resolve_hang.py:3-4 still embedded the reviewer's machine path /home/islaliu/.hermes/webui/bootstrap-8787.log. Rewrote lines 3-4 to a neutral description ('a live thread-stack capture of a hung wakeup turn') — root-cause explanation itself unchanged. Full re-grep of tests/ + tests/manual/ confirms this was the only remaining residue. 7/7 tests pass.
…Copilot nesquena#5) manual_repro_wakeup_hang.py sat in the pytest test root with a hardcoded /home/islaliu/.hermes/... venv path in its docstring. pytest never ran it (no test_ functions) but it was confusing. - git mv tests/manual_repro_wakeup_hang.py -> tests/manual/repro_wakeup_hang.py - Strip the operator-specific absolute interpreter path from the docstring; the run instruction is now parametric ("python tests/manual/repro_wakeup_hang.py" from repo root) and fix the sys.path repo-root math for the now-deeper location. - Exclude tests/manual/ from collection via collect_ignore_glob in the existing tests/conftest.py rather than a nested conftest.py: a second conftest module would collide with the suite's top-level 'from conftest import TEST_BASE/TEST_STATE_DIR' imports (no packages / rootdir-relative module names). Verified: pytest --collect-only tests/ collects 6014 tests, 0 collection errors, the manual script no longer surfaces.
…esquena#5 follow-up) Copilot review nesquena#5 'strip local paths' missed one residue: tests/test_wakeup_model_resolve_hang.py:3-4 still embedded the reviewer's machine path /home/islaliu/.hermes/webui/bootstrap-8787.log. Rewrote lines 3-4 to a neutral description ('a live thread-stack capture of a hung wakeup turn') — root-cause explanation itself unchanged. Full re-grep of tests/ + tests/manual/ confirms this was the only remaining residue. 7/7 tests pass.
…Copilot nesquena#5) manual_repro_wakeup_hang.py sat in the pytest test root with a hardcoded /home/islaliu/.hermes/... venv path in its docstring. pytest never ran it (no test_ functions) but it was confusing. - git mv tests/manual_repro_wakeup_hang.py -> tests/manual/repro_wakeup_hang.py - Strip the operator-specific absolute interpreter path from the docstring; the run instruction is now parametric ("python tests/manual/repro_wakeup_hang.py" from repo root) and fix the sys.path repo-root math for the now-deeper location. - Exclude tests/manual/ from collection via collect_ignore_glob in the existing tests/conftest.py rather than a nested conftest.py: a second conftest module would collide with the suite's top-level 'from conftest import TEST_BASE/TEST_STATE_DIR' imports (no packages / rootdir-relative module names). Verified: pytest --collect-only tests/ collects 6014 tests, 0 collection errors, the manual script no longer surfaces.
…esquena#5 follow-up) Copilot review nesquena#5 'strip local paths' missed one residue: tests/test_wakeup_model_resolve_hang.py:3-4 still embedded the reviewer's machine path /home/islaliu/.hermes/webui/bootstrap-8787.log. Rewrote lines 3-4 to a neutral description ('a live thread-stack capture of a hung wakeup turn') — root-cause explanation itself unchanged. Full re-grep of tests/ + tests/manual/ confirms this was the only remaining residue. 7/7 tests pass.
…Copilot nesquena#5) manual_repro_wakeup_hang.py sat in the pytest test root with a hardcoded /home/islaliu/.hermes/... venv path in its docstring. pytest never ran it (no test_ functions) but it was confusing. - git mv tests/manual_repro_wakeup_hang.py -> tests/manual/repro_wakeup_hang.py - Strip the operator-specific absolute interpreter path from the docstring; the run instruction is now parametric ("python tests/manual/repro_wakeup_hang.py" from repo root) and fix the sys.path repo-root math for the now-deeper location. - Exclude tests/manual/ from collection via collect_ignore_glob in the existing tests/conftest.py rather than a nested conftest.py: a second conftest module would collide with the suite's top-level 'from conftest import TEST_BASE/TEST_STATE_DIR' imports (no packages / rootdir-relative module names). Verified: pytest --collect-only tests/ collects 6014 tests, 0 collection errors, the manual script no longer surfaces.
…esquena#5 follow-up) Copilot review nesquena#5 'strip local paths' missed one residue: tests/test_wakeup_model_resolve_hang.py:3-4 still embedded the reviewer's machine path /home/islaliu/.hermes/webui/bootstrap-8787.log. Rewrote lines 3-4 to a neutral description ('a live thread-stack capture of a hung wakeup turn') — root-cause explanation itself unchanged. Full re-grep of tests/ + tests/manual/ confirms this was the only remaining residue. 7/7 tests pass.
…quena#2), HITL dedup (nesquena#5), _prime in try (nesquena#6) — whole-branch review fix wave
SHOULD-FIX nesquena#1 (renamed-root client cross-alias): drop strict-equality client filter at static/sessions.js:1853. Server-side _profiles_match cross-aliases 'default'-tagged rows to a renamed root 'kinni'; the strict-equality client would reject them, dropping every legacy session for renamed-root users. The server is now solely authoritative for profile scoping. SHOULD-FIX nesquena#2 (messaging-source dedupe ordering): _keep_latest_messaging_session_per_source now runs AFTER the profile filter at api/routes.py:2078. Before, it ran on the merged-cross-profile list with profile-blind keys, discarding the older profile's row across profiles before the scope filter — leaving zero rows for any messaging identity the active profile shared with another profile. NIT nesquena#3: _projects_migrated flag now set only AFTER successful save_projects. NIT nesquena#4: cleaned dead test code in test_is_root_profile_invalidation_drops_stale. NIT nesquena#5: _create_profile_fallback's clone_from=='default' literal now routes through _is_root_profile() for parity with the 5 other callsites. +2 regression tests pin the SHOULD-FIX shapes: - test_keep_latest_messaging_runs_after_profile_filter (source-string ordering) - test_static_sessions_js_trusts_server_profile_scoping (no client re-filter) 4173 -> 4175 tests pass. 0 regressions.
Inline fixes for 4 of 5 Opus SHOULD-FIX items before tag:
1. /api/auth/status now gates passkeys_enabled / passwordless_enabled on
_passkey_feature_flag_enabled() — when flag is off, status reports
no credentials even if passkeys.json has legacy entries. New
passkey_feature_flag field added to the response for the frontend.
2. Settings → System Passkeys block (passkeysSettingsBlock) now starts
display:none and loadPasskeys() reveals it only when the server
confirms passkey_feature_flag === true AND /api/auth/passkeys
doesn't return {disabled: true}. Stops the broken-affordance trap
where users would see Add passkey → click → 404.
3. /api/settings/save now refuses to set passwordless mode when the
passkey feature flag is off. Closes the auth-bypass path Opus flagged:
user goes passwordless while flag on → admin unsets flag → restart
serves the WebUI fully unauthenticated.
4. CHANGELOG entries added for PR nesquena#2685 (replayed-context dedup +
per-turn metering cap) and PR nesquena#2824 (Stop server affordance,
relocated to Settings) — both PRs had functional changes but no
release-notes entries. Also enriched the rate-limit detail on the
nesquena#2739 entry (30 events / 60s / 4KB body cap).
Deferred to follow-up issue (nesquena#5 in Opus review):
- Live tool metering cumulative cap across many tool calls — non-trivial
refactor of _bump_live_prompt_estimate, will be a separate PR
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…SE relay (nesquena#6961) Read/surface half of the maintainer's split for PR nesquena#6961 (child approval routing, nesquena#6943). The resolve half (#1/#2/#3) stays in a follow-up gated on the agent contract (agent#82009). #4 (CORE): scope the child->parent cache by canonical state-db/profile path and only cache positive lookups, so a miss under one profile can no longer poison another profile's identical child id, and a late state.db write is picked up on the next lookup. Adds invalidate_child_parent_cache(). #5 (SILENT): use one aggregate projection (own queue + delegated-child queues, deduped by stable approval id / gateway mirror token) unconditionally on all three surface paths — sidebar attention summary, /api/approval/pending, and the SSE initial snapshot — so a parent-with-1 + child-with-1 now reports count 2 instead of 1. #6 (SILENT): publish the aggregate parent head/count to the parent's SSE subscribers whenever an owned child queue changes (submit_pending, submit_gateway_pending_mirror, retire_gateway_pending_mirror, resolve_gateway_pending_local, resolve_child_approval_locked), so a pure-SSE parent consumer sees child enqueue/resolve without waiting for the 1.5s poll.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
…#6139 r8) Round-8 re-gate (2026-07-31T12:10:36Z): one logical tail read still mixed independently-reopened pathname generations. _read_jsonl_tail opened the journal once, read the tail, closed the descriptor, then the recovery helpers each independently reopened the pathname (path.stat + path.open): _find_record_start_before, _extract_boundary_record_summary, _record_is_structurally_complete, _read_last_complete_line_before, and the head-newline counter. A delete-and-recreate between stages could combine inode-A tail rows/offsets with inode-B boundary/predecessor rows, producing impossible mixed sequences like [100, 2]. ## Fix: thread ONE descriptor + pinned fstat size _read_jsonl_tail now opens the journal ONCE (fh = path.open('rb')), pins the size with os.fstat(fh.fileno()).st_size, and passes that same fh + size through every recovery stage. A try/finally ensures the descriptor is released on every exit (success/error/early-return). The single top-level except (FileNotFoundError, OSError) catches a delete during the open; once open, the handle is stable and all stages read one generation. All 5 private helpers converted from (path: Path, ...) to (fh, size, ...): _find_record_start_before(fh, size, seek_pos) _rfind_byte_before(fh, byte, end_offset) _record_is_structurally_complete(fh, size, record_start) _extract_boundary_record_summary(fh, record_start) _read_last_complete_line_before(fh, size, end_offset, *, budget=None) Each dropped its own path.stat()/path.open() and reads via the passed fh against the pinned size. No path.stat/path.open remains in any helper. ## Point nesquena#4: explicit aggregate budget for the backward scan _read_last_complete_line_before gains a budget keyword (default _SESSION_REPLAY_MAX_BYTES). The loop tracks bytes_consumed and rows_scanned; when either exceeds the budget / _SESSION_REPLAY_MAX_ROWS, it stops and returns None. This bounds the scan across an arbitrarily long invalid-row streak (it can no longer walk to byte 0). ## Point nesquena#5: don't trust oversized-malformed fabricated prefix In _read_last_complete_line_before, the oversized branch now requires _record_is_structurally_complete(fh, size, line_start) to be True before accepting _extract_boundary_record_summary's fabricated prefix. An oversized row that is structurally incomplete (brace depth never returns to 0) is treated like any other malformed line: skipped, scan continues. This closes the hole where an oversized malformed predecessor's head could fabricate a terminal prefix as if the whole row were valid JSON. ## Regressions (4 new, mutation-verified) - test_tail_read_uses_single_generation_under_delete_recreate: counter on Path.open — 1st open returns inode-A (token seq=1 + oversized partial), 2nd+ returns inode-B (done seq=100 terminal). Asserts the journal is opened exactly ONCE and the result is purely inode-A (no seq=100, no terminal leak). Mutation A (reopen for boundary summary) FAILS it. - test_invalid_predecessor_rows_skipped_until_valid_event: reader-level coverage of malformed JSON / JSON scalar / JSON list / blank / mixed rows before an oversized boundary; both readers recover the token (seq=1) + emit apperror. Mutation B (budget=0) FAILS it. - test_backward_predecessor_scan_budget_bounds_the_scan: a tight budget must STOP before reaching a token past 500 malformed rows (None), a generous budget reaches it (seq=1). Mutation B (budget=0) FAILS it. - test_oversized_malformed_predecessor_not_accepted_via_fabricated_prefix: an oversized structurally-incomplete predecessor (newline-terminated but brace-broken) is skipped via the gate; the valid token (seq=1) is recovered, never the fabricated terminal done. Mutation C (gate removed) FAILS it. Existing ~6 test call-sites that called helpers by path updated to the new (fh, size, ...) signature (open once + os.fstat). The 3 TOCTOU tests reworked to exercise the single-open TOCTOU (first open raises). 43/43 pass; lint diff-gate clean; git diff --check clean. Round-6 terminator hardening, _read_jsonl (full read), the summary cache, and routes.py are all unchanged.
CORE #1: allow sequential done→stream_end terminal events (no SSE leak) CORE #2: acquire per-run lock before fence lock in close_acceptance_fence CORE nesquena#3: resolve(strict=True) archive members, reject symlinks (traversal fix) SILENT nesquena#4: wire _evict_acceptance_fence into terminal append + journal deletion SILENT nesquena#5: gate settlement on worklog-worthy predicate + size sanitizer 3 broken regression tests now green. 16 steer fence tests pass.
CORE #1: allow sequential done→stream_end terminal events (no SSE leak) CORE #2: acquire per-run lock before fence lock in close_acceptance_fence CORE nesquena#3: resolve(strict=True) archive members, reject symlinks (traversal fix) SILENT nesquena#4: wire _evict_acceptance_fence into terminal append + journal deletion SILENT nesquena#5: gate settlement on worklog-worthy predicate + size sanitizer 3 broken regression tests now green. 16 steer fence tests pass.
…quena#7188 CORE nesquena#5) When a turn was auto-compressed mid-run and then cancelled, the run's events split across two session journals: steer_delivered landed in the ORIGINAL journal (admission-time session ID), but the cancel terminal was journaled under the ROTATED continuation ID (cancel_stream resolved _cancel_session_id from agent.session_id first). find_run_summary() returned only the first journal glob found, so replay/reattach selected an incomplete journal. The original acceptance fence stayed closed-but-retained (leaked). Fix: capture ONE immutable journal session ID at run admission (active_run_session_id from ACTIVE_RUNS, set before any compression rotation) and use it for BOTH the fence close AND the cancel terminal journal in cancel_stream(). The continuation ID is still used for session/scene persistence (get_session, save, anchor-scene settlement) — never for run-journal identity. The completion and error paths were already correct: they use the RunJournalWriter created at admission (a closure variable that doesn't change when compression rotates agent.session_id). Only cancel_stream() created a new RunJournalWriter with the wrong session ID. Regression test: test_7188_core5_journal_identity_split.py simulates a mid-run compression (agent.session_id rotated to continuation) then cancels, and asserts: - The ORIGINAL journal contains BOTH steer_delivered AND cancel. - NO continuation journal is created for that run. - find_run_summary() returns the complete journal. - The acceptance fence is evicted (not leaked). - Session/scene persistence still targets the continuation session.
CORE #1: allow sequential done→stream_end terminal events (no SSE leak) CORE #2: acquire per-run lock before fence lock in close_acceptance_fence CORE nesquena#3: resolve(strict=True) archive members, reject symlinks (traversal fix) SILENT nesquena#4: wire _evict_acceptance_fence into terminal append + journal deletion SILENT nesquena#5: gate settlement on worklog-worthy predicate + size sanitizer 3 broken regression tests now green. 16 steer fence tests pass.
…quena#7188 CORE nesquena#5) When a turn was auto-compressed mid-run and then cancelled, the run's events split across two session journals: steer_delivered landed in the ORIGINAL journal (admission-time session ID), but the cancel terminal was journaled under the ROTATED continuation ID (cancel_stream resolved _cancel_session_id from agent.session_id first). find_run_summary() returned only the first journal glob found, so replay/reattach selected an incomplete journal. The original acceptance fence stayed closed-but-retained (leaked). Fix: capture ONE immutable journal session ID at run admission (active_run_session_id from ACTIVE_RUNS, set before any compression rotation) and use it for BOTH the fence close AND the cancel terminal journal in cancel_stream(). The continuation ID is still used for session/scene persistence (get_session, save, anchor-scene settlement) — never for run-journal identity. The completion and error paths were already correct: they use the RunJournalWriter created at admission (a closure variable that doesn't change when compression rotates agent.session_id). Only cancel_stream() created a new RunJournalWriter with the wrong session ID. Regression test: test_7188_core5_journal_identity_split.py simulates a mid-run compression (agent.session_id rotated to continuation) then cancels, and asserts: - The ORIGINAL journal contains BOTH steer_delivered AND cancel. - NO continuation journal is created for that run. - find_run_summary() returns the complete journal. - The acceptance fence is evicted (not leaked). - Session/scene persistence still targets the continuation session.