okami.mom/albedo

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

Aly Raffauffix(stream): restore unfinished responses on session attachmenta229862

main
5.8 KiB188 linesraw
1package daemon
2
3import (
4	"bufio"
5	"context"
6	"encoding/json"
7	"errors"
8	"io"
9	"net/url"
10	"os"
11	"strings"
12	"unicode/utf8"
13
14	"albedo/cli/internal/daemon/protocol"
15)
16
17func validateActiveOutput(session string, outputs []protocol.ActiveOutput) error {
18	if outputs == nil || len(outputs) > 256 {
19		return fieldError("active output")
20	}
21	seen := map[string]bool{}
22	var total int64
23	inlineBytes := 0
24	for _, output := range outputs {
25		if output.MessageID == "" || output.RunID == "" || seen[output.MessageID] || output.Bytes < 0 || (output.Kind != "text" && output.Kind != "thinking") || (output.Text == nil) == (output.Reference == nil) || output.ElapsedMs != nil && *output.ElapsedMs < 0 {
26			return fieldError("active output descriptor")
27		}
28		seen[output.MessageID] = true
29		total += output.Bytes
30		if output.Bytes > 64*1024*1024 || total > 64*1024*1024 {
31			return fieldError("active output size")
32		}
33		if output.Text != nil {
34			encoded, _ := json.Marshal(*output.Text)
35			inlineBytes += len(encoded)
36			if inlineBytes > 64*1024 {
37				return fieldError("active output inline size")
38			}
39			if !utf8.ValidString(*output.Text) || int64(len(*output.Text)) != output.Bytes {
40				return fieldError("active output text")
41			}
42			continue
43		}
44		reference := output.Reference
45		parsed, err := url.Parse(reference.URL)
46		prefix := "/sessions/" + url.PathEscape(session) + "/active-output/"
47		if err != nil || parsed.IsAbs() || parsed.Host != "" || parsed.Fragment != "" || !strings.HasPrefix(parsed.Path, prefix) || strings.Contains(strings.TrimPrefix(parsed.Path, prefix), "/") || strings.TrimPrefix(parsed.Path, prefix) == "" || parsed.Query().Get("snapshot") == "" || reference.Field != output.Kind || reference.Bytes != output.Bytes {
48			return fieldError("active output reference")
49		}
50	}
51	return nil
52}
53
54type stagedActiveOutput struct {
55	output protocol.ActiveOutput
56	offset int64
57}
58type activeOutputStage struct {
59	file    *os.File
60	outputs []stagedActiveOutput
61}
62
63func (stage *activeOutputStage) close() {
64	if stage != nil && stage.file != nil {
65		name := stage.file.Name()
66		_ = stage.file.Close()
67		_ = os.Remove(name)
68	}
69}
70
71// Validate and stage complete prefixes before replacing any visible state. Large
72// prefixes stay on disk rather than accumulating in the pending event batch.
73func stageActiveOutput(ctx context.Context, conn *Connection, outputs []protocol.ActiveOutput) (*activeOutputStage, error) {
74	stage := &activeOutputStage{}
75	if len(outputs) == 0 {
76		return stage, nil
77	}
78	offset := int64(0)
79	for _, output := range outputs {
80		stage.outputs = append(stage.outputs, stagedActiveOutput{output: output, offset: offset})
81		if output.Reference == nil {
82			continue
83		}
84		if stage.file == nil {
85			file, err := os.CreateTemp("", "albedo-active-output-*")
86			if err != nil {
87				return nil, err
88			}
89			stage.file = file
90		}
91		if err := stage.readReference(ctx, conn, output); err != nil {
92			stage.close()
93			return nil, err
94		}
95		offset += output.Bytes
96	}
97	return stage, nil
98}
99func (stage *activeOutputStage) readReference(ctx context.Context, conn *Connection, output protocol.ActiveOutput) error {
100	reference := output.Reference
101	route, _ := url.Parse(reference.URL)
102	contentID := route.Path[strings.LastIndex(route.Path, "/")+1:]
103	offset := int64(0)
104	complete := false
105	return walkPages(func(next *string) (protocol.EntryContentPage, *string, error) {
106		query := route.Query()
107		if next != nil {
108			query.Set("next", *next)
109		}
110		route.RawQuery = query.Encode()
111		var page protocol.EntryContentPage
112		err := executeRead(ctx, conn, operation{Name: "read active output", Path: route.String(), Policy: readRecovery}, func(data []byte) error { return decodeRequired(data, &page) })
113		return page, page.Next, err
114	}, func(page protocol.EntryContentPage) error {
115		if page.EntryID != contentID || page.Parts == nil || len(page.Parts) == 0 || complete {
116			return fieldError("active output content identity")
117		}
118		for _, part := range page.Parts {
119			if part.Field != output.Kind || part.Encoding != "utf8" || part.OffsetBytes != offset || !utf8.ValidString(part.Text) || complete || part.Text == "" && !part.Complete {
120				return fieldError("active output content offset")
121			}
122			offset += int64(len(part.Text))
123			if offset > output.Bytes {
124				return fieldError("active output content cutoff")
125			}
126			if _, err := io.WriteString(stage.file, part.Text); err != nil {
127				return err
128			}
129			complete = part.Complete
130		}
131		if page.Next == nil && (!complete || offset != output.Bytes) || page.Next != nil && complete {
132			return fieldError("active output content completeness")
133		}
134		return nil
135	}, "repeated active output page")
136}
137func (stage *activeOutputStage) deliver(ctx context.Context, onEvent func(StreamEvent) error) error {
138	if stage == nil {
139		return nil
140	}
141	for _, item := range stage.outputs {
142		var input io.Reader
143		if item.output.Text != nil {
144			input = strings.NewReader(*item.output.Text)
145		} else {
146			input = io.NewSectionReader(stage.file, item.offset, item.output.Bytes)
147		}
148		reader := bufio.NewReader(input)
149		var chunk strings.Builder
150		emit := func(final bool) error {
151			event := StreamEvent{Type: EventType(item.output.Kind), Text: chunk.String(), MessageID: item.output.MessageID, TurnID: item.output.RunID}
152			if final && item.output.ElapsedMs != nil {
153				event.ElapsedMs = *item.output.ElapsedMs
154				event.ElapsedObserved = true
155			}
156			if err := onEvent(event); err != nil {
157				return err
158			}
159			chunk.Reset()
160			return nil
161		}
162		for {
163			if err := ctx.Err(); err != nil {
164				return err
165			}
166			char, size, err := reader.ReadRune()
167			if errors.Is(err, io.EOF) {
168				break
169			}
170			if err != nil {
171				return err
172			}
173			if char == utf8.RuneError && size == 1 {
174				return fieldError("active output UTF-8")
175			}
176			if chunk.Len()+size > 64*1024 {
177				if err := emit(false); err != nil {
178					return err
179				}
180			}
181			chunk.WriteRune(char)
182		}
183		if err := emit(true); err != nil {
184			return err
185		}
186	}
187	return nil
188}