Skip to content

fix: preserve bounded transcript windows across session lifecycle - #5986

Closed
akay64 wants to merge 13 commits into
nesquena:masterfrom
akay64:fix/bounded-session-reload
Closed

akay64 wants to merge 13 commits into
nesquena:masterfrom
akay64:fix/bounded-session-reload

Conversation

@akay64

@akay64 akay64 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI supports long-running conversations that can contain thousands of messages.
  • The browser normally displays a bounded transcript window for responsive session switching.
  • Several lifecycle paths could still replace that bounded window with the complete transcript.
  • This PR applies the bounded-window contract consistently across settlement, recovery, editing, retry, undo, and pagination.

What Changed

  • Keep settled SSE done, stream_end, reconnect, and error-recovery paths bounded.
  • Prevent full terminal session snapshots from replacing a paginated browser transcript.
  • Preserve same-session SSE ownership during force reloads.
  • Ignore stale or re-entrant session recovery events during an active session load.
  • Keep edit and regenerate operations bounded when their target is already visible.
  • Route /retry and /undo through the canonical bounded session reload path.
  • Preserve the existing transcript while a bounded post-mutation refresh completes.
  • Update the canonical message count and top-bar metadata after loading older messages.
  • Add focused regression tests and document the session-windowing behavior.

Why It Matters

Long conversations could be fully loaded and rendered during normal lifecycle operations, producing:

  • complete transcript dumps;
  • visible flickering;
  • long loading delays;
  • difficult session switching;
  • unnecessary DOM replacement for edit, retry, and undo.

The browser now keeps a bounded visible window while preserving the correct server-side message coordinate, pagination state, and total message count.

Verification

Focused automated verification:

  • 40 passed:
    tests/test_bounded_session_mutations_frontend.py
    tests/test_topbar_lazy_message_count.py
    tests/test_parallel_session_switch.py
  • 19 passed:
    tests/test_cmd_idle_fallback.py
    tests/test_older_history_viewport_preservation.py
  • JavaScript syntax checks passed for commands.js, sessions.js, and ui.js.
  • Ruff passed for the new regression test.
  • git diff --check passed.

Manual UI verification covered with a session of 1500 messages:

  • edit/regenerate;
  • stream completion;
  • retry;
  • undo;
  • moving between sessions.
  • loading messages automatically on scroll up

Risks / Follow-ups

  • Explicit full-history actions such as export or jump-to-session-start remain capable of loading the full transcript.
  • Existing loaded of total top-bar semantics are preserved.
  • No backend, Hermes Agent, or state.db changes are included.
  • No new dependencies, framework, bundler, or build step is introduced.

Contract Routing

  • Contract family: session transcript windowing, recovery/replay, and session metadata.
  • References: docs/CONTRACTS.md and ARCHITECTURE.md.
  • Contract change: none intentional; lifecycle operations now consistently follow the existing bounded transcript contract.

Model Used

Provider: OpenAI, Model: Codex - GPT-5.6 Luna Max, Human super vision and review

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR keeps long session transcripts bounded throughout the browser session lifecycle. The main changes are:

  • Coordinated same-session reloads for retry, undo, and recovery events.
  • Bounded transcript refreshes during stream settlement and reconnect recovery.
  • Window-aware edit and regenerate operations using absolute server coordinates.
  • Updated pagination counts and top-bar metadata after loading older messages.
  • Focused tests and architecture notes for the transcript-windowing contract.

Confidence Score: 5/5

This looks safe to merge.

  • Concurrent same-session callers now wait for or queue the required refresh.
  • Recovery events retain the newest reported message count and avoid stale cross-session updates.
  • No blocking issue was found in the updated lifecycle paths.

Important Files Changed

Filename Overview
static/sessions.js Coordinates concurrent session loads and preserves bounded message windows during refresh and pagination.
static/messages.js Keeps stream settlement and session recovery bounded while checking pane and stream ownership.
static/commands.js Routes retry and undo through the bounded session reload path.
static/ui.js Preserves transcript windows during reconnect, edit, and regenerate workflows.

