mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
refactor: give remote state publication and reviewed keys one owner
Problem: The generic remote event bridge recovered a tab ID by parsing the event name to stamp hydration revisions, reviewed-version detection matched a substring of the source key, and the integrity-reuse comment claimed the SQLite header detects checkpoints that WAL-mode commits need not record there. Root cause: Revision stamping was attached to the transport rather than to the state publisher, and the reviewed-version key format had no named parser. Fix: Route all twelve state publications through publishRemoteTabState, which stamps the revision before emitting; emitRemoteEvent no longer inspects state payloads. Parse reviewed keys with historicalVersionSourceKey, which requires both a source key and a version. Correct the header comment: file identity, size and mtime remain the primary change signal. Verification: The reviewed-key test failed before the parser existed. Full reasonix/desktop package (717s), remote hydration/publication and source alias race tests, internal/projectiondb, go vet, gofmt and repolint passed.
This commit is contained in:
1 parent
ddd0f11cd0
commit
88c3a736ba
12 files changed
+50
-31
No files matched your search
@@ -7,6 +7,13 @@ import (
|
||||
"reasonix/desktop/internal/workspacestate"
|
||||
)
|
||||
|
||||
// historicalVersionSourceKey reports whether key names a reviewed version: the
|
||||
// original source key, ":review:", then that version's fingerprint.
|
||||
func historicalVersionSourceKey(key string) bool {
|
||||
source, version, reviewed := strings.Cut(key, ":review:")
|
||||
return reviewed && source != "" && version != ""
|
||||
}
|
||||
|
||||
func historicalSourceKeyMatches(mappingKey, sourceID string) bool {
|
||||
return mappingKey == sourceID || strings.HasPrefix(mappingKey, sourceID+":review:")
|
||||
}
|
||||
|
||||
+13
-10
@@ -22,19 +22,22 @@ func (a *App) RemoteTabHydrationState(tabID string) (RemoteTabStateView, error)
|
||||
return RemoteTabStateView{State: tab.state, Error: tab.err, HydrationRevision: tab.publication.hydrationRevision}, nil
|
||||
}
|
||||
|
||||
// publishRemoteTabState is the only writer of the state channel, so every
|
||||
// publication carries the tab's next hydration revision. Callers must not hold
|
||||
// remoteTabMu.
|
||||
func (a *App) publishRemoteTabState(tabID string, view RemoteTabStateView) {
|
||||
a.remoteTabMu.Lock()
|
||||
if tab := a.remoteTabs[tabID]; tab != nil {
|
||||
tab.publication.hydrationRevision++
|
||||
view.HydrationRevision = tab.publication.hydrationRevision
|
||||
}
|
||||
a.remoteTabMu.Unlock()
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), view)
|
||||
}
|
||||
|
||||
// emitRemoteEvent bridges a kernel callback to the frontend through the async
|
||||
// emitter so a slow webview never blocks the kernel.
|
||||
func (a *App) emitRemoteEvent(name string, payload any) {
|
||||
if view, ok := payload.(RemoteTabStateView); ok && strings.HasPrefix(name, "remote-tab:") && strings.HasSuffix(name, ":state") {
|
||||
tabID := strings.TrimSuffix(strings.TrimPrefix(name, "remote-tab:"), ":state")
|
||||
a.remoteTabMu.Lock()
|
||||
if tab := a.remoteTabs[tabID]; tab != nil {
|
||||
tab.publication.hydrationRevision++
|
||||
view.HydrationRevision = tab.publication.hydrationRevision
|
||||
}
|
||||
a.remoteTabMu.Unlock()
|
||||
payload = view
|
||||
}
|
||||
if a.remoteEventHook != nil {
|
||||
a.remoteEventHook(name, payload)
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ listingAttempt:
|
||||
if routeUpdate != nil {
|
||||
a.emitRemoteEvent("remote-tab:updated", *routeUpdate)
|
||||
if routeReadyBarrier {
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", routeUpdate.ID), RemoteTabStateView{State: "ready"})
|
||||
a.publishRemoteTabState(routeUpdate.ID, RemoteTabStateView{State: "ready"})
|
||||
}
|
||||
}
|
||||
unlockRoute()
|
||||
|
||||
@@ -230,7 +230,7 @@ func (a *App) publishRemoteTabAttachedReady(tabID string, gen uint64) bool {
|
||||
tab.state = "ready"
|
||||
tab.err = ""
|
||||
a.remoteTabMu.Unlock()
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "ready"})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "ready"})
|
||||
tab.routeEventMu.Unlock()
|
||||
a.applyPendingRemoteTabOpenSelection(tabID)
|
||||
return true
|
||||
|
||||
@@ -289,7 +289,7 @@ func (a *App) adoptRemoteTabFrameCurrent(tabID string, gen uint64, sessionPath s
|
||||
// The frontend treats ready -> ready as a new surface generation. Emit it
|
||||
// before the triggering frame is forwarded so that frame is buffered until
|
||||
// the newly current session snapshot replaces the old transcript.
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "ready"})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "ready"})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (a *App) reconnectRemoteTabGeneration(tabID string, gen uint64) bool {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "reconnecting"})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "reconnecting"})
|
||||
return startRetry
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func (a *App) emitRemoteTabStateForGeneration(tabID string, gen uint64, state, e
|
||||
tab.state = state
|
||||
tab.err = errMsg
|
||||
a.remoteTabMu.Unlock()
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: state, Error: errMsg})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func (a *App) transitionRemoteTabStateLocked(tab *remoteTab, gen uint64, from, s
|
||||
tab.state = state
|
||||
tab.err = errMsg
|
||||
a.remoteTabMu.Unlock()
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: state, Error: errMsg})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func (a *App) emitRemoteTabStateLocked(tab *remoteTab, state, errMsg string) {
|
||||
tab.state = state
|
||||
tab.err = errMsg
|
||||
a.remoteTabMu.Unlock()
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: state, Error: errMsg})
|
||||
}
|
||||
|
||||
func (a *App) installRemoteTabAttachPump(ctx context.Context, tabID string, tab *remoteTab, client *http.Client, base, token, targetPath string, installRoute bool) (context.Context, uint64, uint64, error) {
|
||||
|
||||
@@ -280,7 +280,7 @@ func (a *App) suspendRemoteTabPumps(hostID, state, errText string) {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tab.id), RemoteTabStateView{State: state, Error: errText})
|
||||
a.publishRemoteTabState(tab.id, RemoteTabStateView{State: state, Error: errText})
|
||||
tab.routeEventMu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,7 @@ func (a *App) parkRemoteTabsForServer(hostID, workspace, state, errText string)
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tab.id), RemoteTabStateView{State: state, Error: errText})
|
||||
a.publishRemoteTabState(tab.id, RemoteTabStateView{State: state, Error: errText})
|
||||
tab.routeEventMu.Unlock()
|
||||
}
|
||||
return affected
|
||||
@@ -340,7 +340,7 @@ func (a *App) resumeRemoteTabs(hostID string) {
|
||||
}
|
||||
a.remoteTabMu.Unlock()
|
||||
for _, tabID := range rearmed {
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "reconnecting"})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "reconnecting"})
|
||||
}
|
||||
for _, tabID := range tabIDs {
|
||||
a.goRemoteTabSafe("remoteTabReattach", func() { a.reattachRemoteTab(tabID) })
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
// A rejection is one publication transaction: observers of its error must
|
||||
// already see the restored identity. The route fence also prevents a newer
|
||||
@@ -33,6 +30,6 @@ func (a *App) completeRemoteTabResumeFailure(tabID string, tab *remoteTab, clien
|
||||
a.emitRemoteEvent("remote-tab:updated", meta)
|
||||
a.saveTabsFromRemote()
|
||||
}
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "ready", Error: message})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "ready", Error: message})
|
||||
return false
|
||||
}
|
||||
@@ -287,7 +287,7 @@ func (a *App) recordRemoteTabSessionStatus(tabID string, client *http.Client, ge
|
||||
a.emitRemoteEvent("remote-tab:updated", after)
|
||||
}
|
||||
if readyBarrier || deferredBarrier {
|
||||
a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "ready"})
|
||||
a.publishRemoteTabState(tabID, RemoteTabStateView{State: "ready"})
|
||||
}
|
||||
if pathChanged {
|
||||
a.goRemoteTabSafe("remoteTabStatusTitle", func() { a.refreshRemoteTabTitle(tabID) })
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"reasonix/desktop/internal/workspacestate"
|
||||
@@ -42,7 +41,7 @@ func adoptedSourceRows(state workspacestate.State) map[string]bool {
|
||||
func sourceMappingHasPathAlias(mapping workspacestate.SourceMapping) bool {
|
||||
// A reviewed version is a separate conversation, not another owner of
|
||||
// the original path. Only its versioned SourceKey may alias that session.
|
||||
if strings.Contains(mapping.SourceKey, ":review:") {
|
||||
if historicalVersionSourceKey(mapping.SourceKey) {
|
||||
return false
|
||||
}
|
||||
if mapping.HeadID == "" {
|
||||
|
||||
@@ -32,6 +32,19 @@ func TestWorkspaceSourceAliasesKeepReviewedSessionsIndependent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoricalVersionSourceKeyRequiresSourceAndVersion(t *testing.T) {
|
||||
for key, want := range map[string]bool{
|
||||
"abc:review:v2": true,
|
||||
"abc": false,
|
||||
"abc:review:": false,
|
||||
":review:v2": false,
|
||||
} {
|
||||
if got := historicalVersionSourceKey(key); got != want {
|
||||
t.Errorf("historicalVersionSourceKey(%q) = %v, want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceSourceAliasesRejectAmbiguousPathOwnership(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "history.jsonl")
|
||||
state := workspacestate.State{SourceMappings: map[string]workspacestate.SourceMapping{
|
||||
|
||||
@@ -58,9 +58,9 @@ func integrityFileState(path string) (integrityCertificate, bool) {
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return integrityCertificate{}, false
|
||||
}
|
||||
// SQLite's header includes the change counter and schema cookie. Include
|
||||
// them in addition to metadata, so a checkpoint on coarse-mtime platforms
|
||||
// cannot reuse the proof for an earlier database state.
|
||||
// The header adds the schema cookie and, outside WAL mode, the change
|
||||
// counter. A WAL commit need not bump that counter, so file identity, size
|
||||
// and mtime stay the primary signal that a checkpoint changed the file.
|
||||
return integrityCertificate{main: main, header: string(header[:n])}, true
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user