返回 DeepSeek-Reasonix
inbox_target.go
根目录 / desktop / inbox_target.go
1 package main
2
3 import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/url"
8 "strings"
9
10 "reasonix/internal/sessioninbox"
11 )
12
13 // InboxTargetView fences follow-ups across both local replacement and remote
14 // selection changes. It is process-local and never written to the inbox ledger.
15 type InboxTargetView struct {
16 TabID string `json:"tabId"`
17 SessionPath string `json:"sessionPath"`
18 Generation uint64 `json:"generation"`
19 Selection uint64 `json:"selection"`
20 Remote bool `json:"remote"`
21 HostID string `json:"hostId,omitempty"`
22 Workspace string `json:"workspace,omitempty"`
23 }
24
25 func (a *App) CaptureInboxTarget(tabID, expectedPath string) (InboxTargetView, error) {
26 a.runtimeAdmissionMu.RLock()
27 defer a.runtimeAdmissionMu.RUnlock()
28 return a.captureInboxTarget(tabID, expectedPath)
29 }
30
31 func (a *App) captureInboxTarget(tabID, expectedPath string) (InboxTargetView, error) {
32 a.remoteTabMu.Lock()
33 remote := a.remoteTabs[tabID]
34 if remote != nil {
35 defer a.remoteTabMu.Unlock()
36 if remote.state != "ready" || remote.client == nil || remote.routing.rehydratingPath != "" || remote.routing.currentPath == "" || remote.routing.currentPath != expectedPath {
37 return InboxTargetView{}, fmt.Errorf("inbox target changed")
38 }
39 return InboxTargetView{TabID: tabID, SessionPath: expectedPath, Generation: remote.gen, Selection: remote.selectionRevision,
40 Remote: true, HostID: remote.ref.HostID, Workspace: remote.ref.Workspace}, nil
41 }
42 a.remoteTabMu.Unlock()
43 a.mu.RLock()
44 defer a.mu.RUnlock()
45 for _, tab := range a.tabs {
46 if tab.ID == tabID && tab.Ctrl != nil && tab.SessionPath != "" && tab.SessionPath == expectedPath {
47 return InboxTargetView{TabID: tabID, SessionPath: expectedPath, Generation: tab.SessionGeneration}, nil
48 }
49 }
50 return InboxTargetView{}, fmt.Errorf("inbox target changed")
51 }
52
53 func (a *App) remoteInboxTarget(target InboxTargetView) (*http.Client, string, error) {
54 a.remoteTabMu.Lock()
55 defer a.remoteTabMu.Unlock()
56 tab := a.remoteTabs[target.TabID]
57 if tab == nil || tab.client == nil || tab.state != "ready" || tab.gen != target.Generation || tab.selectionRevision != target.Selection || tab.routing.currentPath != target.SessionPath || tab.routing.rehydratingPath != "" || (target.HostID != "" && (tab.ref.HostID != target.HostID || tab.ref.Workspace != target.Workspace)) {
58 return nil, "", fmt.Errorf("inbox target changed")
59 }
60 return tab.client, tab.base, nil
61 }
62
63 // EnqueueInboxFollowupForTarget is additive; older bindings retain their APIs.
64 func (a *App) EnqueueInboxFollowupForTarget(target InboxTargetView, display, submit string, invocations []InvocationRequest, key string) (InboxReceiptView, error) {
65 if strings.TrimSpace(key) == "" {
66 return InboxReceiptView{}, inboxNotSubmitted(fmt.Errorf("idempotency key required"))
67 }
68 if target.Remote {
69 client, base, err := a.remoteInboxTarget(target)
70 if err != nil {
71 return InboxReceiptView{}, inboxNotSubmitted(err)
72 }
73 return a.enqueueRemoteFollowupAt(client, base, target.SessionPath, display, submit, invocations, key)
74 }
75 a.runtimeAdmissionMu.RLock()
76 defer a.runtimeAdmissionMu.RUnlock()
77 current, err := a.captureInboxTarget(target.TabID, target.SessionPath)
78 if err != nil || current != target {
79 return InboxReceiptView{}, inboxNotSubmitted(fmt.Errorf("inbox target changed"))
80 }
81 ctrl, err := a.inboxCtrl(target.TabID)
82 if err != nil {
83 return InboxReceiptView{}, inboxNotSubmitted(err)
84 }
85 return a.enqueueInboxWithController(target.TabID, ctrl, sessioninbox.IntentFollowup, display, submit, invocations, key, false, "", target.SessionPath)
86 }
87
88 func inboxNotSubmitted(err error) error {
89 return &inboxCodedError{code: "inbox_not_submitted", cause: err}
90 }
91
92 // LookupInboxFollowupForTarget never creates an item, including on a missing
93 // or expired receipt. Position zero identifies an already removed item.
94 func (a *App) LookupInboxFollowupForTarget(target InboxTargetView, key string) (InboxReceiptView, error) {
95 if target.Remote {
96 current, err := a.remoteReceiptTarget(target)
97 if err != nil {
98 return InboxReceiptView{}, err
99 }
100 target = current
101 client, base, err := a.remoteInboxTarget(target)
102 if err != nil {
103 return InboxReceiptView{}, err
104 }
105 ctx, cancel := commandContext(a)
106 defer cancel()
107 data, err := serveGet(ctx, client, serveURL(base, "/inbox/receipt?key="+url.QueryEscape(key)+"&session="+url.QueryEscape(target.SessionPath)))
108 if err != nil {
109 return InboxReceiptView{}, err
110 }
111 var receipt InboxReceiptView
112 if err := json.Unmarshal(data, &receipt); err != nil {
113 return receipt, err
114 }
115 currentClient, currentBase, err := a.remoteInboxTarget(target)
116 if err != nil || currentClient != client || currentBase != base {
117 return InboxReceiptView{}, fmt.Errorf("inbox receipt route changed during read")
118 }
119 return receipt, nil
120 }
121 a.runtimeAdmissionMu.RLock()
122 defer a.runtimeAdmissionMu.RUnlock()
123 owner, err := a.localReceiptTarget(target.SessionPath)
124 if err != nil {
125 return InboxReceiptView{}, err
126 }
127 reader, ok := owner.ctrl.(interface {
128 LookupInboxReceiptForSession(string, string) (sessioninbox.InboxReceipt, bool, error)
129 })
130 if !ok {
131 return InboxReceiptView{}, fmt.Errorf("inbox receipt lookup unavailable")
132 }
133 receipt, found, err := reader.LookupInboxReceiptForSession(owner.path, key)
134 if err != nil {
135 return InboxReceiptView{}, err
136 }
137 if !found {
138 return InboxReceiptView{}, fmt.Errorf("inbox receipt unconfirmed")
139 }
140 if !a.localReceiptOwnerCurrent(owner) {
141 return InboxReceiptView{}, fmt.Errorf("inbox receipt owner changed during read")
142 }
143 return InboxReceiptView{ItemID: receipt.ItemID, Disposition: string(receipt.Disposition), Position: receipt.Position, Paused: receipt.Paused, Idempotent: receipt.Idempotent}, nil
144 }
145
145 lines GO