Skip to content

fix(desktop): preserve archived observer history#1752

Merged
wpfleger96 merged 4 commits into
mainfrom
duncan/observer-archive-window
Jul 11, 2026
Merged

fix(desktop): preserve archived observer history#1752
wpfleger96 merged 4 commits into
mainfrom
duncan/observer-archive-window

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

After a app restart or Command+R reload, the observer panel body showed
archived rows while the thread header said "No updates yet", and the
raw event rail showed an empty count. Scrolling back through older history
triggered archive page loads that were stored but never signaled to React
subscribers, so the UI did not rerender until an unrelated live event
arrived.

Design

Keeps a channel-keyed uncapped raw archive journal (archiveEventsByChannel
in observerRelayStore). All display state is derived once from a single
merged raw-event window:

  • mergeObserverEventWindows(liveScoped, archived) deduplicates by
    (seq, timestamp) and sorts ascending — same identity and ordering as
    compareObserverEvents.
  • buildTranscriptState(combined) runs once over the combined window so
    stateful aggregates (tool start/update, permission request/response,
    plan replacement, message chunks) are never split across two independent
    state machines.
  • The same combined window feeds the transcript, raw event rail, and header
    event count in ManagedAgentSessionPanel.
  • AgentSessionThreadPanel header feeds the same merged window into its
    latest-timestamp computation so "Last updated …" reflects the full loaded
    history, not only the capped live window.
  • Archive page ingest notifies useSyncExternalStore subscribers once per
    batch that added new events; duplicate-only pages do not notify.

Stable boundary identity

Session-group boundary keys use sessionId:firstItemId instead of
runIndex:firstItemId, eliminating key churn when archive pages prepend
events that shift earlier run indices.

Behavior coverage

  • Full 50-row archive page produces exactly one batched subscriber notification.
  • Duplicate-only archive pages produce zero notifications.
  • Tool start (archive) + tool update (live) yields one complete tool row.
  • Permission request (archive) + response (live) yields the production-shaped
    outcome label (Approved (allow_once)).
  • Raw rail / header count includes archived rows after reload-shaped
    archive-only ingest.
  • More than 3,000 loaded archive events are retained.
  • Header "Last updated" timestamp is non-null after archive-only ingest.

@wpfleger96 wpfleger96 requested a review from a team as a code owner July 11, 2026 01:56
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 10, 2026 22:28
… stabilize session-boundary keys

Archive events now route to a per-channel uncapped store instead of the
3,000-event live-relay cap. Session-boundary React keys use firstItemId
(stable across prepend) instead of runIndex (unstable on archive load).

Closes data-loss regression: >3,000 archived frames were silently
discarded. Closes key-churn regression: archive page loads caused
unnecessary boundary node remounts.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…age ingest

Archive page ingest now calls notifyListeners() once after the batch,
so useSyncExternalStore subscribers rerender after every page load.
Previously a full 50-row page wrote to the store silently.

The live/archive merge now happens at the raw ObserverEvent[] level:
mergeObserverEventWindows() deduplicates by (seq, timestamp) and
ManagedAgentSessionPanel calls buildTranscriptState() once over the
combined sorted window. Previously two independent state machines derived
TranscriptItem[] separately, losing stateful aggregates (tool start/update,
permission request/response, plan replacement) that straddle the 3,000-event
live/archive boundary.

The same combined raw window feeds both the transcript and the raw event
rail / header count, so the count no longer shows 0 after archive-only
reload.

Removes archiveTranscriptByChannel (derived), getArchivedChannelTranscript,
useArchivedChannelTranscript, and mergeTranscriptWindows. Replaces with
getArchivedChannelEvents (raw), useArchivedChannelEvents, and
mergeObserverEventWindows.

Adds 5 regression tests: subscription notification on full page, no
notification on pure-duplicate page, tool start+update across boundary,
permission request+response across boundary, raw-rail count after
archive-only ingest.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three pass-3 corrections:

- Assert exactly one batched subscriber notification (notifyCount === 1) for
  a full 50-row archive page, proving the batch contract rather than just
  liveness.

- Reshape the permission cross-boundary fixture to use production-shaped
  optionId/kind fields (allow_once) and outcome:"selected", and assert the
  final row label is exactly "Approved (allow_once)" rather than non-null.

- AgentSessionThreadPanel header: feed useArchivedChannelEvents +
  mergeObserverEventWindows into the latest-timestamp computation so the
  header shows "Last updated …" rather than "No updates yet" after a
  reload where only archived events exist. Drop the live-only
  useAgentTranscript dependency; raw combined events fully determine the
  header timestamp. Add a focused regression that proves the merged window
  yields a finite timestamp after archive-only ingest.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/observer-archive-window branch from 9fb46e2 to 8b4a1df Compare July 11, 2026 02:28
@wpfleger96 wpfleger96 changed the title fix(desktop): separate archive transcript window from live-relay cap, stabilize session-boundary keys fix(desktop): preserve archived observer history Jul 11, 2026
The prior copy claimed current-context knowledge that the frontend
cannot reliably determine across rotate, crash, and reload transitions.
@wpfleger96 wpfleger96 merged commit 51234bd into main Jul 11, 2026
25 checks passed
@wpfleger96 wpfleger96 deleted the duncan/observer-archive-window branch July 11, 2026 03:46
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.

1 participant