Files
DeepSeek-Reasonix/internal/session/transcript.go
T
SivanCola 8b426dc870 Fix submission identity and persistent PowerShell / 修复消息身份、工具状态与持久 PowerShell (#10392)
* Preserve submission identity and unify shell execution state

Problem: optimistic messages could duplicate canonical history, tool views disagreed on terminal state, and Windows terminal rendering could corrupt completion markers.

Root cause: submission identity was not durable across transports and projections, and execution completion depended on rendered terminal text.

Fix: persist optional submission receipts at Controller admission, reconcile transcript identities, share tool presentation, and run persistent PowerShell with a private control channel while preserving legacy message formats.

Verification: owning Go packages, frontend typecheck and focused UI suites, Desktop module tests, PowerShell 7 live tests, compatibility reader checks and browser transcript benchmarks. Native Windows and packaged application qualification remain required.

* Document persistent Windows Agent shell behavior

Explain PowerShell selection, session state, isolated calls and emergency one-shot rollback in English and Chinese. Documentation-only validation: checked paths and behavior against the implementation.

* Include Windows pipe dependency in the Desktop module

Problem: the native Windows package build failed with a missing go.sum entry.
Root cause: Desktop is an independent module and did not inherit root module checksums.
Fix: record go-winio in Desktop module metadata.
Verification: go mod tidy completed; Windows package build is being rerun.

* Preserve explicit POSIX hooks under Windows Agent shell policy

Problem: Windows CI found explicit sh hooks were redirected through Agent shell selection, and the cross-platform golden fixture could no longer force Bash.
Root cause: hook dialect selection shared the newly Windows-native Agent policy; the cache fixture assumed a Bash preference overrode that policy.
Fix: retain explicit Bash discovery for POSIX hooks with a deterministic regression, record a separate pinned Windows PowerShell provider golden, and extract the durable submission-start barrier to keep event routing within its complexity budget.
Verification: focused control/sandbox/hook/boot tests and clean-tree repolint passed; Windows PowerShell golden passed natively. Explicit hook validation is continuing after installing Git for Windows.

* Retain submission identity in rejected-send regression

Problem: the legacy checkpoint test expected send failure to erase submission identity.
Root cause: the assertion encoded transient correlation semantics before durable identity reconciliation.
Fix: assert that identity remains available for late canonical receipts while keeping stale-turn and failure isolation checks.
Verification: checkpoint-turn-event.test.ts passed all 61 assertions.
2026-09-16 14:34:13 +08:00

100 lines
3.1 KiB
Go

package session
import (
"context"
"encoding/json"
"slices"
"reasonix/internal/provider"
"reasonix/internal/transcript"
"reasonix/internal/turnevent"
)
// Transcript is owned by the runtime, not a replaceable controller. The
// Session acceptance lock serializes canonical batches and transient frames.
// No subscriber callback, disk read or network write runs under that lock.
func (r *Runtime) Transcript() *transcript.Projection { return r.transcript }
func (r *Runtime) FollowTranscript(ctx context.Context, req transcript.FollowRequest) (transcript.FollowResponse, error) {
r.session.mu.Lock()
if r.session.binding != nil {
durable, _, _ := r.session.binding.progress()
r.transcript.SetDurableSequence(durable)
}
r.session.mu.Unlock()
return r.transcript.Follow(ctx, req)
}
func (r *Runtime) PublishTranscriptFrame(envelope turnevent.Envelope) error {
s := r.session
s.mu.Lock()
defer s.mu.Unlock()
if s.binding != nil {
durable, _, _ := s.binding.progress()
r.transcript.SetDurableSequence(durable)
}
return r.transcript.ApplyFrame(envelope, s.next-1)
}
func (s *Session) acceptTranscriptCommit(commit Commit) {
if s.transcript == nil {
return
}
var messages []provider.Message
var removed []string
rewrite := false
for _, e := range commit.Events {
switch e.Kind {
case "message/complete", "message/upsert":
var payload struct {
Message provider.Message `json:"message"`
}
if json.Unmarshal(e.Payload, &payload) == nil {
removed = slices.DeleteFunc(removed, func(id string) bool { return id == payload.Message.ID })
messages = append(messages, payload.Message)
}
case "message/retract":
ids, err := retractedMessageIDs(e, e.Payload)
if err == nil {
removed = append(removed, ids...)
messages = slices.DeleteFunc(messages, func(message provider.Message) bool { return slices.Contains(ids, message.ID) })
}
case "history/replace", "legacy/import":
var payload struct {
Messages []provider.Message `json:"messages"`
}
if json.Unmarshal(e.Payload, &payload) == nil {
messages, rewrite = payload.Messages, true
removed = nil
}
}
}
finalID := s.projection.CurrentTurnMessageID
if finalID == "" && len(s.projection.Turns) > 0 {
last := s.projection.Turns[len(s.projection.Turns)-1]
if last.TurnID == commit.TurnID || s.projection.TurnID == "" {
finalID = last.MessageID
}
}
rows := s.transcriptRows(messages)
if len(removed) > 0 && !rewrite {
s.transcript.AcceptRetractions(rows, removed, commit.LastSequence(), commit.TurnID, finalID)
} else {
s.transcript.AcceptBusiness(rows, commit.LastSequence(), commit.TurnID, rewrite, finalID)
}
}
// Caller holds s.mu; live commits and reopened sessions use the same identities.
func (s *Session) transcriptRows(messages []provider.Message) []transcript.Message {
rows := transcript.History(messages, transcript.HistoryOptions{})
for i := range rows {
if rows[i].Role != "user" {
continue
}
if receipt, ok := s.projection.Submissions.byMessage[s.id+"\x00"+rows[i].MessageID]; ok {
rows[i].SubmissionID = receipt.SubmissionID
}
}
return rows
}