diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index d22efed..f5ba00b 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -555,11 +555,25 @@ final class HostConnection { case .transferAccept, .transferReject, .transferReady, .transferCommitted, .transferChunkAck: // Session-transfer replies (mesh P5) only reach a *source* Mac; a phone is never one. break - case .transcriptChunk, .transcriptFetchComplete, .transcriptUnavailable: - // Full-transcript fetch replies (mesh sync) only arrive in response to a - // `fetchTranscript` this device sent. A phone pulls full history on demand via a - // dedicated fetch path (not this subscribe stream), so nothing to do here yet. - break + case .transcriptChunk(let chunk): + // One ordered slice of the full-history fetch we started on open. Match on request id so + // a stale batch from a previous open (session switched underneath us) is ignored. + guard chunk.requestID == transcriptFetchRequestID, chunk.sessionID == openSessionID else { break } + // De-dup against the tail/live stream and advance the warm-resubscribe cursor exactly like + // a snapshot/events batch, then hand the fresh (older) events to RemoteStore, which merges + // them into the transcript by seq rather than appending. + let fresh = chunk.events.filter { !seenSeq.contains($0.seq) } + seenSeq.formUnion(chunk.events.map(\.seq)) + if let maxSeq = chunk.events.map(\.seq).max() { openMaxSeq = max(openMaxSeq ?? 0, maxSeq) } + if !fresh.isEmpty { callbacks.openBackfill(EventBatch(sessionID: chunk.sessionID, events: fresh)) } + case .transcriptFetchComplete(let done): + // Terminal success — every batch shipped. Clear the in-flight id; the cursor/dedup state + // is already advanced by the chunks above. + if done.requestID == transcriptFetchRequestID { transcriptFetchRequestID = nil } + case .transcriptUnavailable(let un): + // The host holds no transcript for this session (or can't serve the format). The cold tail + // still shows; there's just no deeper history to add. Clear the in-flight id. + if un.requestID == transcriptFetchRequestID { transcriptFetchRequestID = nil } case .toolSummaries: // Collapsed Bash summary lines the owner pushes to peer *Macs* (mesh session sync). // The phone renders its own deterministic command summaries, so it ignores these.