The first-turn wire sequence emitted by the harness is:
turn_started(sessionId=null) → session/new(sessionId=null) → session_resolved(sessionId=X) → session/prompt(sessionId=X)
The previous splitIntoSessionRuns opened a synthetic 'unknown' run for the
leading null-session items, then opened a second run when session_resolved
arrived. This caused two bugs:
1. Leaked system prompt: the session/new metadata had no following user prompt
in its 'unknown' run so pendingSystemPrompt was never consumed and emitted
as a standalone 'System prompt' row. Caught by Desktop Smoke E2E (3),
observer-feed-screenshots.spec.ts:726 (expected count 0, got 1).
2. Spurious session boundary: two runs from one session triggered
buildTranscriptDisplayBlocks to inject a session-boundary block on a feed
that has exactly one session — the opposite of the signal this PR adds.
Fix: buffer pre-resolution null-session items and prepend them to the first
run that has a non-null sessionId. Only if the entire stream has no resolved
session do they form a fallback 'unknown' run. Mid-stream null-session items
(after resolution) continue to attribute to the current run as before.
Added two regression tests:
- firstTurnSequence_noStandaloneSystemPrompt: asserts zero standalone
session/new blocks, zero session-boundary blocks, system prompt present
inside the turn block (prompt bundle).
- genuineSecondSession_boundaryPreserved: asserts the deferral fix does not
collapse two genuinely distinct sessions — boundary is still injected.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Two observer-feed defects fixed in a single PR:
Slice 1 — channel-scoped archive (index at ingest + idempotent backfill)
The archive read path queried all owner_p kind 24200 rows for an identity,
then relied on a display-time scopeByChannel filter. When the filter was
missing or skipped, frames from other channels leaked into the current
channel's observer feed.
Fix: add an observer_channel_index table that maps (identity, relay, event_id)
to channel_id. Three new Tauri commands drive this:
- read_archived_observer_events_for_channel: channel-scoped paginated read via
the index (replaces the raw owner_p scan in useLoadArchivedObserverEvents)
- index_observer_channel_id: TS calls this after decrypting events to write
index entries
- read_unindexed_observer_rows: returns all owner_p kind 24200 rows not yet
indexed, for the one-shot idempotent backfill
Backfill strategy (choice i, backfill-before-read): a React.useEffect in
useLoadArchivedObserverEvents runs once on mount, reads all unindexed rows,
decrypts each, and batch-writes index entries. After backfill the channel
index is authoritative — the scoped read pages it directly with no zero-match
raw-page guard needed.
Null/decrypt-failed channelId rows stay unscoped and are hidden from every
scoped channel view (Will's ruling: option (a) — display decision only, rows
stay on disk).
Slice 2 — session-boundary rendering
Items rendered flat with no session grouping; archived and live frames
interleaved without any visual marker of where one agent session ended and
another began, causing users to think archived historical context is still
live.
Fix: split TranscriptItem[] into contiguous session runs before calling
buildTranscriptDisplayBlocks. This also fixes the pendingSystemPrompt
single-slot clobber (each run gets its own lifecycle). A new session-boundary
display block kind is interleaved between runs; SessionBoundaryDivider renders
a horizontal rule with session start time. No boundary emitted for
single-session transcripts.
Liveness — "current session" gating
Track latest-live-session-id per (normalized agent pubkey, channelId) in
observerRelayStore, set on the handleRelayObserverEvent path, cleared in
resetAgentObserverStore. Label a boundary "current session" only when it is
both the newest-visible session AND equals that latest-live id; otherwise label
it "most recent observed session".
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>