Files
DeepSeek-Reasonix/internal/control/transcript_persistence.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

75 lines
1.9 KiB
Go

package control
import (
"errors"
"reasonix/internal/agent"
"reasonix/internal/transcript"
"reasonix/internal/turnevent"
)
func (c *Controller) captureTranscriptCheckpoint(ledger *turnevent.Ledger, digest string) {
if c.sessionEngineEnabled() {
return
}
if digest == "" {
digest, _ = agent.ContentDigestForMessages(c.History())
}
p, err := c.transcriptProjection()
if err != nil {
return
}
state, err := p.Checkpoint(digest)
state.ProviderCount = c.HistoryLen()
c.turnEvents.mu.Lock()
defer c.turnEvents.mu.Unlock()
if c.turnEvents.ledger != ledger {
return
}
if err != nil {
c.turnEvents.projectionWriteErr = err
return
}
c.turnEvents.pendingCheckpoint = &state
}
// Serializes retries and terminal writes, then reads the latest pending cut.
// A delayed retry cannot overwrite a newer successful checkpoint with an old
// one. No controller or projection lock is held during disk I/O.
func (c *Controller) persistTranscriptCheckpoint(ledger *turnevent.Ledger) error {
if c.sessionEngineEnabled() {
return nil
}
c.turnEvents.persistMu.Lock()
defer c.turnEvents.persistMu.Unlock()
c.turnEvents.mu.RLock()
if c.turnEvents.ledger != ledger {
c.turnEvents.mu.RUnlock()
return errors.New("transcript runtime changed before checkpoint persistence")
}
state := c.turnEvents.pendingCheckpoint
path := c.turnEvents.projectionPath
priorErr := c.turnEvents.projectionWriteErr
projectionErr := c.turnEvents.projectionErr
c.turnEvents.mu.RUnlock()
if projectionErr != nil {
return projectionErr
}
if state == nil {
return priorErr
}
err := transcript.SaveCheckpoint(path, *state)
c.turnEvents.mu.Lock()
defer c.turnEvents.mu.Unlock()
if c.turnEvents.ledger == ledger {
c.turnEvents.projectionWriteErr = err
if err == nil {
c.turnEvents.projectionPersistedThrough = state.CoveredThroughSeq
if c.turnEvents.pendingCheckpoint == state {
c.turnEvents.pendingCheckpoint = nil
}
}
}
return err
}