fix(ipad): incremental batched session-list rendering to preserve momentum scroll - #1
Closed
CharlesMcquade wants to merge 1471 commits into
Closed
CharlesMcquade wants to merge 1471 commits into
CharlesMcquade wants to merge 1471 commits into
Conversation
Release exp: fail-closed gateway-restart recovery for agent updates (nesquena#6054)
Release exp: cascade delegated subagent deletion (nesquena#5926)
… CONTRIBUTING.md Distilled habits that get a PR merged in one review round instead of several: four root causes + ten rules + show-your-work PR-description contract. Full doc in docs/GUIDELINES.md, compressed 10-point form in AGENTS.md (agent entry point), pointer added to CONTRIBUTING.md reading list.
…6075) Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
docs: change guidelines to reduce PR review rounds (GUIDELINES.md + AGENTS.md)
Release exp: preserve visible reasoning on deferred anchor paint (nesquena#6048)
…ep unsaved sessions startable Two independent defects that chain into "new conversations cannot be started": every POST /api/session/draft and /api/chat/start 404s a few seconds after POST /api/session/new returned 200. 1. Search inputs are parsed as username fields. index.html contains no <form> at all, but does contain password inputs (the Settings "Access Password" panel). Chromium therefore groups the document's unowned fields into one synthetic password form and picks the first text input in the DOM — #sessionSearch, the sidebar conversation filter — as that form's username field, then autofills the saved account name into it on load. autocomplete="off" was already present; it is ignored for credential heuristics. The autofill fires oninput -> filterSessions() -> GET /api/sessions/search ?content=1&depth=5: a full content search the user never asked for, repeated on every page load. Fix: give the four search inputs type="search" plus the per-manager opt-outs (data-1p-ignore / data-lpignore / data-bwignore / data-form-type), and give the Settings password inputs a real <form> owner with proper autocomplete tokens so Chromium scopes the credential form to them instead of to the whole document. The form uses display:contents, so layout is unchanged. CSS suppresses WebKit's native search clear button, since these fields ship their own (#sessionSearchClear). 2. A never-persisted session is treated as evictable. new_session() intentionally keeps a session in RAM until its first message (nesquena#1171), so the SESSIONS cache is its only copy. _session_is_evictable() (nesquena#4765) short-circuited on zero messages, reasoning that an empty shell "is recreated on next access". It is not: get_session() has no recreate path and raises KeyError, so both routes 404 and the session can never be started. The content search from (1) pulls every hit through get_session(), which blows past sessions_cache_max (default 300) and evicts the session being composed. That is why this reproduces within seconds on an install with ~1700 sessions, and why it grew worse as the session count rose. Fix: require proof of persistence before evicting, for empty sessions too — which is what the function's own docstring already promised ("Its full state is already persisted to the JSON sidecar"). The zero-message branch was the single path that bypassed it. nesquena#4765's memory bound is unaffected: sessions loaded from disk remain evictable, and its existing tests still pass. Trade-off: an unsaved empty session now stays resident for the process lifetime (one per "New Conversation" click, an empty object each). That is a deliberate application of the function's stated invariant — "slightly more RAM for a session we are unsure about is strictly better than evicting an unsaved session". Bounding it by age instead would also work if maintainers prefer. Validation: pytest tests/test_issue4765_sessions_lru_eviction.py -> 9 passed The added regression test fails on the unpatched predicate (verified by reverting api/models.py alone) and passes with the fix. Note: (1) fixes what triggers the churn, (2) fixes what turns that churn into an unstartable session. Either fix alone leaves a real defect in place, so they are submitted together. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the previous commit. It gave the Settings password fields an explicit <form>, but onboarding.js injects two more password inputs (API key, password) with no form owner. While the wizard is mounted the document is back to having unowned password fields — exactly the shape that makes Chromium synthesise one document-wide credential form and autofill the saved account name into the sidebar conversation filter. Same treatment: a display:contents <form> wrapper (no layout impact) plus autocomplete tokens. All five password inputs in the app are now form-owned, so there is no unowned credential form left for Chromium to build from. Verified no CSS depends on .onboarding-field / .onboarding-copy being children or siblings of the wrapped nodes, and no JS traverses their parents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CharlesMcquade
added a commit
that referenced
this pull request
Aug 22, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 22, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 22, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 24, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 24, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 24, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Aug 27, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 8, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 8, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 9, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 9, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 9, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 14, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 14, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 14, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 17, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 17, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 17, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 18, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 18, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 18, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 19, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 19, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 19, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 20, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 20, 2026
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.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 20, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 22, 2026
…ings Finding #1 (ReferenceError): _appendTouchBatch called _renderOneSession which was nested inside renderSessionListFromCache — out of scope. Fix: store the _renderOneSession closure in _touchRenderState during the initial render. Finding #2 (final batch dropped): _appendTouchBatch returned early when loaded>=total before appending the final partial batch. Fix: compute targetEnd from oldLoaded+BATCH_SIZE and append before checking completion. Finding nesquena#3 (no row-identity authority): _appendTouchBatch re-derived rows from mutable _allSessions, splicing a new-cache suffix onto an old-cache prefix. Fix: use canonical _touchRenderState.flatRows + DOM SID prefix validation. On mismatch, trigger full re-render instead of splicing. Finding nesquena#4 (group/lifecycle incoherent): duplicate group wrappers (initial render didn't set data-group-label), per-group spacers not found by append, indefinite fallback RAF. Fix: set data-group-label in initial render, _teardownTouchSentinel clears observer+RAF+state, generation-scoped fallback RAF. Added 5 executed node-VM regression tests covering 60→100→final growth, node preservation with zero innerHTML writes, stale generation rejection, SID mismatch → full re-render, and exception recovery.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 26, 2026
…squena#7188 rework) Addresses all three gate-certifier blocking regressions by making the journal lock a true runtime-lifecycle transaction: Must-fix #1 (CORE) — acceptance fence preventing late Steer: - Add path-shared acceptance fence (_ACCEPTANCE_FENCE) to RunJournalWriter, checked under the journal lock by accept_and_append_if_nonterminal. - close_acceptance_fence() called BEFORE the completion Steer drain (streaming.py:~12205) and BEFORE the cancel eager pop (streaming.py:~13351). - Terminal events (done/cancel/apperror/stream_end) route through close_acceptance_fence_and_publish_terminal(), closing the fence atomically with the terminal append. - Eager cancellation journals+publishes the cancel event with a fresh canonical event ID via the same transaction (was publishing outside the journal entirely). - Late Steer after fence close is rejected with fence_closed (surfaced as stream_dead), preventing leak into next turn. Must-fix #2 (CORE) — archive-backed Steer HTTP 400: - _verified_steer_attachment_paths now permits a contained extracted directory (excluding the inbox root) by expanding to concrete member files. /api/upload/extract returns a directory, not a file. Must-fix nesquena#3 (SILENT) — server-side terminal settlement durability: - _persist_terminal_anchor_scene_from_journal() materializes the canonical journal scene into anchor_activity_scenes during server-side terminal settlement, for completion, cancellation, and error paths. - Called from the streaming finally block and cancel_stream(), so Steer deliveries survive settle/replay without relying on the browser's async scene POST. Tests: test_steer_fence_rework_7188.py covers all three regression scenarios the gate-certifier reproduced. 126 existing Steer/journal tests still pass.
CharlesMcquade
added a commit
that referenced
this pull request
Sep 26, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🏄 The "swipe through sessions" fix
I use Hermes WebUI on my iPad and iPhone a lot. Every time I flick-scrolled through my session list, it would freeze — catching after 1-2 items. Momentum? Gone. Felt like the list was angry at me for trying to scroll fast.
Digging into it, the problem was violent: every scroll tick, the virtual scroll system would
innerHTML = ""the session list and rebuild it from scratch. On iOS Safari, that kills the native momentum scroll state machine mid-gesture. So each flick would render 2 sessions then stop dead.The fix was surprisingly satisfying. Instead of the wipe-and-rebuild virtualization (which is fine on desktop where scroll wheels don't have momentum), touch devices now get:
Plus some CSS cleanup:
.sidebarwasoverflow:visible(causing scroll chaining on iPadOS), and.session-listwas missing-webkit-overflow-scrolling:touch.Result: butter-scroll through thousands of sessions. Fully loaded, mobile, desktop — all happy.
Changes
static/style.css
.sidebaroverflow:visible→overflow:hidden(desktop scope, prevents scroll chaining).session-listadded-webkit-overflow-scrolling:touch(iOS momentum scroll support).sidebar .resize-handleright:-2px→right:0(not clipped by new overflow)static/sessions.js
_isTouchPrimary()—matchMedia('(pointer:coarse)')_isSessionListTouchScrolling()and_deferRenderSessionListFromCache()— defer background renders during active touch scroll (1200ms idle window vs 700ms desktop)_ensureTouchSentinelObserver()— IntersectionObserver on sentinel div for incremental row loading_setupTouchSentinel()— creates/updates the "Loading more…" sentinel after each renderrenderSessionListFromCache(opts)— accepts{force:true}to bypass the touch deferral_sessionVirtualWindow()— returns batched window[0, loadedCount)on touch devices instead of virtualized window_scheduleSessionVirtualizedRender()— bails early on touch (IntersectionObserver handles append)_resyncSessionVirtualWindowAfterRender()— uses{force:true}for scroll correction re-synctests/test_ipad_sidebar_scroll_stuck.py (21 tests)
Covers CSS overflow changes, webkit scroll, touch guard functions, sentinel observer, batch constants, and filter-change reset.
tests/test_streaming_sidebar_scroll.py (patched)
Updated hardcoded CSS string to include
-webkit-overflow-scrolling:touch.Tested