okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(cli): extract local daemon lifecycled9c1a13

main
10.3 KiB315 linesraw
1package daemon
2
3import (
4	"bufio"
5	"context"
6	"encoding/json"
7	"errors"
8	"io"
9	"net"
10	"net/http"
11	"strings"
12	"unicode/utf8"
13
14	"albedo/cli/internal/daemon/protocol"
15)
16
17type streamBatch struct {
18	Generation string            `json:"generation"`
19	Cursor     *int64            `json:"cursor"`
20	Events     []json.RawMessage `json:"events"`
21	Snapshot   json.RawMessage   `json:"snapshot"`
22}
23
24func (c *ChatClient) ResetStream() {
25	c.mu.Lock()
26	defer c.mu.Unlock()
27	c.progressCallIDs = nil
28	c.afterSeq = -1
29	c.afterGeneration = ""
30}
31func streamFailure(kind StreamFailureKind, cause error) error {
32	return &StreamError{Kind: kind, Cause: cause}
33}
34func classifyStreamFailure(err error) error {
35	if err == nil {
36		return nil
37	}
38	if _, ok := errors.AsType[*StreamError](err); ok {
39		return err
40	}
41	if api, ok := errors.AsType[*APIError](err); ok {
42		kind := StreamTerminal
43		if api.StatusCode == 408 || api.StatusCode == 429 || api.StatusCode >= 500 {
44			kind = StreamTransient
45		}
46		return streamFailure(kind, err)
47	}
48	if errors.Is(err, bufio.ErrTooLong) {
49		return streamFailure(StreamProtocol, err)
50	}
51	if _, ok := errors.AsType[net.Error](err); ok {
52		return streamFailure(StreamTransient, err)
53	}
54	if isConnectionError(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
55		return streamFailure(StreamTransient, err)
56	}
57	return streamFailure(StreamTerminal, err)
58}
59func (c *ChatClient) Stream(ctx context.Context, tail int, onEvent func(StreamEvent) error) error {
60	return c.StreamWithProgress(ctx, tail, onEvent, nil)
61}
62func (c *ChatClient) StreamWithProgress(ctx context.Context, tail int, onEvent func(StreamEvent) error, onBatchConsumed func()) error {
63	defer func() {
64		if ctx.Err() != nil {
65			c.ResetStream()
66		}
67	}()
68	c.mu.Lock()
69	after, generation := c.afterSeq, c.afterGeneration
70	c.mu.Unlock()
71	params := protocol.GetSessionParams{Tail: new(int64(min(200, max(0, tail))))}
72	if generation != "" {
73		params.AfterGeneration = &generation
74		params.AfterSeq = &after
75	}
76	err := scanEventStream(ctx, c.conn, operation{Name: "watch session", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
77		return protocol.NewGetSessionRequest(base, c.agentID, &params)
78	}, Policy: readRecovery}, streamLimits{requireSSE: true, lineBytes: 1048577, errorBytes: 65536}, func(scanner *bufio.Scanner) error { return c.readStream(ctx, scanner, onBatchConsumed, onEvent) })
79	return classifyStreamFailure(err)
80}
81
82// SSE framing is independent of network fragmentation and supports multiline data.
83func scanSSEFrames(ctx context.Context, scanner *bufio.Scanner, consume func([]byte) error) error {
84	var data strings.Builder
85	for scanner.Scan() {
86		if err := ctx.Err(); err != nil {
87			return err
88		}
89		line := scanner.Text()
90		if line == "" {
91			if data.Len() > 0 {
92				payload := strings.TrimSuffix(data.String(), "\n")
93				if !utf8.ValidString(payload) {
94					return streamFailure(StreamProtocol, errors.New("SSE data is not UTF-8"))
95				}
96				if err := consume([]byte(payload)); err != nil {
97					return err
98				}
99				data.Reset()
100			}
101			continue
102		}
103		if after, ok := strings.CutPrefix(line, "data:"); ok {
104			fragment := after
105			fragment = strings.TrimPrefix(fragment, " ")
106			if data.Len()+len(fragment)+1 > 1048577 {
107				return streamFailure(StreamProtocol, errors.New("SSE frame exceeds 1 MiB"))
108			}
109			data.WriteString(fragment)
110			data.WriteByte('\n')
111		}
112	}
113	if err := scanner.Err(); err != nil {
114		return err
115	}
116	if data.Len() != 0 {
117		return io.ErrUnexpectedEOF
118	}
119	return nil
120}
121func (c *ChatClient) readStream(ctx context.Context, scanner *bufio.Scanner, onBatchConsumed func(), onEvent func(StreamEvent) error) error {
122	return scanSSEFrames(ctx, scanner, func(payload []byte) error {
123		var batch streamBatch
124		if err := decodeRequired(payload, &batch, "generation", "cursor", "events"); err != nil {
125			return streamFailure(StreamProtocol, err)
126		}
127		if !validGeneration(batch.Generation) || batch.Cursor == nil || *batch.Cursor < 0 || batch.Events == nil || len(batch.Events) > 256 {
128			return streamFailure(StreamProtocol, fieldError("stream batch"))
129		}
130		var first sessionEventEnvelope
131		if len(batch.Events) > 0 {
132			if err := json.Unmarshal(batch.Events[0], &first); err != nil {
133				return streamFailure(StreamProtocol, err)
134			}
135		}
136		if first.Type == "failure" {
137			if len(batch.Events) != 1 || batch.Snapshot != nil {
138				return streamFailure(StreamProtocol, fieldError("failure batch"))
139			}
140			var reason protocol.SafeReason
141			if err := json.Unmarshal(first.Data, &reason); err != nil || reason.Code == "" {
142				return streamFailure(StreamProtocol, fieldError("failure reason"))
143			}
144			failure := &APIError{Code: reason.Code, Message: reason.Detail}
145			if failure.DaemonRestarting() {
146				// older daemons end streams this way while they shut down
147				return streamFailure(StreamTransient, failure)
148			}
149			return streamFailure(StreamTerminal, failure)
150		}
151		reset := first.Type == "reset"
152		c.mu.Lock()
153		previous, generation := c.afterSeq, c.afterGeneration
154		c.mu.Unlock()
155		sequence := previous
156		var events []wireChatEvent
157		var current []ToolProgress
158		var outputs []protocol.ActiveOutput
159		if reset {
160			snapshot, err := decodeSession(batch.Snapshot)
161			if err != nil {
162				return streamFailure(StreamProtocol, err)
163			}
164			if snapshot.ID != c.agentID || snapshot.wire.Cursor.Generation != batch.Generation {
165				return streamFailure(StreamProtocol, fieldError("snapshot identity"))
166			}
167			sequence = snapshot.wire.Cursor.Sequence
168			outputs = snapshot.wire.ActiveOutput
169			before := int64(0)
170			if len(snapshot.wire.History.Items) > 0 {
171				before = snapshot.wire.History.Items[0].Position
172			}
173			events = append(events, wireChatEvent{StreamEvent: StreamEvent{Type: EventReset, Before: before, More: snapshot.wire.History.Older != nil, Snapshot: &snapshot}})
174			hydrated, err := hydrateHistory(ctx, c.conn, c.agentID, snapshot.wire.History.Items)
175			if err != nil {
176				return streamFailure(StreamTransient, err)
177			}
178			history, err := historyEvents(hydrated)
179			if err != nil {
180				return streamFailure(StreamProtocol, err)
181			}
182			for _, event := range history {
183				events = append(events, wireChatEvent{StreamEvent: event})
184			}
185			events = append(events, wireChatEvent{StreamEvent: StreamEvent{Type: EventUsage, Usage: usageValue(snapshot.wire.Usage), Replayed: true}})
186			for _, progress := range snapshot.wire.CurrentProgress {
187				parsed, err := validateToolProgress(progress)
188				if err != nil || parsed == nil {
189					return streamFailure(StreamProtocol, fieldError("current progress"))
190				}
191				current = append(current, *parsed)
192				events = append(events, wireChatEvent{StreamEvent: StreamEvent{Type: EventToolProgress, Progress: parsed}})
193			}
194		} else if batch.Snapshot != nil || generation == "" || generation != batch.Generation {
195			return streamFailure(StreamProtocol, errors.New("initial or changed generation requires a snapshot reset"))
196		}
197		restoredEvents := len(events)
198		for index, raw := range batch.Events {
199			var envelope sessionEventEnvelope
200			if err := decodeRequired(raw, &envelope, "type", "data"); err != nil {
201				return streamFailure(StreamProtocol, err)
202			}
203			if reset && index == 0 {
204				if _, err := decodeChatEvent(raw); err != nil {
205					return streamFailure(StreamProtocol, err)
206				}
207				continue
208			}
209			if envelope.Type == "reset" || envelope.Type == "failure" || envelope.Sequence == nil || *envelope.Sequence != sequence+1 {
210				return streamFailure(StreamProtocol, errors.New("stream events are not contiguous"))
211			}
212			sequence = *envelope.Sequence
213			event, err := decodeChatEnvelope(envelope, len(raw), &eventContentReader{ctx: ctx, conn: c.conn, session: c.agentID})
214			if err != nil {
215				if api, ok := errors.AsType[*APIError](err); ok && api.Code == "history_publication_failed" {
216					c.ResetStream()
217					return streamFailure(StreamTransient, err)
218				}
219				if _, ok := errors.AsType[*StreamError](err); ok {
220					return err
221				}
222				return streamFailure(StreamProtocol, err)
223			}
224			if event != nil {
225				events = append(events, *event)
226			}
227		}
228		if sequence != *batch.Cursor {
229			return streamFailure(StreamProtocol, errors.New("batch cursor does not cover its events"))
230		}
231		active, err := c.nextProgressCallIDs(current, events, reset)
232		if err != nil {
233			return streamFailure(StreamProtocol, err)
234		}
235		stage, err := stageActiveOutput(ctx, c.conn, outputs)
236		if err != nil {
237			if api, ok := errors.AsType[*APIError](err); ok && (api.StatusCode == 404 || api.StatusCode == 410) {
238				c.ResetStream()
239				return streamFailure(StreamTransient, err)
240			}
241			return classifyStreamFailure(err)
242		}
243		prefixDelivered := !reset
244		for index, event := range events {
245			if !prefixDelivered && index == restoredEvents {
246				if err := stage.deliver(ctx, onEvent); err != nil {
247					stage.close()
248					return streamFailure(StreamTerminal, err)
249				}
250				prefixDelivered = true
251			}
252			if err := onEvent(event.StreamEvent); err != nil {
253				stage.close()
254				return streamFailure(StreamTerminal, err)
255			}
256		}
257		if !prefixDelivered {
258			if err := stage.deliver(ctx, onEvent); err != nil {
259				stage.close()
260				return streamFailure(StreamTerminal, err)
261			}
262		}
263		stage.close()
264		c.mu.Lock()
265		c.progressCallIDs = active
266		c.afterSeq = *batch.Cursor
267		c.afterGeneration = batch.Generation
268		c.mu.Unlock()
269		if onBatchConsumed != nil {
270			onBatchConsumed()
271		}
272		return nil
273	})
274}
275func (c *ChatClient) nextProgressCallIDs(snapshot []ToolProgress, events []wireChatEvent, reset bool) (map[string]struct{}, error) {
276	active := map[string]struct{}{}
277	if reset {
278		for _, progress := range snapshot {
279			if _, ok := active[progress.CallID]; ok {
280				return nil, errors.New("duplicate progress call")
281			}
282			active[progress.CallID] = struct{}{}
283		}
284	} else {
285		c.mu.Lock()
286		for id := range c.progressCallIDs {
287			active[id] = struct{}{}
288		}
289		c.mu.Unlock()
290	}
291	for _, event := range events {
292		if event.Replayed {
293			continue
294		}
295		switch event.Type {
296		case EventRetry, EventUser, EventMessage, EventError, EventInterrupted, EventTurnCompleted:
297			clear(active)
298		case EventToolProgress:
299			if event.Progress == nil {
300				clear(active)
301			} else {
302				active[event.Progress.CallID] = struct{}{}
303			}
304		case EventTool:
305			delete(active, event.ProgressCallID)
306		}
307		if len(active) > maxActiveToolProgress {
308			return nil, errors.New("too many active progress calls")
309		}
310	}
311	if len(active) > maxActiveToolProgress {
312		return nil, errors.New("too many active progress calls")
313	}
314	return active, nil
315}