Repository navigation
Expand file tree
/
Copy pathheld.go
More file actions
284 lines (264 loc) · 11.4 KB
/
Copy pathheld.go
File metadata and controls
284 lines (264 loc) · 11.4 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package remote
// held.go is the WAITING ROOM: the questions a conversation raised while nobody
// was attached to it, kept so that the next surface to arrive is handed them
// instead of never hearing them at all.
//
// It exists because version 2 separated a conversation's life from a
// connection's. A card is a question the turn STOPS on — consent.go blocks the
// call, an automation card waits for an answer that never times out, a harness
// offer holds the turn before its first request — so a card raised into an
// empty room used to be a turn that sat there until something else killed it.
// That single fact is what made half the --host gap list honest: a lane whose
// answer could not arrive was better left off the belt entirely than present
// and hanging. With a persistent engine the room is no longer empty, it is
// merely unattended, and the difference is this file.
//
// WHAT IS HELD IS EVERY QUESTION THAT ARRIVES ON A TURN'S STREAM AND CAN BE
// ANSWERED OVER THIS WIRE. The three kinds below are three of the resolve-doors
// [WrappedAgent] carries. The harness lane's two cards — a design, a subharness
// intake — are deliberately absent: they never travel a stream, and the session
// replays whichever of them is still standing onto every new subscription
// (standinglane.go), so remembering them here would hand one question over
// twice.
import (
"time"
"github.com/Agent-Field/codeaf/internal/session"
)
// The three kinds a [HeldQuestion] can carry, which are the three resolve-doors
// this wire has. They are spelled once here so the engine and the surface
// cannot disagree about a string; wire.go's HeldQuestion.Kind documents why an
// unknown one is skipped rather than refused.
const (
HeldConsent = "consent"
HeldHarness = "harness"
HeldConnect = "connect"
)
// heldSet is one session's outstanding questions, oldest first.
//
// EVERY CARD IS TRACKED, AND WAITING IS A FACT ABOUT A SURFACE RATHER THAN
// ABOUT THE ROOM. A card is waiting for the window in front of you when that
// window has never been sent it: the surfaces the frame actually went to are
// remembered on the card, and every other surface — including one that dialled
// a second ago — is owed it. Handing it to the surface that already drew it
// would draw the same question twice, and that is the only case being excluded.
//
// THIS IS DELIBERATELY NOT "IS ANYBODY ATTACHED". A terminal that was killed is
// still in the room until its socket reports the end, which is a scheduling
// delay away and not a fact anybody can wait for; a welcome that asked whether
// the room was empty would tell the window that just replaced that terminal
// there was nothing to answer. Asking what THIS surface has seen is a question
// the answer to which cannot be late.
//
// It is NOT independently locked. Every door below is called with the owning
// [Session]'s mutex held, because every one of them is part of a larger
// decision that mutex already guards — "record this event and say who is
// watching", "let this surface in and tell it what is outstanding". A second
// lock here would only be a second order to get wrong.
type heldSet struct {
order []heldKey
items map[heldKey]*heldItem
}
// heldKey is which question, across the two shapes of id this wire carries: the
// three lanes that mint a uint64 and the connect lane, whose id is a string
// (session.Event's ConnectID states why the two are different fields).
type heldKey struct {
kind string
id uint64
text string
}
type heldItem struct {
question HeldQuestion
// seen is the arrival number of every surface this card's frame was sent
// to ([server.arrived], unique per attach). A surface not named here has
// never drawn this question and is owed it.
seen map[uint64]struct{}
}
func newHeldSet() *heldSet {
return &heldSet{items: map[heldKey]*heldItem{}}
}
// raise records one event, if that event is a question. It is called for every
// event on every stream, so the cheap answer — this is a text delta — has to be
// the first one.
//
// `drawn` is the arrival number of every surface this event's frame is being
// sent to, which is the whole of the difference between a card somebody is
// looking at and a card nobody is.
func (h *heldSet) raise(event EventWire, stream uint64, drawn []uint64) {
key, ok := heldKeyOf(event.Event)
if !ok {
return
}
if existing, found := h.items[key]; found {
// A question asked twice is one question. The card can legitimately be
// re-emitted — a session swap replays nothing, but a lane that retries
// its own offer would — and each re-emission only adds to the list of
// surfaces that have drawn it.
for _, arrived := range drawn {
existing.seen[arrived] = struct{}{}
}
return
}
seen := make(map[uint64]struct{}, len(drawn))
for _, arrived := range drawn {
seen[arrived] = struct{}{}
}
h.order = append(h.order, key)
h.items[key] = &heldItem{
question: HeldQuestion{
Kind: key.kind,
Event: event,
Stream: stream,
Since: time.Now(),
},
seen: seen,
}
}
// answered drops one card. It is keyed rather than searched so that dropping a
// consent card cannot take down a harness offer that happens to share an id —
// two lanes, two counters (session.Event's ID field states that law).
//
// ITS ONLY CALLER IS [heldSet.keepOnly], and that is the point: a card leaves
// this room because the engine no longer has its question open, never because
// some door on the wire remembered to say so.
func (h *heldSet) answered(kind string, id uint64, text string) {
key := heldKey{kind: kind, id: id, text: text}
if _, found := h.items[key]; !found {
return
}
delete(h.items, key)
for index, held := range h.order {
if held == key {
h.order = append(h.order[:index], h.order[index+1:]...)
break
}
}
}
// keepOnly drops every card the engine no longer has a question open for, and
// it is THE ONLY THING THAT EVER EMPTIES THIS ROOM.
//
// A DOOR-KEYED CLEAR MISSES EVERY NEW DOOR, which is the defect class this
// replaced. Each resolve-door on the wire used to take its own card down — a
// line in MethodConsent, one in MethodStandingResolve, one in MethodConnectKey —
// so the day the surface started answering every lane through the ONE door
// [MethodQuestionResolve] (the questions wave), nothing dropped anything. The
// owner answered a standing proposal, the engine recorded it once, and the same
// card came back on every attach: a tab switch, a conversation switch, a second
// window — each a new arrival number, each handed the answered card again by
// [heldSet.waitingFor], with [heldSet.outstanding] still counting it so the
// conversation read `waiting · your call` and home counted it in `want you`
// A displaced window must release its held input.
//
// So the room is reconciled against the QUESTION'S OWN LIFE instead
// ([session.Agent.OpenQuestions] is the whole of it — raised through one door,
// answered or withdrawn through one door), and there is nothing left for a new
// resolve-door to forget to call. It also subsumes what settleConnect did by
// hand: a connect ask whose five-minute wait ran out is a question the engine
// no longer has open, and so is one that was answered.
//
// IT IS NOT A PURE MIRROR OF THAT LIST and cannot become one. What a surface is
// handed on arrival is the lane's own FRAME — the EventConsentRequest, the
// EventConnectAsk the card is drawn from — which the question object does
// not carry. This room keeps the frames; the engine says which of them still
// stand.
func (h *heldSet) keepOnly(open map[heldKey]bool) {
for _, key := range append([]heldKey(nil), h.order...) {
if !open[key] {
h.answered(key.kind, key.id, key.text)
}
}
}
// waitingFor is what a welcome carries and what Held.Questions answers: the
// questions this surface has not been sent, oldest first, because the oldest is
// the one that has been holding a turn the longest.
func (h *heldSet) waitingFor(arrived uint64) []HeldQuestion {
var out []HeldQuestion
for _, key := range h.order {
item := h.items[key]
if item == nil {
continue
}
if _, drawn := item.seen[arrived]; drawn {
continue
}
out = append(out, item.question)
}
return out
}
// outstanding is how many questions are unanswered, whoever is or is not
// looking at them. It is what the idle policy reads: a card nobody has answered
// is a turn that has stopped, and the conversation holding it is not idle even
// while somebody has it on screen.
func (h *heldSet) outstanding() int { return len(h.items) }
// forget drops everything, which is what a session swap does to the questions
// of the conversation it replaced. A card belongs to the agent that raised it,
// and the surface has already been handed a fresh welcome that forgot
// everything before it — the same law [server.pump] states about a late event.
func (h *heldSet) forget() {
h.order = nil
h.items = map[heldKey]*heldItem{}
}
// heldKeyOfQuestion is the same key read off a [session.Question] rather than
// off the event that announced it, which is what lets [heldSet.keepOnly]
// compare this room against the engine's own list of what is still open.
//
// THE TWO SPELLINGS MUST AGREE, and they are written beside each other for that
// reason: a kind read one way here and another way there would prune a card
// that is still standing — a question a person can see and no longer answer.
func heldKeyOfQuestion(q session.Question) (heldKey, bool) {
switch q.Kind {
case session.QuestionConsent:
if q.ID == 0 {
return heldKey{}, false
}
return heldKey{kind: HeldConsent, id: q.ID}, true
case session.QuestionHarness:
if q.ID == 0 {
return heldKey{}, false
}
return heldKey{kind: HeldHarness, id: q.ID}, true
case session.QuestionConnect:
if q.Ref == "" {
return heldKey{}, false
}
return heldKey{kind: HeldConnect, text: q.Ref}, true
}
return heldKey{}, false
}
// heldKeyOf says which resolve-door answers an event, and whether any does.
//
// THE KINDS ARE READ FROM THE EVENT AND NEVER GUESSED. Each one names the field
// its answer travels back on — [WrappedAgent.ResolveConsent] takes Event.ID,
// ResolveConnect takes the string in ConnectID — so a card whose id could not be read is not held,
// because holding it would promise an answer that lands nowhere.
func heldKeyOf(event session.Event) (heldKey, bool) {
switch event.Kind {
case session.EventConsentRequest:
if event.ID == 0 {
return heldKey{}, false
}
return heldKey{kind: HeldConsent, id: event.ID}, true
case session.EventHarnessOffer, session.EventHarnessDesignDone:
// One door answers both: an offer to RUN a harness and a finished page
// asking to be SAVED both go back through ResolveHarness, which is why
// they share a kind here (internal/session's harness.go and its
// EventHarnessDesignDone both name that method).
//
// ONLY THE OFFER ARRIVES HERE. A design card is sent through
// emitHarness, which reaches the harness lane's watchers and no turn's
// stream, so it never passes this classifier — and it does not need to:
// internal/session replays a card that is still standing onto every new
// subscription, so the window that comes back is handed it there
// (standinglane.go). Holding it here as well would draw one question
// twice.
if event.ID == 0 {
return heldKey{}, false
}
return heldKey{kind: HeldHarness, id: event.ID}, true
case session.EventConnectAsk:
if event.ConnectID == "" {
return heldKey{}, false
}
return heldKey{kind: HeldConnect, text: event.ConnectID}, true
}
return heldKey{}, false
}