Stream each task's transcript over its own SSE; keep session note order

This commit is contained in:
Ian Gulliver
2026-10-14 23:53:17 -07:00
parent eabfc10c8d
commit 2b4d92e7d5
18 changed files with 304 additions and 191 deletions
+3
View File
@@ -502,15 +502,18 @@ paths:
description: >
Server-sent events. Each event's id is its seq; data is the Event as JSON.
Events after Last-Event-ID (or the after query parameter) are replayed before live events.
With task_id, only that task's events are sent.
parameters:
- { name: Last-Event-ID, in: header, required: false, schema: { type: integer, format: int64 } }
- { name: after, in: query, required: false, schema: { type: integer, format: int64 } }
- { name: task_id, in: query, required: false, schema: { type: string } }
responses:
"200":
description: Event stream.
content:
text/event-stream:
schema: { type: string }
"404": { $ref: "#/components/responses/Error" }
/api/admin/status:
get:
+3 -1
View File
@@ -157,4 +157,6 @@ Merging or discarding a task stops new snapshots for it, waits for one in progre
## Events
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. The web UI (`subscribe` in `web/src/api.ts`) tracks the last seq it saw and opens a new stream from it when nothing (event or ping) has arrived for 40s, 2s after the browser gives up on one (as it does when `tailscale serve` answers 502 while byre restarts), when the browser comes back online, and on becoming visible if the stream is stale. While disconnected the navbar shows a red dot and "reconnecting…"; partial output (`agent.delta`) sent while disconnected is lost, and the finished `agent.message` replaces it.
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. `?task_id=` limits the stream, replay and live, to one task's events (404 for an unknown task). Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. The web UI (`subscribe` in `web/src/api.ts`) tracks the last seq it saw and opens a new stream from it when nothing (event or ping) has arrived for 40s, 2s after the browser gives up on one (as it does when `tailscale serve` answers 502 while byre restarts), when the browser comes back online, and on becoming visible if the stream is stale. While disconnected the navbar shows a red dot and "reconnecting…"; partial output (`agent.delta`) sent while disconnected is lost, and the finished `agent.message` replaces it.
The web UI keeps two streams: one for all events from the page's load onward, which drives the task, session and inbox lists and the navbar's connection dot, and one per selected task (`selectTask` in `web/src/state.ts`), opened with `?task_id=` and `after=0` so it replays the task's whole history and then stays live. The transcript is drawn only from the task stream, so a reconnect after a byre restart fills in both the history and anything missed. Selecting another task closes the old task stream. Events arriving together are drawn in one batch.
+116 -102
View File
@@ -742,8 +742,9 @@ type ZombieKind string
// StreamEventsParams defines parameters for StreamEvents.
type StreamEventsParams struct {
After *int64 `form:"after,omitempty" json:"after,omitempty"`
LastEventID *int64 `json:"Last-Event-ID,omitempty"`
After *int64 `form:"after,omitempty" json:"after,omitempty"`
TaskId *string `form:"task_id,omitempty" json:"task_id,omitempty"`
LastEventID *int64 `json:"Last-Event-ID,omitempty"`
}
// ListInboxParams defines parameters for ListInbox.
@@ -990,6 +991,19 @@ func (siw *ServerInterfaceWrapper) StreamEvents(w http.ResponseWriter, r *http.R
return
}
// ------------- Optional query parameter "task_id" -------------
err = runtime.BindQueryParameterWithOptions("form", true, false, "task_id", r.URL.Query(), &params.TaskId, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
if err != nil {
var requiredError *runtime.RequiredParameterError
if errors.As(err, &requiredError) {
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "task_id"})
} else {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "task_id", Err: err})
}
return
}
headers := r.Header
// ------------- Optional header parameter "Last-Event-ID" -------------
@@ -1982,106 +1996,106 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
"8ZDdxWGNsQ5j/z5c1zD5p8073d7+bwAAAP//",
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+31 -4
View File
@@ -314,7 +314,7 @@ func writeFile(t *testing.T, path, content string) {
func TestEventStreamResume(t *testing.T) {
h := newHarness(t)
stream := h.openStream(nil)
stream := h.openStream("", nil)
p := h.createProject("demo")
first := stream.next()
if first.Kind != "project.created" || *first.ProjectId != p.Id {
@@ -326,7 +326,7 @@ func TestEventStreamResume(t *testing.T) {
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "while away"}, &task)
h.do("PUT", "/api/me", api.UserUpdate{GitName: "Alice", GitEmail: "alice@example.org"}, nil)
resumed := h.openStream(&first.Seq)
resumed := h.openStream("", &first.Seq)
missed := resumed.next()
if missed.Kind != "task.created" || *missed.TaskId != task.Id || missed.Seq != first.Seq+1 {
t.Fatalf("unexpected replayed event %+v", missed)
@@ -347,6 +347,33 @@ func TestEventStreamResume(t *testing.T) {
}
}
func TestTaskEventStream(t *testing.T) {
h := newHarness(t)
p := h.createProject("demo")
mine := api.Task{}
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "mine"}, &mine)
other := api.Task{}
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "other"}, &other)
stream := h.openStream("?task_id="+mine.Id, nil)
replayed := stream.next()
if replayed.Kind != "task.created" || *replayed.TaskId != mine.Id {
t.Fatalf("unexpected replayed event %+v", replayed)
}
h.do("POST", "/api/tasks/"+other.Id+"/discard", nil, nil)
h.do("POST", "/api/tasks/"+mine.Id+"/discard", nil, nil)
if live := stream.next(); live.Kind != "task.discarded" || *live.TaskId != mine.Id {
t.Fatalf("unexpected live event %+v", live)
}
code := h.do("GET", "/api/events?task_id=nope", nil, nil)
if code != http.StatusNotFound {
t.Fatalf("want 404 streaming an unknown task, got %d", code)
}
}
type stream struct {
t *testing.T
cancel context.CancelFunc
@@ -354,11 +381,11 @@ type stream struct {
r *bufio.Reader
}
func (h *harness) openStream(lastEventID *int64) *stream {
func (h *harness) openStream(query string, lastEventID *int64) *stream {
h.t.Helper()
ctx, cancel := context.WithCancel(context.Background())
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events", nil)
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events"+query, nil)
if err != nil {
h.t.Fatalf("new request: %v", err)
}
+19 -1
View File
@@ -19,6 +19,21 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
if params.LastEventID != nil {
last = *params.LastEventID
}
taskID := ""
if params.TaskId != nil {
taskID = *params.TaskId
_, err := s.store.GetTask(r.Context(), taskID)
if err != nil {
writeStoreError(w, err)
return
}
}
backfill := func(after int64) ([]api.Event, error) {
if taskID == "" {
return s.store.EventsAfter(r.Context(), after, backfillPage)
}
return s.store.TaskEvents(r.Context(), taskID, after, backfillPage)
}
rc := http.NewResponseController(w)
w.Header().Set("Content-Type", "text/event-stream")
@@ -29,7 +44,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
defer s.hub.unsubscribe(live)
for {
events, err := s.store.EventsAfter(r.Context(), last, backfillPage)
events, err := backfill(last)
if err != nil {
return
}
@@ -60,6 +75,9 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
if ev.Seq != 0 && ev.Seq <= last {
continue
}
if taskID != "" && (ev.TaskId == nil || *ev.TaskId != taskID) {
continue
}
if !writeEvent(w, ev) {
return
}
+18 -6
View File
@@ -52,7 +52,7 @@ export const put = (path, opts) => request("put", path, opts);
export const del = (path, opts) => request("delete", path, opts);
const staleAfterMs = 40_000;
const retryAfterMs = 2_000;
export function subscribe(after, onEvent, onState) {
export function subscribe(after, taskId, onEvent, onState) {
let last = after;
let heard = Date.now();
let source;
@@ -61,7 +61,11 @@ export function subscribe(after, onEvent, onState) {
clearTimeout(retry);
source?.close();
heard = Date.now();
const s = new EventSource(`/api/events?after=${last}`);
const query = new URLSearchParams({ after: String(last) });
if (taskId) {
query.set("task_id", taskId);
}
const s = new EventSource(`/api/events?${query}`);
source = s;
s.onopen = () => {
heard = Date.now();
@@ -92,13 +96,21 @@ export function subscribe(after, onEvent, onState) {
onState(false);
connect();
};
setInterval(check, 5_000);
addEventListener("online", connect);
document.addEventListener("visibilitychange", () => {
const onVisible = () => {
if (document.visibilityState === "visible") {
check();
}
});
};
const interval = setInterval(check, 5_000);
addEventListener("online", connect);
document.addEventListener("visibilitychange", onVisible);
connect();
return () => {
clearInterval(interval);
clearTimeout(retry);
removeEventListener("online", connect);
document.removeEventListener("visibilitychange", onVisible);
source?.close();
};
}
//# sourceMappingURL=api.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
+2 -2
View File
@@ -34,7 +34,7 @@ function route() {
const match = /^#\/task\/(\w+)$/.exec(location.hash);
const taskId = match?.[1];
if (taskId !== state.selectedTaskId) {
void selectTask(taskId);
selectTask(taskId);
}
}
async function main() {
@@ -54,7 +54,7 @@ async function main() {
}, { capture: true });
const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
subscribe(status.last_event_seq, handleEvent, (connected) => {
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
state.connected = connected;
notify("connection");
});
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
+32 -28
View File
@@ -1,4 +1,4 @@
import { get } from "./api.js";
import { get, subscribe } from "./api.js";
export const state = {
me: undefined,
projects: [],
@@ -71,7 +71,9 @@ export async function loadInbox() {
state.inbox = await get("/api/inbox");
notify("inbox");
}
let liveDuringLoad;
let closeTaskStream;
let taskBuffer = [];
let taskFlush = 0;
const visitsKey = "byre.visits";
export function lastVisits() {
return JSON.parse(localStorage.getItem(visitsKey) ?? "{}");
@@ -82,31 +84,44 @@ function markVisited(taskId) {
}
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
}
export async function selectTask(taskId) {
export function selectTask(taskId) {
markVisited(state.selectedTaskId);
markVisited(taskId);
closeTaskStream?.();
closeTaskStream = undefined;
clearTimeout(taskFlush);
taskFlush = 0;
taskBuffer = [];
state.selectedTaskId = taskId;
state.transcript = [];
state.lastTranscriptSeq = 0;
liveDuringLoad = undefined;
notify("selection");
if (!taskId) {
return;
}
const held = [];
liveDuringLoad = held;
for (;;) {
const page = await get("/api/tasks/{taskId}/events", { path: { taskId }, query: { after: state.lastTranscriptSeq, limit: 1000 } });
if (state.selectedTaskId !== taskId || liveDuringLoad !== held) {
return;
}
appendTranscript(page);
if (page.length < 1000) {
break;
}
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
}
function queueTaskEvent(ev) {
taskBuffer.push(ev);
if (!taskFlush) {
taskFlush = setTimeout(flushTaskEvents, 0);
}
liveDuringLoad = undefined;
appendTranscript(held);
}
function flushTaskEvents() {
const events = taskBuffer;
taskBuffer = [];
taskFlush = 0;
let stored = [];
for (const ev of events) {
if (ev.seq !== 0) {
stored.push(ev);
continue;
}
appendTranscript(stored);
stored = [];
notify("stream", ev);
}
appendTranscript(stored);
}
function appendTranscript(events) {
const fresh = events.filter((ev) => ev.seq > state.lastTranscriptSeq);
@@ -119,20 +134,9 @@ function appendTranscript(events) {
}
export function handleEvent(ev) {
if (ev.seq === 0) {
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
notify("stream", ev);
}
notify("event", ev);
return;
}
if (ev.task_id && ev.task_id === state.selectedTaskId) {
if (liveDuringLoad) {
liveDuringLoad.push(ev);
}
else {
appendTranscript([ev]);
}
}
switch (ev.kind) {
case "user.updated":
if (ev.payload["login"] === state.me?.login) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -3
View File
@@ -93,14 +93,18 @@ export function mountTranscript(root) {
transcript.scrollTop = transcript.scrollHeight;
}
};
const put = (el) => {
if (spinner.parentNode === transcript) {
const putBefore = (el, anchor) => {
if (anchor?.parentNode === transcript) {
transcript.insertBefore(el, anchor);
}
else if (spinner.parentNode === transcript) {
transcript.insertBefore(el, spinner);
}
else {
transcript.append(el);
}
};
const put = (el) => putBefore(el, pending[0]?.el);
const renderHead = () => {
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
setRunning(task !== undefined && currentSession(task.id)?.state === "running");
@@ -168,7 +172,7 @@ export function mountTranscript(root) {
const entry = { el, text, imgs };
pending.push(entry);
ctx.run = undefined;
put(el);
putBefore(el, spinner);
transcript.scrollTop = transcript.scrollHeight;
return () => {
el.remove();
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -481,7 +481,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. */
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. With task_id, only that task's events are sent. */
get: operations["streamEvents"];
put?: never;
post?: never;
@@ -1629,6 +1629,7 @@ export interface operations {
parameters: {
query?: {
after?: number;
task_id?: string;
};
header?: {
"Last-Event-ID"?: number;
@@ -1647,6 +1648,7 @@ export interface operations {
"text/event-stream": string;
};
};
404: components["responses"]["Error"];
};
};
getStatus: {
+24 -6
View File
@@ -103,7 +103,12 @@ export const del = <P extends PathsWith<"delete">>(path: P, opts?: Options<Op<P,
const staleAfterMs = 40_000;
const retryAfterMs = 2_000;
export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onState: (connected: boolean) => void): void {
export function subscribe(
after: number,
taskId: string | undefined,
onEvent: (ev: ByreEvent) => void,
onState: (connected: boolean) => void,
): () => void {
let last = after;
let heard = Date.now();
let source: EventSource | undefined;
@@ -113,7 +118,11 @@ export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onSta
clearTimeout(retry);
source?.close();
heard = Date.now();
const s = new EventSource(`/api/events?after=${last}`);
const query = new URLSearchParams({ after: String(last) });
if (taskId) {
query.set("task_id", taskId);
}
const s = new EventSource(`/api/events?${query}`);
source = s;
s.onopen = () => {
heard = Date.now();
@@ -146,12 +155,21 @@ export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onSta
connect();
};
setInterval(check, 5_000);
addEventListener("online", connect);
document.addEventListener("visibilitychange", () => {
const onVisible = () => {
if (document.visibilityState === "visible") {
check();
}
});
};
const interval = setInterval(check, 5_000);
addEventListener("online", connect);
document.addEventListener("visibilitychange", onVisible);
connect();
return () => {
clearInterval(interval);
clearTimeout(retry);
removeEventListener("online", connect);
document.removeEventListener("visibilitychange", onVisible);
source?.close();
};
}
+2 -2
View File
@@ -35,7 +35,7 @@ function route(): void {
const match = /^#\/task\/(\w+)$/.exec(location.hash);
const taskId = match?.[1];
if (taskId !== state.selectedTaskId) {
void selectTask(taskId);
selectTask(taskId);
}
}
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
subscribe(status.last_event_seq, handleEvent, (connected) => {
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
state.connected = connected;
notify("connection");
});
+34 -28
View File
@@ -1,4 +1,4 @@
import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
import { get, subscribe, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
@@ -87,7 +87,9 @@ export async function loadInbox(): Promise<void> {
notify("inbox");
}
let liveDuringLoad: ByreEvent[] | undefined;
let closeTaskStream: (() => void) | undefined;
let taskBuffer: ByreEvent[] = [];
let taskFlush = 0;
const visitsKey = "byre.visits";
@@ -102,33 +104,48 @@ function markVisited(taskId: string | undefined): void {
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
}
export async function selectTask(taskId: string | undefined): Promise<void> {
export function selectTask(taskId: string | undefined): void {
markVisited(state.selectedTaskId);
markVisited(taskId);
closeTaskStream?.();
closeTaskStream = undefined;
clearTimeout(taskFlush);
taskFlush = 0;
taskBuffer = [];
state.selectedTaskId = taskId;
state.transcript = [];
state.lastTranscriptSeq = 0;
liveDuringLoad = undefined;
notify("selection");
if (!taskId) {
return;
}
const held: ByreEvent[] = [];
liveDuringLoad = held;
for (;;) {
const page = await get("/api/tasks/{taskId}/events", { path: { taskId }, query: { after: state.lastTranscriptSeq, limit: 1000 } });
if (state.selectedTaskId !== taskId || liveDuringLoad !== held) {
return;
}
appendTranscript(page);
if (page.length < 1000) {
break;
}
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
}
function queueTaskEvent(ev: ByreEvent): void {
taskBuffer.push(ev);
if (!taskFlush) {
taskFlush = setTimeout(flushTaskEvents, 0);
}
liveDuringLoad = undefined;
appendTranscript(held);
}
function flushTaskEvents(): void {
const events = taskBuffer;
taskBuffer = [];
taskFlush = 0;
let stored: ByreEvent[] = [];
for (const ev of events) {
if (ev.seq !== 0) {
stored.push(ev);
continue;
}
appendTranscript(stored);
stored = [];
notify("stream", ev);
}
appendTranscript(stored);
}
function appendTranscript(events: ByreEvent[]): void {
@@ -143,21 +160,10 @@ function appendTranscript(events: ByreEvent[]): void {
export function handleEvent(ev: ByreEvent): void {
if (ev.seq === 0) {
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
notify("stream", ev);
}
notify("event", ev);
return;
}
if (ev.task_id && ev.task_id === state.selectedTaskId) {
if (liveDuringLoad) {
liveDuringLoad.push(ev);
} else {
appendTranscript([ev]);
}
}
switch (ev.kind) {
case "user.updated":
if (ev.payload["login"] === state.me?.login) {
+6 -3
View File
@@ -116,13 +116,16 @@ export function mountTranscript(root: HTMLElement): () => void {
transcript.scrollTop = transcript.scrollHeight;
}
};
const put = (el: HTMLElement) => {
if (spinner.parentNode === transcript) {
const putBefore = (el: HTMLElement, anchor: HTMLElement | undefined) => {
if (anchor?.parentNode === transcript) {
transcript.insertBefore(el, anchor);
} else if (spinner.parentNode === transcript) {
transcript.insertBefore(el, spinner);
} else {
transcript.append(el);
}
};
const put = (el: HTMLElement) => putBefore(el, pending[0]?.el);
const renderHead = () => {
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
@@ -207,7 +210,7 @@ export function mountTranscript(root: HTMLElement): () => void {
const entry = { el, text, imgs };
pending.push(entry);
ctx.run = undefined;
put(el);
putBefore(el, spinner);
transcript.scrollTop = transcript.scrollHeight;
return () => {
el.remove();