Skip to content

fix(ipad): incremental batched session-list rendering to preserve momentum scroll - #1

Closed
CharlesMcquade wants to merge 1471 commits into
masterfrom
fix/ipad-sidebar-scroll-stuck
Closed

CharlesMcquade wants to merge 1471 commits into
masterfrom
fix/ipad-sidebar-scroll-stuck

Conversation

@CharlesMcquade

Copy link
Copy Markdown
Owner

🏄 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:

  • Initial batch of 60 rows rendered upfront. Enough to fill any screen.
  • Bottom spacer div that accounts for unrendered rows, keeping the scrollbar accurate.
  • IntersectionObserver on a sentinel div. When you scroll near the bottom and stop, it fires once, appends 40 more rows, and re-observes. Momentum never eats a DOM hit mid-scroll.
  • Background updates deferred via 1200ms idle guard on touch — SSE syncs, unread badges, gateway polls all wait until your scroll gesture is done.

Plus some CSS cleanup: .sidebar was overflow:visible (causing scroll chaining on iPadOS), and .session-list was missing -webkit-overflow-scrolling:touch.

Result: butter-scroll through thousands of sessions. Fully loaded, mobile, desktop — all happy.

Changes

static/style.css

  • .sidebar overflow:visible → overflow:hidden (desktop scope, prevents scroll chaining)
  • .session-list added -webkit-overflow-scrolling:touch (iOS momentum scroll support)
  • .sidebar .resize-handle right:-2px → right:0 (not clipped by new overflow)

static/sessions.js

  • Added _isTouchPrimary() — matchMedia('(pointer:coarse)')
  • Added _isSessionListTouchScrolling() and _deferRenderSessionListFromCache() — defer background renders during active touch scroll (1200ms idle window vs 700ms desktop)
  • Added _ensureTouchSentinelObserver() — IntersectionObserver on sentinel div for incremental row loading
  • Added _setupTouchSentinel() — creates/updates the "Loading more…" sentinel after each render
  • renderSessionListFromCache(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-sync

tests/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

  • Automated: 21 new + 2 existing sidebar scroll regression tests, all passing
  • Manual: iPad Safari — full momentum scroll through 80+ sessions, batch loading works, background updates don't interrupt gesture

rodboev and others added 30 commits July 14, 2026 04:01
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.