Reviews (7): Last reviewed commit: "test(fix-chat-msg-dump): align settlemen..." | Re-trigger Greptile

Comment thread static/sessions.js Outdated
Comment thread static/messages.js Outdated

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔬 Gate review (Codex) — SHIP ONLY WITH FIXES (3 reproduced bounded-window regressions)

Thanks @akay64 — the core bounded-window preservation is largely sound (195 targeted tests pass, syntax + ESLint + diff-check clean), and the goal is right (keep the window bounded so we don't reintroduce the #5966-class memory blowup). But the gate reproduced three silent regressions on the lifecycle paths before this can ship:

  1. [SILENT — lost mutation, /undo shows stale] static/sessions.js:1480 + static/messages.js:7270 — a same-session mutation reload (e.g. /undo) returns early when _loadingSessionId is already set, so the window refresh is dropped. Reproduced with a Node probe: /undo reported success while the old messages stayed visible. Fix: allow mutation reloads to supersede the current generation, and queue one post-load refresh when session-updated reports a newer count instead of returning solely because _loadingSessionId is set.

  2. [SILENT — window collapse on reconnect] static/ui.js:9199 — reconnect recovery collapses an expanded bounded window back to 30. _messageReloadLimitForSession() has no captured hint here, so a 90-row loaded window requests 30 and silently removes 60 visible rows (invalidating scroll position). Verified with the actual helper. Fix: derive the limit from _settledSessionMessageWindowLimit(null,{forceBounded:true}), or capture+clear the current-window hint around this request.

  3. [SILENT — scroll hijack] static/messages.js:5414 — follow-scroll intent is captured before the awaited bounded-window fetch and applied at :5623 afterward, so if the reader scrolls up during the request, completion still forces them back to the bottom. Fix: re-capture/revalidate _shouldFollowMessagesOnDomReplace() after the await + ownership check, immediately before DOM replacement.

All three are the "correct window content, wrong lifecycle handling" class — lost messages on undo, dropped rows on reconnect, scroll yanked from under the reader. Once mutation/refresh work is superseded-not-dropped, the reconnect limit honors the current window, and scroll-follow is re-evaluated post-await, re-gate. Solid direction on a genuinely valuable perf fix.

@nesquena-hermes nesquena-hermes added the size:L Large PR (>10 files or >250 LOC) label Jul 12, 2026
@akay64
akay64 requested a review from nesquena-hermes July 12, 2026 12:58
@akay64

akay64 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes Thanks for the review. I addressed all three lifecycle regressions on the PR branch:

  1. Same-session mutation refreshes no longer return early when another bounded load is active. Calls from /undo, /retry, and session-updated now share the active load promise and queue at most one bounded follow-up refresh. The follow-up is skipped when the active load already reached the reported message count, while cross-session stale events remain suppressed.

  2. Reconnect recovery now preserves the currently displayed bounded window even when no force-reload hint exists. The shared reload-limit logic reuses the bounded-window calculation, so an expanded 90-row view will not collapse back to the default 30-row window.

  3. Completion follow intent is now re-evaluated after the bounded fetch and session-ownership check, immediately before the final settled render/follow action. If a user scrolls upward while the request is pending, the stale pre-fetch follow decision can no longer pull them back to the bottom.

I also added focused regression coverage for the mutation queue, reconnect window preservation, and deferred scroll-follow race, plus updated older structural tests to reflect the loadSession coordinator/core split.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Re-gate (Codex) — all three lifecycle regressions resolved, PASS

Thanks @akay64. I re-pulled the branch at 45903d1d0 and read the actual diffs for all four touched files against origin/master. Each of the three regressions from the prior gate is now genuinely fixed, not just described. Walking them in order:

1. [was: /undo shows stale — dropped mutation refresh] — FIXED. The old loadSession is split into a thin coordinator (sessions.js loadSession) over _loadSessionOnce, and same-session mutation reloads now supersede instead of returning early. cmdRetry/cmdUndo no longer do their own bare GET + S.messages=… assignment; they route through the bounded path:

await loadSession(activeSid,{force:true,keepStaleUntilLoaded:true,externalRefreshReason:'undo'});

When a same-session load is already active, _queueSessionLoadAfterActive sets followUpRequiresMutation=true for retry/undo, so _runQueuedSessionLoad always runs the follow-up even when the count check would otherwise skip it (sessions.js ~line 60). That's exactly right — undo decreases the count, so a count-gated skip would have re-introduced the stale view. session-updated events go through the new _queueSessionUpdatedRefresh (messages.js:7173), which still suppresses cross-session events (if(loadingSid&&loadingSid!==sid) return false;) but coalesces same-session ones into one bounded follow-up carrying minimumMessageCount.

2. [was: reconnect collapses 90→30] — FIXED. refreshSession() (ui.js:9199) now derives its limit from the shared helper instead of an unbounded GET:

const refreshLimit=(typeof _messageReloadLimitForSession==='function'
  ? _messageReloadLimitForSession(sid) : 30)||30;

and _messageReloadLimitForSession (sessions.js:~2975) now falls back to _settledSessionMessageWindowLimit(null,{forceBounded:true}) rather than _INITIAL_MSG_LIMIT. I traced the truncated case: loadedWindow = max(0, loadedRenderableCount) → for a 90-row expanded view it returns max(30, 90, …) = 90, so the expanded window survives the reconnect. Confirmed.

3. [was: scroll yanked to bottom] — FIXED. _finishDone is now async, shouldFollowOnDone is computed after the awaited _fetchSettledSessionMessageWindow and after the re-check if(isActiveSession&&!_isSessionCurrentPane(activeSid)) isActiveSession=false; (messages.js:~5445). The final scroll additionally re-evaluates at DOM-replace time via _followSettledDoneIfStillPinned() (messages.js:5627), so a reader who scrolled up during the fetch is no longer pulled back down.

One minor, non-blocking note

The else if(shouldFollowOnDone&&…) fallback at messages.js:5628 is effectively unreachable, since _followSettledDoneIfStillPinned is a top-level declaration that's always defined. It's harmless and matches the codebase's defensive typeof guard style, so I'd leave it — just flagging that shouldFollowOnDone at :5445 is now only the fallback path; the live decision is the re-evaluation at :5627.

Verification

Regression coverage lands in tests/test_bounded_session_mutations_frontend.py (test_undo_and_retry_use_bounded_session_reload, test_older_message_window_refreshes_canonical_count_and_topbar, test_duplicate_same_session_force_reload_is_coalesced) plus the reconnect/scroll suites. These are source-pattern assertions consistent with the existing frontend test approach. I did not execute them (cron is read-only), but the assertions match the diffs I read.

Net: the "correct window content, wrong lifecycle handling" class is closed — undo refreshes, reconnect keeps the expanded window, scroll-follow is re-evaluated post-await. Direction and execution both look right. Re-gate: PASS from my side, pending your merge call.

@akay64

akay64 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Re-gate: PASS from my side, pending your merge call.

@nesquena-hermes I would suggest to wait, I have run into a case where /retry and /undo wont do a full dump of the transcript, but they will add a batch of 30 to the total. The behavior is better than before, so it wont dump 1500 messages in but also I would not consider it done yet. Id suggest to allow me to run another hardening pass and update this PR before we merge.

You can also take the decision to merge as is as the current behavior is a LOT better than not having it at the moment and I will open another PR after hardening. Your call.

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔬 Re-gate round 2 (Codex) — all 3 prior findings FIXED ✅, one new rotated-continuation edge

Excellent progress @akay64 — all three round-1 regressions are verified fixed:

  1. /undo, /retry, same-session mutation reload — mutation refresh now queues behind an active same-session load and awaits the bounded follow-up (no more stale-visible-after-success).
  2. ✅ Reconnect preserves an expanded window — the current renderable window determines the request limit (90 stays 90, not collapsed to 30).
  3. ✅ Reader scrolling up during the terminal fetch — follow intent is re-evaluated after the await and stays sticky through DOM replacement (no forced scroll-to-bottom).

One new edge the coordinator introduced, on the context-compression rotation path:

  • [SILENT — rotated continuation shows stale parent window] static/messages.js:5429 — after a compression session rotation, the settled-window fetch uses activeSid instead of the completed/rotated completedSid, so the continuation session's messages + tool calls get overwritten with the stale parent-window data. Reproduced (Node): the resulting session id was continuation-new but its visible transcript was STALE PARENT WINDOW instead of FINAL ANSWER. The current tests encode the activeSid fetch, so they pass while missing this. Fix: _fetchSettledSessionMessageWindow(completedSid, completedSession) (use the completed/rotated sid, not the active one), and add a rotated-compression regression at tests/test_settled_session_window.py:160 asserting the bounded window + final answer come from the continuation session (not the parent).

That's the last one — a one-token sid fix (activeSidcompletedSid) plus the rotated-session test. Everything else (stale-generation guarding, duplicate-refresh suppression, full-render promotion, bounded cap per #5966/#5974, and all other stream/reattach/activity paths) verified clean across 95 bounded-lifecycle + 198 streaming/compression tests. Fix the rotation sid + add the test and re-gate — should be green next round.

@akay64

akay64 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Changes:

  • Mutation reloads now size from renderable rows, preventing hidden tool rows from turning a 30-row window into 60.
  • Rotated compression completion now fetches the settled window using completedSid, preserving continuation messages/tool calls.
  • Added focused regressions and updated affected assertions.

Verification passed:

  • 83 bounded lifecycle tests
  • 29 rotation/session-sync tests
  • 30 settled-window/external-refresh tests
  • JS syntax, Ruff, and diff checks

@akay64
akay64 requested a review from nesquena-hermes July 13, 2026 07:44
@akay64

akay64 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes Just wanted to check in if this is good to go now?

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head gate certificate: RED ⛔

Contributor SHA: c9d98d4c6e8a9918312599925720fb392a26c240

The earlier four lifecycle findings are fixed at this head. Current-master integration exposes two additional silent regressions, plus a mechanical rebase/CI blocker.

Blocking findings

  1. Per-session SSE can remain permanently closed after a truncated turn settles (static/messages.js:5785, :5806, :5826; _resumeSessionStreamAfterLiveChat at :7543).

    • _clearOwnerInflightState() schedules the session-stream resume before the awaited bounded-window fetch.
    • The zero-delay callback runs while S.activeStreamId still owns the chat stream, so _chatStreamActiveForSession(sid) returns true and the callback exits.
    • S.activeStreamId is cleared only after the await, and no later resume is scheduled. Background completion and session-updated events can then be lost until navigation/reload.
    • Fix: resume completedSid after terminal ownership is cleared and final state is applied, on fetch success and fallback paths. Add a delayed-fetch regression test that proves the session SSE restarts after settlement.
  2. Reconnect/manual recovery can silently discard loaded transcript rows (static/sessions.js:3149-3182, :3238-3247; static/messages.js:6580; static/ui.js:9560-9568).

    • A fully loaded 100-row pane with _messagesTruncated=false produces msg_limit=30; replacement drops 70 visible rows.
    • An expanded 600-row pane produces msg_limit=600; the backend clamps this to _MAX_MSG_LIMIT=500, then the client wholesale-replaces S.messages, dropping 100 visible rows.
    • Fix: preserve the currently loaded renderable width for non-truncated recovery; never issue an oversized bounded limit that the backend will clamp before replacement. For widths above _msgLimitMax, omit the limit/full-fetch or merge the bounded tail without replacing the loaded head. Make refreshSession() use the same ceiling-aware helper. Add 100-row/full and 600-row/truncated mutation-sensitive tests.
  3. Current-master rebase and test accommodations are required. The PR conflicts in static/sessions.js and tests/test_cross_session_message_load_isolation.py. After compositional conflict resolution, untouched master tests still inspect the old async function loadSession body instead of _loadSessionOnce:

    • test_inflight_stream_reuse.py: 1 failure
    • test_extension_session_hooks.py: 3 failures
    • Update those extractors while retaining both extension-hook/live-recovery and new coordinator harness dependencies.

Evidence

  • Threat scan: CLEAN; all execution was sandboxed.
  • Current-master focused integration: 73 passed, 1 failed; extension-hook slice: 13 passed, 3 failed (the four current-master static-shape blockers above).
  • Direct sandboxed real-helper probe:
    • full100 = ...&msg_limit=30
    • expanded600 = ...&msg_limit=600
    • delayed active-stream clear: starts = 0
  • Codex: SHIP ONLY WITH FIXES, reproduced both product defects.
  • Opus: no additional blocker; it identified the same history-collapse paths as residuals. Reproduction makes them blocking.
  • Branch CI is green on the old base, but does not exercise these current-master conflicts.

Please rebase, preserve the current-master extension/live-recovery behavior in the loader split, fix both product paths, and request re-gate.

@nesquena-hermes nesquena-hermes added gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address ux User experience / visual polish labels Jul 20, 2026
@akay64
akay64 force-pushed the fix/bounded-session-reload branch from c9d98d4 to 24f68d2 Compare July 22, 2026 10:31
@akay64
akay64 marked this pull request as draft July 22, 2026 10:37
@akay64
akay64 marked this pull request as ready for review July 22, 2026 11:44

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate: runtime fixes converge, but the required SSE-restart regression is still false-green

I re-gated exact head 0d6cb6826543ff041b544b9a54c2c22bb2eeed9b against the July 20 gate certificate. The production changes now close the two runtime defects: terminal settlement defers the per-session resume until the finally block after ownership/state finalization, and the recovery URL helper omits unsafe 30-row/over-ceiling limits. The current-master extractor accommodations are also legitimate. Seven targeted sandboxed slices passed, 88 tests total.

One named gate requirement is still missing. tests/test_settled_session_window.py::test_deferred_owner_cleanup_waits_for_settlement_before_resuming_continuation does not drive the production done path or prove a session SSE restarts:

  • it replaces _clearActivePaneInflightIfOwner() with a fake that directly clears S.activeStreamId;
  • it waits on a standalone promise unrelated to _fetchSettledSessionMessageWindow();
  • it manually calls a stub _resumeSessionStreamAfterLiveChat() that only appends to an array;
  • it never runs attachLiveStream, startSessionStream, the LIVE_STREAMS guards, or an EventSource;
  • it has no fetch-rejection/fallback case.

The test would stay green while bypassing the exact ownership/stream-registry interaction that caused permanent post-completion SSE silence. The adjacent source-order assertion is useful, but it is not the requested behavioral regression.

Required fix

Replace or supplement that test with a behavioral Node harness that drives the production terminal callback with a controllable delayed _fetchSettledSessionMessageWindow():

  1. Start with a live chat stream owned by the parent session and a completion that rotates to a continuation session.
  2. While the settled-window promise is pending, assert no per-session subscription restarts.
  3. Resolve the promise and assert terminal state is applied, chat ownership is cleared, and exactly one real/instrumented startSessionStream(continuationSid) or continuation EventSource is created through the production guards.
  4. Repeat with the fetch rejected and prove the bounded local fallback is installed and the same continuation SSE restart occurs.

Please do not stub out the ownership transition or resume guard itself; observing calls/EventSource construction is fine, but the production guard path must run. This is test-only rework unless the faithful harness exposes another source/registry ordering race. The runtime fixes and the other prior findings do not need to be reopened.

@akay64
akay64 requested a review from nesquena-hermes July 22, 2026 12:46
@nesquena-hermes nesquena-hermes removed the changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address label Jul 22, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — RED ⛔

Certified contributor head: a02d1db3ef2a92a94e06ed98be7cc4a41db42a0a
Frozen/current master: e9e9ed09a531562d6ccbe56d114162128136aa22
Rebased gate evidence: a02d1db3ef2a92a94e06ed98be7cc4a41db42a0a (already based on the frozen master; no synthetic rebase delta)
Verdict: not ship-ready. The new production-path SSE regression test is real and mutation-sensitive, but the full exact-head review reproduced five candidate-owned lifecycle regressions outside that test.

What I ran

  • Threat scan: SUSPICIOUS, score 3, solely for the new test's balanced source extraction plus eval. No hot files or other findings. Executable candidate tests ran without network in the sandbox after static adjudication.
  • GitHub CI: all 23 reported checks green at the certified head.
  • Focused sandbox gate: 113 passed across settled-window, bounded-mutation, cross-session isolation, extension hooks, inflight reuse, limit-ceiling, and external-refresh suites.
  • Full serial suite: 13,610 passed in the no-network sandbox. The 10 remaining browser/wheel cases were sandbox-environment failures and all passed on the exact candidate head in the normal test environment; no candidate test remains failing.
  • Mutation sensitivity: disabling only the real production continuation restart makes both new success and rejected-fetch cases fail, proving the test exercises the production handoff.
  • Independent switch-during-settlement probe: clean; switching to another session while the settled fetch is pending does not overwrite the new pane or open a continuation SSE.
  • Codex: KICK BACK TO AUTHOR, with four findings independently reproduced below.
  • Senior review: the previous SSE-test blocker is closed; independently raised the reconnect resolve_model=0 regression reproduced below.
  • Fable: SHIP-UX on the intended happy-path behavior.
  • Static/browser: Ruff-forward, scope-undefined, ESLint runtime, git diff --check, and browser smoke (/, /#settings, /#sessions) clean.
  • Crown-jewel stream matrix: transparent-stream and compact-worklog are clean across all checkpoints. A matched hide_all_activity candidate/control drive produced the same result on both trees: during_stream, after_done, and reload_mid_stream clean; switch_away_back and reload_after_done report no-assistant-turn. The initial all-mode head run's extra reload_mid_stream failure did not reproduce, so no stream-matrix failure is attributed to this PR.

Blocking findings

  1. An older done settlement can clobber a newer same-session chat stream.

    • Drive the real attachLiveStream/done path, hold the settled-window fetch, attach stream-new for the same non-rotated session, then resolve the old fetch.
    • Reproduced final state: LIVE_STREAMS[parent].streamId == "stream-new", but S.activeStreamId == null; the old settled transcript replaces the new owner's messages and no session SSE resumes.
    • Fix: after every settlement await, revalidate both pane identity and exact chat-stream ownership. Only install/clear state when the finishing stream still owns the pane. Preserve a newer owner untouched. Add this interleaving as an executable production-function regression.
  2. A queued refresh for session A can supersede a newer navigation to B.

    • Start A refresh, queue a second A mutation refresh, navigate to B while A is pending, then resolve A.
    • Reproduced API order starts B metadata, then the queued A follow-up increments the generation, invalidates B, and restores sid-atlas with queued-atlas-won.
    • Fix: bind queued follow-ups to the active-load/navigation epoch. A follow-up may start only if no newer cross-session intent owns the coordinator. Add the exact A→queued-A→B race.
  3. Edit/regenerate uses a stale pagination offset after concurrent older-page loading.

    • Start edit at local index 20 in a window with offset 70 (keep_count=90), load older rows so the offset becomes 40 while /truncate is pending, then resolve.
    • Reproduced client rows after truncate: absolute 40–59. Correct rows are 40–89. The stale offset silently drops 30 still-valid rows before resend.
    • Fix: serialize mutation and pagination, or compute the post-truncate local slice from the current window offset/absolute identities after the await. Add edit and regenerate pagination-race tests.
  4. Extension preload vetoes can cancel internal undo/retry refreshes after the server mutation succeeds.

    • With a before-session-open handler returning {cancel:true}, both /undo and /retry mutate the server, but loadSession starts no internal reload.
    • Reproduced /retry: stale pane remains and send() still runs with retry-user. Reproduced /undo: stale pane remains while the UI shows the success toast.
    • Fix: internal mutation recovery must bypass navigation veto hooks (for example, an explicit internal/skipExtHooks contract), or treat cancellation as a hard mutation-recovery failure and do not resend/announce success. Add executable veto tests for both commands.
  5. Reconnect refresh now leaves unresolved model aliases in rendered state.

    • refreshSession changed from the default resolved session request to _sessionMessageReloadUrl(...resolve_model=0) and does not schedule _resolveSessionModelForDisplaySoon.
    • Reproduced one API call with resolve_model=0; S.session.model and the top bar remain fast-alias, with no resolve_model=1 follow-up.
    • Fix: schedule the same deferred model/provider/context hydration used by ordinary session loads, or preserve resolved display metadata across the bounded refresh. Add an alias/provider refresh regression.

Required next head

Fix all five ownership/recovery defects, keep the new production SSE test, and add executable regressions for each interleaving above. Then run a full exact-head re-gate from zero. This certifier did not merge, tag, deploy, or close the PR.

@akay64
akay64 marked this pull request as draft July 22, 2026 16:59
@akay64
akay64 marked this pull request as ready for review July 22, 2026 20:59

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate: the older settlement still idles the replacement stream

I re-gated exact head 6e0005261529a83a9d229daeed69032b0eec8b21 against the prior exact-owner finding. The new guard at static/messages.js:5837-5844 correctly prevents an older done continuation from replacing the newer same-session stream's transcript, tool state, and S.activeStreamId. The earlier undo/retry, reconnect-window, scroll-follow, rotated-continuation, and renderable-count fixes also remain intact.

One ownership side effect is still outside that guard:

  • When _settlementStillOwnsPane is false, the code sets isActiveSession=false, but control still reaches the unconditional _setActivePaneIdleIfOwner() at static/messages.js:6055.
  • _setActivePaneIdleIfOwner() treats same-session pane identity as ownership (static/messages.js:2249-2254) and calls setBusy(false).
  • The real setBusy(false) (static/ui.js:7910-7951) clears busy/composer/status state and may dequeue and send a queued message. An older settled stream can therefore mark stream-new idle and drain its queue even though the new code correctly preserves S.activeStreamId === "stream-new" and LIVE_STREAMS[parentSid].streamId === "stream-new".

The new regression misses this. _done_continuation_stream_harness stubs setBusy as a no-op (tests/test_settled_session_window.py:436-452), seeds S.busy=true, but does not emit or assert busy/composer/status/queue-drain effects. Both success and rejection cases can stay green while the replacement stream is incorrectly idled.

Required fix

  1. Carry the exact (streamId, source) ownership result through every active-pane terminal side effect. If the finishing settlement no longer owns the pane, it must not call the active-pane idle path, clear busy/composer/status state, set _queueDrainSid, or drain a queued item. Do not use same-SID pane identity as a substitute for stream ownership.
  2. Make the production-function harness observe setBusy, S.busy, status/composer clears, and queue-drain/send effects. For both settlement success and rejection, assert that a newer same-session owner remains busy and no queued item drains. Keep a control proving that a genuinely owning completion becomes idle.

Layer-1 threat scan is SUSPICIOUS solely on the fixed-source eval(extractFrom(...)) harness shape, so policy required a strict no-run review. I did not execute candidate code or tests. That scanner verdict is not itself the defect or a contributor-quality finding; the ownership control flow above is independently established by the current source. Please fix it and request another exact-head re-gate.

@akay64

akay64 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes Thanks for the detailed re-gate. The remaining exact-owner issue appears valid, but I’m no longer able to continue expanding this PR into the surrounding settlement and active-pane lifecycle.

The current fix resolves the original full-transcript reload problem and has been working reliably in my daily use over the last week. However, I understand that the remaining ownership edge case prevents it from meeting the project’s merge gate.

I’m therefore withdrawing from further work on this PR. The maintainers are welcome to reuse or build on any of the commits if useful. Thank you for the reviews thus far. 🫡

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thanks @akay64 for the substantial bounded-window and lifecycle work here. The branch fixed several real defects, and the review history preserves reusable fix shapes and tests. The latest exact head still allows an older settlement to idle a replacement same-session stream, and you have explicitly withdrawn from further expansion of this PR.

Closing this implementation lane with credit, not closing the underlying issues. Maintainers may reuse the commits or test cases in a narrower replacement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push size:L Large PR (>10 files or >250 LOC) ux User experience / visual polish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants