返回 DeepSeek-Reasonix
remote_submit.go
根目录 / desktop / remote_submit.go
1 package main
2
3 import (
4 cryptorand "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "net/http"
9 "reasonix/internal/control"
10 "reasonix/internal/serve"
11 "reasonix/internal/sessioninbox"
12 "strings"
13 "time"
14 )
15
16 func (a *App) SubmitRemoteTab(tabID, text string) error {
17 return a.SubmitRemoteTabWithSubmission(tabID, text, "")
18 }
19
20 // remoteBusySubmitError reports whether the serve refused a foreground submit
21 // because a turn is still active; its answer directs clients to the durable
22 // inbox follow-up endpoint.
23 func remoteBusySubmitError(err error) bool {
24 var statusErr *serveHTTPStatusError
25 return errors.As(err, &statusErr) && statusErr.statusCode == http.StatusConflict &&
26 strings.Contains(statusErr.message, serve.SubmitBusyMessage)
27 }
28
29 // remoteSessionChangedSubmitError reports the serve's expected-session fence:
30 // the submit raced a session switch or a session re-adoption. Nothing started,
31 // and the same submit succeeds once the route settles.
32 func remoteSessionChangedSubmitError(err error) bool {
33 var statusErr *serveHTTPStatusError
34 return errors.As(err, &statusErr) && statusErr.statusCode == http.StatusConflict &&
35 strings.Contains(statusErr.message, serve.SubmitSessionChangedMessage)
36 }
37
38 // remoteSubmitRetryDelaysMs paces a submit that raced a session change. The
39 // window is a route settle (resume commit plus buffered-frame drain), so a few
40 // short retries cover it; anything longer is reported as a transient outcome
41 // the composer holds and retries with the rest of the durable queue.
42 var remoteSubmitRetryDelaysMs = []int{120, 250, 500}
43
44 // submitWithRouteRetry posts one foreground submit, re-resolving the route and
45 // retrying while the serve answers that the active session changed. A response
46 // that is not that fence (2xx, busy, or any other refusal) returns immediately.
47 func (a *App) submitWithRouteRetry(tabID string, attempt func(client *http.Client, base, expectedPath string) error) error {
48 var lastErr error
49 for round := 0; ; round++ {
50 client, base, expectedPath, err := a.remoteTabCommandTarget(tabID)
51 if err != nil {
52 return err
53 }
54 lastErr = attempt(client, base, expectedPath)
55 if lastErr == nil || !remoteSessionChangedSubmitError(lastErr) || round >= len(remoteSubmitRetryDelaysMs) {
56 break
57 }
58 time.Sleep(time.Duration(remoteSubmitRetryDelaysMs[round]) * time.Millisecond)
59 }
60 if lastErr != nil && remoteSessionChangedSubmitError(lastErr) {
61 // The route did not settle in the retry window: report a transient
62 // outcome so the composer holds the message visibly instead of showing
63 // a failure the user cannot act on.
64 return inboxTargetTransient(lastErr)
65 }
66 return lastErr
67 }
68
69 // queuedFollowupError reports a submit that could not start a turn because the
70 // previous one is still running: the message is durably queued as the follow-up
71 // the serve asks for. The receipt rides RPCErrorData so the renderer can show
72 // the visible queue entry immediately instead of a failure.
73 type queuedFollowupError struct {
74 receipt InboxReceiptView
75 }
76
77 func (e *queuedFollowupError) Error() string { return "reasonix_error:queued_followup" }
78
79 func (e *queuedFollowupError) RPCErrorData() map[string]any {
80 return map[string]any{"queuedFollowup": e.receipt}
81 }
82
83 // queueBusyFollowup converts a busy-window submit into the durable inbox
84 // follow-up the serve asks for, so a message typed while the previous turn is
85 // finishing is queued for the next turn instead of surfacing a conflict. The
86 // submission id doubles as the inbox idempotency key, so a retried submit can
87 // never double-queue the same message.
88 func (a *App) queueBusyFollowup(tabID, text, submissionID string) (*queuedFollowupError, error) {
89 idempotency := strings.TrimSpace(submissionID)
90 if idempotency == "" {
91 raw := make([]byte, 12)
92 if _, err := cryptorand.Read(raw); err != nil {
93 return nil, err
94 }
95 idempotency = "submit-busy-" + hex.EncodeToString(raw)
96 }
97 receipt, err := a.enqueueInbox(tabID, sessioninbox.IntentFollowup, text, text, nil, idempotency, false)
98 if err != nil {
99 return nil, err
100 }
101 // Remote inbox enqueues bypass the local change notification; tell the
102 // renderer so the queue strip reconciles this item.
103 a.emitInboxChanged(tabID)
104 return &queuedFollowupError{receipt: receipt}, nil
105 }
106
107 func (a *App) SubmitRemoteTabWithSubmission(tabID, text, submissionID string) error {
108 // Report the connection state before capability negotiation so a tab that
109 // has not finished bootstrap is never misdiagnosed as a legacy Serve.
110 if _, _, _, err := a.remoteTabCommandTarget(tabID); err != nil {
111 return err
112 }
113 if err := a.requireRemoteExecutionProtocol(tabID); err != nil {
114 return err
115 }
116 if err := a.requireRemotePermissionPresets(tabID); err != nil {
117 return err
118 }
119 if a.remoteModelApplicationReady(tabID) {
120 return a.SubmitRemoteTabWithModelApplication(tabID, text, submissionID, control.ModelApplicationChoice{Mode: "latest"})
121 }
122 for {
123 revision, admittedGen, err := a.ensureRemoteModelSettings(tabID)
124 if err != nil {
125 return &submissionNotAcceptedError{cause: err}
126 }
127 if !a.remoteTabAdmissionCurrent(tabID, admittedGen) {
128 continue
129 }
130 input := map[string]string{"input": text}
131 if submissionID != "" {
132 input["submissionId"] = submissionID
133 }
134 body, _ := json.Marshal(input)
135 err = a.submitWithRouteRetry(tabID, func(client *http.Client, base, expectedPath string) error {
136 ctx, cancel := commandContext(a)
137 defer cancel()
138 return servePostForSession(ctx, client, serveURL(base, "/submit"), body, expectedPath, revision)
139 })
140 if err != nil && remoteBusySubmitError(err) {
141 // The turn is finishing; queue the message as a visible durable
142 // follow-up and report it as queued rather than failed.
143 if queued, queueErr := a.queueBusyFollowup(tabID, text, submissionID); queueErr == nil {
144 return queued
145 }
146 }
147 return err
148 }
149 }
150
150 lines GO