diff --git a/api/openapi.yaml b/api/openapi.yaml index bd8a580..7edde38 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2436,9 +2436,10 @@ components: Status: type: object - required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, clients, live_agents, running_turns, rate_limits, agent_processes, zombies, stuck_after_seconds, protocol_errors] + required: [started_at, db_bytes, last_event_seq, schema, projects, active_tasks, sse_clients, clients, live_agents, running_turns, rate_limits, agent_processes, zombies, stuck_after_seconds, protocol_errors] properties: live_agents: { type: integer, description: Agent processes currently running. } + schema: { type: string, description: "Fingerprint of the database's applied migrations. It changes when a migration lands or the database is recreated; the app then drops the events it cached from this server." } agent_processes: type: array description: Every agent process byre is tracking, idle sessions included. diff --git a/docs/claude-protocol.md b/docs/claude-protocol.md index d76ef48..deeea48 100644 --- a/docs/claude-protocol.md +++ b/docs/claude-protocol.md @@ -30,6 +30,7 @@ NDJSON both ways: one JSON object per line on stdin and stdout. Non-JSON stdout - Newer CLIs also send `command_lifecycle` lines (`command_uuid`, `state` such as `completed`). byre drops them, as it does `keep_alive`. - The `init` event arrives with the first turn, not at launch, so the session ID is unknown until the first message is sent. - While a tool runs, `tool_progress` lines with `"heartbeat": true`, the tool's `elapsed_time_seconds`, and a `parent_tool_use_id` naming the tool call arrive every 30 seconds. A Bash call also brings `system` `task_started` and `task_notification` lines around it. +- `system` lines with subtype `status` (`"status": "requesting"` as each model request starts) and `thinking_tokens` (a running `estimated_tokens` while the model thinks, several a second) are progress only. byre stores neither them nor `tool_progress`; the other lines it doesn't handle are kept as `agent.vendor` events. - Subagents (the `Agent` tool, listed in `init` tools as `Task`): `system` `task_started` (`task_type` `local_agent`, `tool_use_id` naming the `Agent` call, `subagent_type`, `description`, `prompt`, `is_backgrounded`), `task_progress` (`description` of what it is doing), `task_updated` (by `task_id` only), and `task_notification` (`status`, `summary`). `assistant` and `user` lines from inside the subagent carry `parent_tool_use_id` set to the `Agent` call's ID, plus `agent_id`, `subagent_type` and `task_description`. A foreground subagent (`run_in_background: false`) sends only its prompt this way, as a `user` text block, and its final report arrives as the `Agent` call's `tool_result`. A background one (the default) streams all its assistant and tool-result lines; the `Agent` call's `tool_result` comes back at once saying it launched, the turn can end with its own `result` while the subagent runs, and when the subagent finishes the CLI starts another turn by itself, ending in a second `result` carrying `origin.kind` `task-notification`. ## Control channel diff --git a/docs/macos.md b/docs/macos.md index 7ee2471..93ddb0a 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -48,6 +48,8 @@ The update UI must not offer Skip or turning automatic updates off: on a Mac run CloudKit has no unique constraints, so two Macs can each create a record for the same thing; duplicate records are expected. See `docs/operations.md`, "CloudKit schema", before changing the SwiftData models. The store is named `byre.store` because the app isn't sandboxed, so SwiftData's default `default.store` would be shared with every other unsandboxed app. Deleting the local files while the app isn't running makes the next launch download the records again. +Each task's events are cached on disk per server, under `run.fc.byre.mac/events//` in Application Support, so a launch fetches only the events newer than the cached ones. Events never change, so the cache is keyed on the one thing that rewrites them: the `schema` fingerprint in `/api/status` (the server's applied migrations), and the whole server's cache is dropped when it differs. A task's file goes when the task closes, and a file that stops decoding is cut at its last good line, with the rest fetched again. A server restart replaces the Store while the old one still shows the transcripts, so each connection's cache opens with a generation and takes appends only from feeds loaded under the current one; a file is only ever extended by a writer that read it first. + ## Gaps in SwiftUI Places where SwiftUI alone may fall short, each checked side by side against its fallback; the fallback is used only where the check failed: diff --git a/internal/agents/manager.go b/internal/agents/manager.go index f065d29..9c8424b 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -35,6 +35,8 @@ const agentActor = "agent" const restartNotice = "byre restarted while you were mid-turn, so your last turn was cut off. Re-check the working tree (git status and the files you were editing) before continuing where you left off." +var progressSubtypes = map[string]bool{"status": true, "thinking_tokens": true} + type Manager struct { store *store.Store worktreeRoot string @@ -1143,9 +1145,12 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error { m.setVendorUsage(l.vendor, claudeUsage(info.RateLimitInfo)) case "system": - if ev.Subtype == "init" { + if ev.Subtype == "init" || progressSubtypes[ev.Subtype] { return nil } + + case "tool_progress": + return nil } raw := map[string]any{} @@ -1261,7 +1266,6 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error { } } } - m.refreshFacts(ctx, l.taskID) handoff := compacting && finished if sha != "" { payload["commit"] = sha diff --git a/internal/agents/mergefacts.go b/internal/agents/mergefacts.go index 0556f2f..3398f77 100644 --- a/internal/agents/mergefacts.go +++ b/internal/agents/mergefacts.go @@ -83,20 +83,6 @@ func (m *Manager) settleFacts(ctx context.Context, dirty map[string]bool) error return nil } -func (m *Manager) refreshFacts(ctx context.Context, taskID string) { - t, err := m.store.GetTask(ctx, taskID) - if err == nil { - var p api.Project - p, err = m.store.GetProject(ctx, t.ProjectId) - if err == nil { - err = m.settleTaskFacts(ctx, t, p) - } - } - if err != nil { - slog.Error("refresh merge facts", "task", taskID, "err", err) - } -} - func (m *Manager) settleTaskFacts(ctx context.Context, t api.Task, p api.Project) error { m.factsRun.Lock() defer m.factsRun.Unlock() diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 4e40dcf..d055528 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1487,8 +1487,11 @@ type Status struct { // RunningTurns Turns running now, oldest session first. RunningTurns []TurnRef `json:"running_turns"` - SseClients int `json:"sse_clients"` - StartedAt time.Time `json:"started_at"` + + // Schema Fingerprint of the database's applied migrations. It changes when a migration lands or the database is recreated; the app then drops the events it cached from this server. + Schema string `json:"schema"` + SseClients int `json:"sse_clients"` + StartedAt time.Time `json:"started_at"` // StuckAfterSeconds How long a running turn may go without a stream event before it counts as stuck. StuckAfterSeconds int `json:"stuck_after_seconds"` @@ -5303,229 +5306,230 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/server.go b/internal/server/server.go index e31536a..84524a8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -762,6 +762,7 @@ func (s *Server) status(ctx context.Context) (api.Status, error) { StartedAt: s.startedAt, DbBytes: dbBytes, LastEventSeq: c.LastEventSeq, + Schema: s.store.Schema(), Projects: c.Projects, ActiveTasks: c.ActiveTasks, SseClients: s.hub.count(), diff --git a/internal/store/migrations/109-drop-progress-vendor-events.sql b/internal/store/migrations/109-drop-progress-vendor-events.sql new file mode 100644 index 0000000..52c9d1e --- /dev/null +++ b/internal/store/migrations/109-drop-progress-vendor-events.sql @@ -0,0 +1,3 @@ +DELETE FROM events WHERE kind = 'agent.vendor' + AND (json_extract(payload, '$.type') = 'tool_progress' + OR (json_extract(payload, '$.type') = 'system' AND json_extract(payload, '$.subtype') IN ('status', 'thinking_tokens'))); diff --git a/internal/store/store.go b/internal/store/store.go index cdeb319..d6cfc8b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -2,8 +2,10 @@ package store import ( "context" + "crypto/sha256" "database/sql" "embed" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -74,6 +76,7 @@ const TaskColors = 11 type Store struct { db *sql.DB path string + schema string writeMu sync.Mutex onEvent func([]api.Event) } @@ -92,7 +95,36 @@ func Open(path string, onEvent func([]api.Event)) (*Store, error) { return nil, err } - return &Store{db: db, path: path, onEvent: onEvent}, nil + schema, err := schemaFingerprint(db) + if err != nil { + db.Close() + return nil, err + } + + return &Store{db: db, path: path, schema: schema, onEvent: onEvent}, nil +} + +func schemaFingerprint(db *sql.DB) (string, error) { + rows, err := db.Query(`SELECT name, applied_at FROM migrations ORDER BY name`) + if err != nil { + return "", err + } + defer rows.Close() + + h := sha256.New() + for rows.Next() { + name, appliedAt := "", "" + err = rows.Scan(&name, &appliedAt) + if err != nil { + return "", err + } + fmt.Fprintf(h, "%s %s\n", name, appliedAt) + } + return hex.EncodeToString(h.Sum(nil))[:16], rows.Err() +} + +func (s *Store) Schema() string { + return s.schema } func migrate(db *sql.DB, dataDir string) error { diff --git a/mac/Sources/Byre/EventCache.swift b/mac/Sources/Byre/EventCache.swift new file mode 100644 index 0000000..4c82e49 --- /dev/null +++ b/mac/Sources/Byre/EventCache.swift @@ -0,0 +1,139 @@ +import Foundation + +private let encoder: JSONEncoder = { + let e = JSONEncoder() + e.dateEncodingStrategy = .secondsSince1970 + return e +}() + +private let decoder: JSONDecoder = { + let d = JSONDecoder() + d.dateDecodingStrategy = .secondsSince1970 + return d +}() + +final class EventCache: @unchecked Sendable { + private let dir: URL + private let queue = DispatchQueue(label: "run.fc.byre.events") + private var lastSeqs: [String: Int64] = [:] + private var generation = 0 + + init(host: String) { + dir = URL.applicationSupportDirectory.appending(path: "run.fc.byre.mac/events/\(host)", directoryHint: .isDirectory) + } + + func open(schema: String) { + queue.async { + self.generation += 1 + let fm = FileManager.default + let marker = self.dir.appending(path: "schema") + if (try? String(contentsOf: marker, encoding: .utf8)) == schema { + return + } + self.lastSeqs = [:] + try? fm.removeItem(at: self.dir) + do { + try fm.createDirectory(at: self.dir, withIntermediateDirectories: true) + try schema.write(to: marker, atomically: true, encoding: .utf8) + } catch { + log.error("open event cache: \(errorText(error), privacy: .public)") + } + } + } + + func load(_ taskId: String) async -> (events: [ByreEvent], generation: Int) { + await withCheckedContinuation { c in + queue.async { + let events = self.read(taskId) + self.lastSeqs[taskId] = events.last?.seq ?? 0 + c.resume(returning: (events, self.generation)) + } + } + } + + func append(_ taskId: String, _ events: [ByreEvent], generation: Int) { + queue.async { + guard generation == self.generation, let last = self.lastSeqs[taskId] else { + return + } + let fresh = events.filter { $0.seq > last } + guard let newest = fresh.last else { + return + } + do { + try self.write(taskId, fresh) + self.lastSeqs[taskId] = newest.seq + } catch { + log.error("cache events of \(taskId, privacy: .public): \(errorText(error), privacy: .public)") + self.forget(taskId) + } + } + } + + func drop(_ taskId: String) { + queue.async { + self.forget(taskId) + } + } + + func keep(_ taskIds: Set) { + queue.async { + let files = (try? FileManager.default.contentsOfDirectory(at: self.dir, includingPropertiesForKeys: nil)) ?? [] + for url in files where url.pathExtension == "ndjson" { + let taskId = url.deletingPathExtension().lastPathComponent + if !taskIds.contains(taskId) { + self.forget(taskId) + } + } + } + } + + private func file(_ taskId: String) -> URL { + dir.appending(path: "\(taskId).ndjson") + } + + private func forget(_ taskId: String) { + lastSeqs[taskId] = nil + try? FileManager.default.removeItem(at: file(taskId)) + } + + private func read(_ taskId: String) -> [ByreEvent] { + let url = file(taskId) + guard let data = try? Data(contentsOf: url) else { + return [] + } + var events: [ByreEvent] = [] + var kept = 0 + for line in data.split(separator: 0x0a, omittingEmptySubsequences: false) where !line.isEmpty { + guard let ev = try? decoder.decode(ByreEvent.self, from: line) else { + break + } + events.append(ev) + kept = min(line.endIndex + 1, data.endIndex) - data.startIndex + } + if kept < data.count, let h = try? FileHandle(forWritingTo: url) { + try? h.truncate(atOffset: UInt64(kept)) + try? h.close() + } + return events + } + + private func write(_ taskId: String, _ events: [ByreEvent]) throws { + var data = Data() + for ev in events { + data.append(try encoder.encode(ev)) + data.append(0x0a) + } + let url = file(taskId) + let fm = FileManager.default + if !fm.fileExists(atPath: url.path), !fm.createFile(atPath: url.path, contents: nil) { + throw ApiError(status: 0, message: "create \(url.path)") + } + let h = try FileHandle(forWritingTo: url) + defer { + try? h.close() + } + try h.seekToEnd() + try h.write(contentsOf: data) + } +} diff --git a/mac/Sources/Byre/Feed.swift b/mac/Sources/Byre/Feed.swift index b6a8127..a454214 100644 --- a/mac/Sources/Byre/Feed.swift +++ b/mac/Sources/Byre/Feed.swift @@ -42,6 +42,7 @@ final class Feed { @ObservationIgnored private var scheduled = false @ObservationIgnored private var toolNames: [String: String] = [:] @ObservationIgnored private let shown: () -> Bool + @ObservationIgnored private var persist: ([ByreEvent]) -> Void = { _ in } init(shown: @escaping () -> Bool) { self.shown = shown @@ -60,12 +61,13 @@ final class Feed { } } - func load(_ history: [ByreEvent], inherited: Int = 0) { + func load(_ history: [ByreEvent], inherited: Int = 0, persist: @escaping ([ByreEvent]) -> Void = { _ in }) { if ready { return } ready = true self.inherited = inherited + self.persist = persist buffer = history + buffer flush() } @@ -131,6 +133,7 @@ final class Feed { return } events.append(contentsOf: fresh) + persist(fresh) let found = findCaptures(fresh) if !found.isEmpty { captures.append(contentsOf: found) diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index 101a672..56869df 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -173,6 +173,7 @@ final class Connection { let overlays = Overlays() let forwards: Forwards let pages: Pages + let cache: EventCache var phase: Phase = .starting @ObservationIgnored private var stopped = false @ObservationIgnored private var restarting = false @@ -181,6 +182,7 @@ final class Connection { self.origin = origin forwards = Forwards(host: origin.host()!) pages = Pages(api: API(origin: origin)) + cache = EventCache(host: origin.host()!) Task { await start() } @@ -231,9 +233,10 @@ final class Connection { } private func attempt(_ old: Store?) async -> Bool { - let store = Store(api: API(origin: origin), images: old?.images ?? ImageDrafts(), forwards: forwards, pages: pages) + let store = Store(api: API(origin: origin), images: old?.images ?? ImageDrafts(), forwards: forwards, pages: pages, cache: cache) do { let status = try await store.api.client.getStatus().ok.body.json + cache.open(schema: status.schema) async let me: Void = store.loadMe() async let projects: Void = store.loadProjects() async let inbox: Void = store.loadInbox() @@ -254,6 +257,7 @@ final class Connection { store.startGlobalStream(after: status.lastEventSeq) { [weak self] in self?.reconnected(status.startedAt) } + store.startFeeds() store.remote.start() forwards.sync(store.api, Set(store.activeTasks().map(\.task.id))) Notifier.shared.deliver() diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index ed0df3d..8178d7b 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -92,9 +92,11 @@ final class Store { let remote: Remote let forwards: Forwards let pages: Pages + let cache: EventCache private var taskFeeds: [String: Feed] = [:] @ObservationIgnored private var historyLoads: Set = [] + @ObservationIgnored private var feedsStarted = false @ObservationIgnored private var requestSeqs: [String: Int] = [:] @ObservationIgnored private var taskReloads: Set = [] @ObservationIgnored private var listReloads: Set = [] @@ -109,11 +111,12 @@ final class Store { let changesMemory = ChangesMemory() @ObservationIgnored private var localSends: [String: LocalSends] = [:] - init(api: API, images: ImageDrafts, forwards: Forwards, pages: Pages) { + init(api: API, images: ImageDrafts, forwards: Forwards, pages: Pages, cache: EventCache) { self.api = api self.images = images self.forwards = forwards self.pages = pages + self.cache = cache leadFeed = Feed(shown: { true }) paneFocus = PaneFocus(api: api) remote = Remote(api: api) @@ -443,8 +446,10 @@ final class Store { private func applyTasks(_ projectId: String, _ list: [ByreTask]) async throws { tasks[projectId] = list.sorted { $0.createdAt > $1.createdAt } - for t in list where t.status == .active && !t.background { - _ = taskFeed(t.id) + if feedsStarted { + for t in list where t.status == .active && !t.background { + _ = taskFeed(t.id) + } } dropClosedFeeds() drafts?.prune(self) @@ -622,7 +627,8 @@ final class Store { } private func loadHistory(_ taskId: String, _ feed: Feed) async throws { - var got: [ByreEvent] = [] + let (cached, generation) = await cache.load(taskId) + var got = cached while true { let page = try await api.client.listTaskEvents(path: .init(taskId: taskId), query: .init(after: got.last?.seq ?? 0, limit: historyPage)).ok.body.json got += page @@ -630,7 +636,18 @@ final class Store { break } } - feed.load(got, inherited: got.prefix { $0.taskId != taskId }.count) + feed.load(got, inherited: got.prefix { $0.taskId != taskId }.count, persist: { [cache] events in + cache.append(taskId, events, generation: generation) + }) + } + + func startFeeds() { + feedsStarted = true + let active = activeTasks() + cache.keep(Set(active.map(\.task.id))) + for (_, task) in active where !task.background { + _ = taskFeed(task.id) + } } private func taskFeed(_ taskId: String) -> Feed { @@ -658,6 +675,7 @@ final class Store { for taskId in taskFeeds.keys { if taskId != selectedTaskId, let task = findTask(taskId), task.status != .active { taskFeeds[taskId] = nil + cache.drop(taskId) } } }