Files
DeepSeek-Reasonix/internal/control/snapshot_conflict_diagnostic.go
T
yhh b18d8ef3aa feat(studio): storage relocation, composer intake, and the checkpoint agreement
Accumulated work from the branch, carried across the rebase onto origin/studio
and merged with what landed there in the meantime.

Storage: a workspace can name where its state lives, and the server tracks the
one relocation it may be running (internal/storage, config/storage_roots,
serve/storage, the Storage panel).

Composer intake: what a drop or a long paste becomes is one pure function —
payload in, action out — so "what happens if I drop a folder" is a test rather
than a branch of an onDrop handler. It sits on the drop router that landed
upstream: that one answers which pane a drop hit, this one answers what it
means and says so before the pointer is released. A paste long enough to bury
the composer becomes a chip beside it instead.

Session: the checkpoint agreement and the lineage/torn-log recovery cases,
plus the conflict diagnostic learning where two transcripts parted and which
roles stood there — a position and a role tell a local reshape from a foreign
write without recording any content.

Model refs: the default-model write checks the live catalog, so an extension
model the runtime knows can be persisted as the default.
2026-08-19 23:52:04 -07:00

91 lines
2.8 KiB
Go

package control
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"reasonix/internal/agent"
"reasonix/internal/store"
)
type snapshotConflictDiagnostic struct {
At time.Time `json:"at"`
BranchID string `json:"branch_id"`
Mode string `json:"mode"`
Outcome string `json:"outcome"`
Kind string `json:"kind,omitempty"`
DiskMessages int `json:"disk_messages,omitempty"`
SnapshotMessages int `json:"snapshot_messages,omitempty"`
BaseRevision int64 `json:"base_revision,omitempty"`
DiskRevision int64 `json:"disk_revision,omitempty"`
RecoveryBranchID string `json:"recovery_branch_id,omitempty"`
ExistingRecovery bool `json:"existing_recovery,omitempty"`
// DivergedAt and the two roles say where the transcripts parted and what
// stood there: an assistant turn that became a tool row is a local reshape,
// two different assistant turns are two writers. No content is recorded.
DivergedAt int `json:"diverged_at"`
DiskRole string `json:"disk_role,omitempty"`
SnapshotRole string `json:"snapshot_role,omitempty"`
}
// conflictDiagDedup bounds repeated conflict event log lines for the same
// {path, disk revision} key within a process.
var conflictDiagDedup sync.Map // key -> struct{}
func appendSnapshotConflictDiagnostic(path, mode, outcome string, saveErr error, recoveryPath string, existing bool) {
path = strings.TrimSpace(path)
if path == "" {
return
}
var diskRev int64
var conflict *agent.SessionSnapshotConflictError
if errors.As(saveErr, &conflict) && conflict != nil {
diskRev = conflict.DiskRevision
}
dedupKey := path + "\x00" + outcome + "\x00" + strconv.FormatInt(diskRev, 10)
if _, loaded := conflictDiagDedup.LoadOrStore(dedupKey, struct{}{}); loaded {
return
}
rec := snapshotConflictDiagnostic{
At: time.Now(),
BranchID: agent.BranchID(path),
Mode: mode,
Outcome: outcome,
DivergedAt: -1,
}
if conflict != nil {
rec.Kind = string(conflict.Kind)
rec.DiskMessages = conflict.ExistingMessages
rec.SnapshotMessages = conflict.SnapshotMessages
rec.BaseRevision = conflict.BaseRevision
rec.DiskRevision = conflict.DiskRevision
rec.DivergedAt = conflict.DivergedAt
rec.DiskRole = conflict.DiskRole
rec.SnapshotRole = conflict.SnapshotRole
}
if recoveryPath != "" {
rec.RecoveryBranchID = agent.BranchID(recoveryPath)
rec.ExistingRecovery = existing
}
data, err := json.Marshal(rec)
if err != nil {
return
}
logPath := store.SessionConflictLog(path)
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
return
}
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return
}
defer f.Close()
_, _ = f.Write(append(data, '\n'))
}