Repository navigation
Expand file tree
/
Copy pathcallclass.go
More file actions
213 lines (203 loc) · 12 KB
/
Copy pathcallclass.go
File metadata and controls
213 lines (203 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package remote
// ── THE ROAD IS CLASSED, NOT FREE-FOR-ALL ───────────────────────────────────
//
// callClass is WHAT A METHOD OWES THE ONES AROUND IT, and it is THE ONE
// PREDICATE that decides where the method runs: [server.serve] asks it and
// nothing else in this package asks that question anywhere else. A list
// sprinkled through the switch in [server.invoke] would be a second answer
// waiting to disagree with this one, and one method in the wrong place there is
// a keystroke queued behind a getter — which is what this file was written for.
//
// ORDERED is anything that opens a stream or changes the conversation's shape.
// What it owes is an ORDER, twice over: a stream's first event may not overtake
// the result that names it ([server.release] is one slot for that reason), and
// a surface that sets a model and then submits means those two things in that
// order (server.go's file header).
//
// A GETTER is a read. The surface asks half of them from its update loop, so
// they may not queue behind anything (a listing that blocked a keystroke was
// the measured defect) and they may not wait longer than [callDeadline] (a
// terminal that has stopped repainting).
//
// AN ACT is a person's small write — answering a card, taking the keyboard,
// interrupting, saying they have started typing. It owes nothing to anything
// for the getter's reason said the other way round: a key is not queued behind
// a listing. It waits the same [callDeadline] a getter does, and the note below
// the classes says why the longer window it was given first had to come back
// out.
//
// WORK is a long piece of the engine's own labour that a surface asked for and
// nothing else waits on: [MethodCompact], which may ask the model for a summary
// and take a minute doing it. It ran on the ordered lane until 2026-09-28, and
// everything the person sent while it ran queued behind it and waited out its
// own deadline there. It owes no order for the reason below: the surface asks
// for it from a command goroutine of its own, so nothing it sends was ever
// ordered after it, and the conversation guards itself — a summary is spliced
// in only if the region it read is unchanged (internal/session's
// compact_summary.go), which is what an in-process surface already relied on.
//
// AND LETTING ANY OF THEM OVERTAKE AN ORDERED CALL COSTS NOTHING, which is
// the whole licence for this split and is worth stating plainly: EVERY CALL ON
// THIS WIRE IS A SYNCHRONOUS ROUND TRIP ([Client.callAnswered] waits for the
// result frame). A caller that makes two calls therefore has the first one's
// answer in hand before the second frame is written, so two calls whose order
// matters were already ordered by the caller and cannot be reordered here; two
// made from different goroutines were never ordered by anything, before this
// change or after it.
//
// A METHOD THIS FUNCTION HAS NOT HEARD OF IS ORDERED. A door that lands next
// year keeps its place in the queue until someone decides it is a getter or a
// small act, which is the same allow-list shape [watcherMay] uses.
//
// ── AND ORDERED NEVER MEANT "ON THE READER" ─────────────────────────────────
//
// That was the confusion this file carried until 2026-09-11, and it is why a
// keystroke could sit behind a send. Running the ordered bodies on the
// goroutine that reads the socket gave the order for free and took the READING
// away with it: [MethodSubmit] alone rebinds the client, re-reads the person's
// rules, reassembles the system prompt, journals the message and
// starts the naming errand before it returns, and for all of that the engine
// was not reading its own pipe. A person who pressed Enter and then Esc was
// queued behind their own send. [MethodCompact] was the extreme case — it does
// the summarizer's work itself — and it was described here as the price of
// being ordered, which it never was.
//
// SO THE READER READS, AND NOTHING ELSE. Every class runs its body off it; what
// a class chooses is WHICH goroutine ([callClass.road]). Ordered calls go to
// the connection's one [orderedLane] — one goroutine, arrival order, which is
// exactly the guarantee the reader was giving and is now given by the thing
// that actually needs it. Getters and small acts owe each other nothing, so
// each takes a goroutine of its own.
//
// AND THE ORDERED LANE IS THE ONLY OWNER OF [server.pending]. Every method that
// names a stream is ordered, so the one slot is written and read by one
// goroutine and needs no lock. A method that opened a stream from any other
// class would be racing it, which is what [TestEveryStreamOpenerIsOrdered]
// refuses.
type callClass int
const (
classOrdered callClass = iota
classGetter
classAct
classWork
)
// road is WHERE a class of call runs, and this is the one place a class is
// turned into a goroutine. [server.serve] switches on it and nothing else
// decides it, so a method's road is a property of its class rather than a
// branch at the site that happens to read the socket.
//
// NEITHER ROAD IS THE READER. That is the law of this file said as a type: the
// goroutine that reads the socket has no third option to be given.
type road int
const (
// inOrder is the connection's one ordered lane: off the reader, and in the
// order the frames arrived (orderedlane.go).
inOrder road = iota
// onItsOwn is a goroutine per call. A getter, a small act and a piece of
// work owe nothing to each other, so none of them owes a queue.
onItsOwn
)
func (c callClass) road() road {
if c == classOrdered {
return inOrder
}
return onItsOwn
}
// ── AND AN ACT DOES NOT GET A LONGER WINDOW THAN A GETTER ───────────────────
//
// It is the obvious second half of this fix and it was written, measured and
// TAKEN BACK OUT, so here is the measurement rather than the temptation.
//
// The argument for it is real: a getter that gives up costs one stale number
// and is asked again on the next frame, while an act that gives up costs a
// DECISION — the engine takes the answer and this window is told it did not.
// So an act was given three times [callDeadline], thirty seconds.
//
// WHAT THAT BUYS IS A FROZEN TERMINAL, because an act is asked from the update
// loop exactly as a getter is: internal/tui3's [app.answerQuestion] calls its
// door straight from Update rather than from a command, so the window is also
// the longest that terminal can sit without repainting after a key is pressed.
// Measured in a test run, eighteen copies of the ordinary road's own e2e run six
// at a time: with thirty seconds, two copies took 53 seconds where every other
// copy took 22, and both lost the receipt — the surface was not drawing. Twelve
// copies of the same commit's parent produced no copy over 33 seconds.
//
// So the window is [callDeadline] for everything, and what stops an act needing
// more is the road above rather than a bigger number: it is no longer queued
// behind anything. A deadline it does reach is now said honestly instead of as a
// dead connection ([Client.late]), and the receipt stamped before the door is
// what makes the engine's news close as yours (internal/tui3's
// [app.markQuestionSent]) — which is the pair that makes a short window safe.
// lateCallTail is what a deadline says on a connection that is still here,
// after [Client.where]. THE CONNECTION IS NOT GONE. The engine may be working
// on this call right now; what ran out is this end's patience. Naming the
// machine the same way [Client.gone] does keeps the two sentences one voice;
// an empty where is "the engine", which is the sentence the surface already
// quotes when a door never answered.
const lateCallTail = " did not answer in time"
func classify(method string) callClass {
switch method {
case MethodClientToolsSet, MethodClientToolAnswer:
return classAct
case methodFactoryStatus, MethodAppTasks, MethodAppTask, MethodAppProgramActions, MethodAppTaskRouting, MethodAppTaskRoutingOptions, MethodAppQuickTasks, MethodPing, MethodCheckWork,
MethodModel, MethodTitle, MethodUsage, MethodContextTokens, MethodSpendRail,
MethodTranscript, MethodEarlier, MethodRewindPoints, MethodPlanSpend,
MethodPlanTasks, MethodPlanTaskPage, MethodPlanTaskWork, MethodPlanRunSummary, MethodRefreshRunSummary,
MethodReasoningFor, MethodEffort, MethodResolvedEffort, MethodResolvedApproval,
MethodAttachedSkills, MethodSkillShelf, MethodSkillFolders, MethodSetupPlan,
MethodSessionsRecent, MethodHeldQuestions,
MethodPlacesWorld, MethodPlacesTask, MethodPlacesLedger, MethodPlacesSearch,
MethodMemorySelection, MethodMemorySnapshot, MethodMemoryChanged, MethodMemoryProvenance, MethodMemoryProject, MethodMemoryApplies,
MethodTaskRoom, MethodTaskPending, MethodTaskEffort,
MethodListDir, MethodStatPaths, MethodFetchFile,
// The wall's two model asks are reads of the naming role, asked off the
// update loop and bounded by the wall; queued behind a turn they would
// wait out the wall's patience and answer nobody.
MethodTeamsName, MethodTeamsPropose,
// The desktop app's task and process readings are drawn while a reply
// streams, so they are never queued behind it.
MethodProcessList, MethodProcessPage, MethodProcessRecord, MethodProcessActions, MethodRenderList, MethodRenderGet,
// The automations readings are the watcher's beat (wire_automations.go):
// a store read every two seconds that a turn in flight must never hold
// up, or a run's line would arrive when the reply did.
MethodAutomationsList, MethodAutomationsRuns, MethodAutomationsChanges,
MethodAutomationsCursor, MethodAutomationsActive, MethodAutomationsWindows:
return classGetter
case MethodTaskWorkspaceCancel, MethodClaimChatStart, MethodAppTaskRoutingAct, MethodAppTaskAction, MethodAppQuickTaskAction, MethodAppMoveToTask, MethodAppResumeAfterQuit, MethodAppResumeAfterClose, MethodAppRename, MethodProcessRerun, MethodProcessRerunAt, MethodProcessStopAt, MethodProcessStopAll, MethodRenderCancel, MethodQuestionResolve, MethodQuestionHold, MethodQuestionClock,
MethodConsent, MethodConsentRemember,
MethodHarness, MethodConnect, MethodConnectKey,
MethodNoteConnected,
MethodMemoryRemember, MethodMemoryMemories,
MethodMemoryRememberAlways, MethodMemoryAlways, MethodMemoryMove,
MethodTake, MethodAnswerLaneOffer, MethodInterrupt, MethodUnqueueFollowUp,
MethodPlanNote, MethodPlanPause, MethodPlanResume, MethodPlanCancel, MethodPlanAmend, MethodPlanPriority,
MethodTyping,
// A person's change to an automation is a keystroke on a list, and it
// is about the machine's store rather than the conversation's order: a
// pause pressed while a reply streams owes that reply nothing. The claim
// is the one store write a window makes on its own behalf, and it is as
// small as a key.
MethodAutomationsCreate, MethodAutomationsUpdate, MethodAutomationsSetStatus,
MethodAutomationsDelete, MethodAutomationsRunNow, MethodAutomationsStopRun,
MethodAutomationsClaim:
return classAct
case MethodTaskWorkspaceReview, MethodTaskWorkspaceApply, MethodOpenPlanTaskWork, MethodCompact, MethodTaskRunHere,
// A side ask is a model call of up to two minutes that writes nothing
// into the conversation, so it owes a turn no order and a turn owes it
// none (sideask.go).
MethodSideAsk, MethodSideAskStream,
// Reply suggestions are also independent model work. A draft writes
// no turn, so a person's send must not queue behind its response.
methodReplySuggestion,
// An errand is the same: detached model work that writes no turn.
methodErrand:
return classWork
default:
return classOrdered
}
}
// opensAStream reports whether calls of this class may name a stream, which is
// the same question as "may they write [server.pending]". Exactly one class
// may, one goroutine runs that class, and [server.dispatch] starts the pump for
// exactly that one — which is the whole of why the slot needs no lock.
func (c callClass) opensAStream() bool { return c == classOrdered }