Files
DeepSeek-Reasonix/desktop/transcript_api.go
T
SivanCola 738e9b956e Unify transcript recovery and completion with Follow v2 / 统一会话同步与完成机制 (#10385)
* Unify transcript recovery and completion through runtime-owned v2

Problem: completed or reconnecting chats could lose visible output, infer incorrect run state, or display an intermediate duration.

Root cause: independent history, event-ledger and runtime snapshots described different cuts, and sampling settlement could precede its business commit.

Fix: publish canonical batches and independently versioned sampling frames from the session runtime; use one mandatory Follow v2 consumer for local and remote chat, persistence barriers, stable identities and bounded canonical paging. Preserve old session logs and provider serialization.

Verification: root and Desktop Go suites, transcript/session/control race tests, 371 frontend suites, typechecks, production build, Chromium and 42 macOS Electron layout scenarios; Windows cross-compilation. Native Windows verification follows on this branch.

* Keep contributor prompt instructions outside transcript migration

Problem: changing contributor AGENTS text affects repository instruction inputs. Root cause: protocol documentation had also been copied into the contributor instruction file. Fix: retain the existing instructions and keep migration details in the bilingual user documentation. Verification: cache and provider serialization guards passed; no runtime code changed.

* Separate transcript bookkeeping and honor native viewport geometry

Problem: transcript additions exceeded existing complexity and file-size budgets, and the Windows layout fixture assumed odd native widths were exact CSS widths. Root cause: runtime reduction, settlement publication and store contracts were embedded in larger owners; 200% Windows scaling rounded a requested 1009px surface to 1010px. Fix: extract cohesive helpers/types without changing publication order or provider bytes; test representable sizes on both sides of the launcher threshold and measure actual CSS geometry. Verification: repolint, transcript/type/hooks checks and focused Go tests pass; Windows native tests and race checks exercise the extracted boundaries.

* Migrate remote regression fixtures to transcript Follow

Route mock remote output through the mandatory Follow stream and keep metadata separate from snapshot cuts. Assert prefix preservation and ordered completion instead of legacy history rebases. Use unique mock sampling identities across turns and wait for pane transitions in native layout tests.

* Use reverse slice iteration for transcript retractions

* Give explicit transcript retries a fresh snapshot

A retry during ancillary hydration could join an already-installed old snapshot. Mark explicit retry reads as fresh so they supersede the previous load while ordinary startup reads still coalesce. Force a pending ancillary read in the regression and verify its late completion cannot overwrite the replacement.
2026-09-16 09:20:23 +08:00

137 lines
4.6 KiB
Go

package main
import (
"context"
"fmt"
"reasonix/internal/control"
"reasonix/internal/transcript"
)
// ResumeTranscriptSessionForTab adopts a session without materializing a legacy
// history response. The caller obtains its bounded page from TranscriptSnapshotForTab.
func (a *App) ResumeTranscriptSessionForTab(tabID, path string) (HistorySwitchPhases, error) {
page, err := a.resumeSessionForTranscript(tabID, path, 0, false)
if page.Switch == nil {
return HistorySwitchPhases{}, err
}
return *page.Switch, err
}
func (a *App) OpenChannelTranscriptSessionForTab(tabID, path string) (HistorySwitchPhases, error) {
page, err := a.openChannelSessionForTranscript(tabID, path, 0, false)
if page.Switch == nil {
return HistorySwitchPhases{}, err
}
return *page.Switch, err
}
func (a *App) transcriptAPIForTab(tabID string) (control.TranscriptProjectionAPI, func() bool, error) {
tab, ctrl := a.tabAndCtrlByID(tabID)
if ctrl == nil {
return nil, nil, a.workspaceNotReadyErr(tab)
}
api, ok := ctrl.(control.TranscriptProjectionAPI)
if !ok {
return nil, nil, control.ErrTranscriptProjectionUnavailable
}
a.mu.RLock()
bound := tab != nil && a.tabs[tabID] == tab && tab.Ctrl == ctrl
epoch := a.runtimeEpochForTabLocked(tab)
a.mu.RUnlock()
if !bound {
return nil, nil, fmt.Errorf("runtime changed while binding transcript")
}
return api, func() bool {
a.mu.RLock()
defer a.mu.RUnlock()
return a.tabs[tabID] == tab && tab.Ctrl == ctrl && a.runtimeEpochForTabLocked(tab) == epoch
}, nil
}
func (a *App) TranscriptSnapshotForTab(tabID string, req transcript.PageRequest) (transcript.Snapshot, error) {
api, current, err := a.transcriptAPIForTab(tabID)
if err != nil {
return transcript.Snapshot{}, err
}
result, err := api.TranscriptSnapshot(req)
if !current() {
return transcript.Snapshot{}, fmt.Errorf("runtime changed while reading transcript")
}
return result, err
}
func (a *App) TranscriptFollowForTab(tabID string, req transcript.FollowRequest) (control.TranscriptFollowResponse, error) {
api, current, err := a.transcriptAPIForTab(tabID)
if err != nil {
return control.TranscriptFollowResponse{}, err
}
follow, ok := api.(control.TranscriptFollowAPI)
if !ok {
return control.TranscriptFollowResponse{}, control.ErrTranscriptProjectionUnavailable
}
// Follow is a subscription, not a bounded command. Its 25-second idle
// poll must not be cancelled by the 15-second command deadline.
ctx := a.bootContext()
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
result, err := follow.TranscriptFollow(ctx, req)
if !current() {
if result.Subscription != "" {
_, _ = follow.TranscriptFollow(context.Background(), transcript.FollowRequest{Subscription: result.Subscription, Close: true})
}
return control.TranscriptFollowResponse{}, fmt.Errorf("runtime changed while following transcript")
}
return result, err
}
func (a *App) TranscriptPageForTab(tabID string, req transcript.PageRequest) (transcript.Snapshot, error) {
return a.TranscriptSnapshotForTab(tabID, req)
}
// TranscriptOutlineForTab pages the complete turn index of one snapshot. The
// capability is optional beside the projection, so a controller without it is
// reported as unavailable instead of answering an empty outline.
func (a *App) TranscriptOutlineForTab(tabID string, req transcript.OutlineRequest) (transcript.OutlinePage, error) {
api, current, err := a.transcriptAPIForTab(tabID)
if err != nil {
return transcript.OutlinePage{}, err
}
outline, ok := api.(control.TranscriptOutlineAPI)
if !ok {
return transcript.OutlinePage{}, control.ErrTranscriptProjectionUnavailable
}
result, err := outline.TranscriptOutline(req)
if !current() {
return transcript.OutlinePage{}, fmt.Errorf("runtime changed while reading transcript outline")
}
return result, err
}
func (a *App) TranscriptContentForTab(tabID string, req transcript.ContentRequest) (transcript.ContentChunk, error) {
api, current, err := a.transcriptAPIForTab(tabID)
if err != nil {
return transcript.ContentChunk{}, err
}
result, err := api.TranscriptContent(req)
if !current() {
return transcript.ContentChunk{}, fmt.Errorf("runtime changed while reading transcript content")
}
return result, err
}
func (a *App) TranscriptReplayForTab(tabID string, req control.TranscriptReplayRequest) (control.TranscriptReplay, error) {
api, current, err := a.transcriptAPIForTab(tabID)
if err != nil {
return control.TranscriptReplay{}, err
}
result, err := api.TranscriptReplay(req)
if !current() {
return control.TranscriptReplay{}, fmt.Errorf("runtime changed while replaying transcript")
}
return result, err
}