返回 DeepSeek-Reasonix
remote_tab_reclaim.go
根目录 / desktop / remote_tab_reclaim.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9 )
10
11 func takeoverViewLocallyOwned(view SessionTakeoverView) bool {
12 return view.Mirrored || view.Holder == "external" || view.Holder == "other"
13 }
14
15 // reconcileRemoteTabReclaimOwnership keeps an ambiguous reclaim response from
16 // changing input authority. Only a successful, generation-fenced ownership
17 // probe may update the spectator pin.
18 func (a *App) reconcileRemoteTabReclaimOwnership(
19 tabID string,
20 client *http.Client,
21 base, expectedPath string,
22 stillCurrent func(*remoteTab) bool,
23 ) {
24 a.goRemoteTabSafe("reclaimOwnershipProbe", func() {
25 probeCtx, probeCancel := context.WithTimeout(context.Background(), 15*time.Second)
26 defer probeCancel()
27 view, err := takeoverOwnership(probeCtx, client, base, expectedPath)
28 if err != nil {
29 return
30 }
31 locallyOwned := takeoverViewLocallyOwned(view)
32 a.remoteTabMu.Lock()
33 current := a.remoteTabs[tabID]
34 if !stillCurrent(current) || current.session.takenOver == locallyOwned {
35 a.remoteTabMu.Unlock()
36 return
37 }
38 current.session.takenOver = locallyOwned
39 meta := remoteTabMetaLocked(current)
40 a.remoteTabMu.Unlock()
41 a.emitRemoteEvent("remote-tab:updated", meta)
42 })
43 }
44
45 // remoteTabOwnershipState fences a session's return from a local writer back
46 // to Serve. Both fields share that lifetime: a committing reclaim sets them
47 // and the re-hydrated surface clears them.
48 //
49 // reclaimRevision rejects /status payloads reserved before the reclaim
50 // completed — they still carry the pre-reclaim takenOver=true and would
51 // re-pin the spectator banner after ownership returned. readyBarrierPending
52 // defers the re-hydration barrier while a turn is in flight, because firing
53 // it mid-turn bumps the frontend connection generation, orphans the
54 // optimistic submission, and leaves a zombie "processing" indicator beside
55 // the rendered reply.
56 type remoteTabOwnershipState struct {
57 reclaimRevision uint64
58 readyBarrierPending bool
59 }
60
61 // remoteTabReclaimObservation is the tab state a reclaim fences against: it
62 // releases remoteTabMu for a long poll, then dereferences tab.
63 type remoteTabReclaimObservation struct {
64 tab *remoteTab
65 gen uint64
66 runtimeRevision uint64
67 selectionRevision uint64
68 }
69
70 // observeRemoteTabForReclaim snapshots the tab a reclaim fences against. The
71 // tab can close or reconnect between the command-target read and this
72 // snapshot; either is a disconnected tab, not a nil or retired binding.
73 func (a *App) observeRemoteTabForReclaim(tabID string, client *http.Client) (remoteTabReclaimObservation, error) {
74 a.remoteTabMu.Lock()
75 defer a.remoteTabMu.Unlock()
76 tab := a.remoteTabs[tabID]
77 if tab == nil || tab.client != client {
78 return remoteTabReclaimObservation{}, fmt.Errorf("remote tab %q is not connected", tabID)
79 }
80 return remoteTabReclaimObservation{
81 tab: tab, gen: tab.gen,
82 runtimeRevision: tab.runtime.revision, selectionRevision: tab.selectionRevision,
83 }, nil
84 }
85
86 // remoteSessionTakenOver reports whether a session-entry refusal means the
87 // session is owned by a local runtime on the serve host. The tab then
88 // attaches as a read-only spectator instead of dying with the 409. All three
89 // refusal shapes match: the explicit takeover wording (mirrored session), the
90 // plain lease wording ("in use by another Reasonix process" — the holder is a
91 // local window/CLI whose transcript the file-backed /history serves anyway,
92 // and whose lease /reclaim can take back), and the final-format writer
93 // wording ("session writer is owned by another runtime" — the identity's
94 // writer.lock lives with a local runtime).
95 func remoteSessionTakenOver(err error) bool {
96 if err == nil {
97 return false
98 }
99 msg := err.Error()
100 if strings.Contains(msg, "taken over by a local Reasonix") {
101 return true
102 }
103 if strings.Contains(msg, "writer is owned by another runtime") {
104 return true
105 }
106 return strings.Contains(msg, "in use by another Reasonix process")
107 }
108
108 lines GO