@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +16 -2
  3. package/dist/agents.js +11 -0
  4. package/dist/cli-adapter.js +8 -0
  5. package/dist/component.js +65 -0
  6. package/dist/config-schema.js +265 -0
  7. package/dist/execution/activity.js +78 -0
  8. package/dist/execution/content.js +184 -0
  9. package/dist/execution/json-sections.js +231 -0
  10. package/dist/execution/presentation.js +79 -0
  11. package/dist/execution/preview.js +13 -0
  12. package/dist/execution/process.js +125 -0
  13. package/dist/execution/projection.js +327 -0
  14. package/dist/execution/query.js +18 -0
  15. package/dist/execution/routes.js +189 -0
  16. package/dist/execution/schema.js +322 -0
  17. package/dist/execution/service.js +1993 -0
  18. package/dist/features.js +22 -0
  19. package/dist/http.js +65 -0
  20. package/dist/performance/analysis.js +330 -0
  21. package/dist/performance/catalog.js +391 -0
  22. package/dist/performance/dashboard.js +483 -0
  23. package/dist/performance/error.js +15 -0
  24. package/dist/performance/index.js +6 -0
  25. package/dist/performance/inflight.js +43 -0
  26. package/dist/performance/projection.js +138 -0
  27. package/dist/performance/prompt/base.txt +38 -0
  28. package/dist/performance/prompt/builder.js +21 -0
  29. package/dist/performance/routes/performance-route.js +439 -0
  30. package/dist/performance/schema.js +628 -0
  31. package/dist/performance/timeline.js +98 -0
  32. package/dist/performance/trace-detail.js +91 -0
  33. package/dist/presentation/service.js +270 -0
  34. package/dist/presentation/tools/workbench-view.js +65 -0
  35. package/dist/presentation/tools.js +10 -0
  36. package/dist/project/directories.js +363 -0
  37. package/dist/project/git-health.js +498 -0
  38. package/dist/project/ide.js +66 -0
  39. package/dist/project/migration.js +22 -0
  40. package/dist/project/routes/directories.js +89 -0
  41. package/dist/project/routes/git.js +30 -0
  42. package/dist/project/routes/task-defaults.js +42 -0
  43. package/dist/project/session-health.js +9 -0
  44. package/dist/project/startup.js +28 -0
  45. package/dist/project/task-defaults.js +68 -0
  46. package/dist/project/tools/session-control.js +664 -0
  47. package/dist/project/tools/session-control.txt +60 -0
  48. package/dist/project/tools/worktree-archive.js +76 -0
  49. package/dist/project/tools/worktree-enter.js +175 -0
  50. package/dist/project/tools/worktree-leave.js +119 -0
  51. package/dist/project/tools/worktree-list.js +52 -0
  52. package/dist/project/tools.js +26 -0
  53. package/dist/project/vcs-branch-watcher.js +76 -0
  54. package/dist/project/vcs.js +114 -0
  55. package/dist/project/worktree-janitor.js +140 -0
  56. package/dist/project/worktrees.js +98 -0
  57. package/dist/push/bridge.js +171 -0
  58. package/dist/push/index.js +4 -0
  59. package/dist/push/routes/push.js +224 -0
  60. package/dist/push/service.js +93 -0
  61. package/dist/push/store.js +121 -0
  62. package/dist/push/types.js +58 -0
  63. package/dist/review/git.js +226 -0
  64. package/dist/review/routes.js +45 -0
  65. package/dist/review/schema.js +32 -0
  66. package/dist/review/state.js +79 -0
  67. package/dist/stats/aggregator.js +252 -0
  68. package/dist/stats/canonical.js +89 -0
  69. package/dist/stats/cli/stats.js +302 -0
  70. package/dist/stats/engine.js +176 -0
  71. package/dist/stats/index.js +5 -0
  72. package/dist/stats/rollup.js +460 -0
  73. package/dist/stats/routes/stats.js +94 -0
  74. package/dist/stats/routes/usage.js +83 -0
  75. package/dist/stats/storage.js +86 -0
  76. package/dist/stats/types.js +267 -0
  77. package/dist/worker.js +6 -0
  78. package/package.json +232 -4
  79. package/src/agents.ts +15 -0
  80. package/src/cli-adapter.ts +11 -0
  81. package/src/component.ts +73 -0
  82. package/src/config-schema.ts +289 -0
  83. package/src/execution/activity.ts +83 -0
  84. package/src/execution/content.ts +189 -0
  85. package/src/execution/json-sections.ts +230 -0
  86. package/src/execution/presentation.ts +91 -0
  87. package/src/execution/preview.ts +12 -0
  88. package/src/execution/process.ts +118 -0
  89. package/src/execution/projection.ts +330 -0
  90. package/src/execution/query.ts +19 -0
  91. package/src/execution/routes.ts +324 -0
  92. package/src/execution/schema.ts +336 -0
  93. package/src/execution/service.ts +2149 -0
  94. package/src/features.ts +22 -0
  95. package/src/http.ts +75 -0
  96. package/src/performance/analysis.ts +356 -0
  97. package/src/performance/catalog.ts +902 -0
  98. package/src/performance/dashboard.ts +521 -0
  99. package/src/performance/error.ts +32 -0
  100. package/src/performance/index.ts +6 -0
  101. package/src/performance/inflight.ts +46 -0
  102. package/src/performance/projection.ts +143 -0
  103. package/src/performance/prompt/base.txt +38 -0
  104. package/src/performance/prompt/builder.ts +24 -0
  105. package/src/performance/routes/performance-route.ts +541 -0
  106. package/src/performance/schema.ts +695 -0
  107. package/src/performance/timeline.ts +100 -0
  108. package/src/performance/trace-detail.ts +97 -0
  109. package/src/presentation/service.ts +303 -0
  110. package/src/presentation/tools/workbench-view.ts +75 -0
  111. package/src/presentation/tools.ts +11 -0
  112. package/src/project/directories.ts +391 -0
  113. package/src/project/git-health.ts +577 -0
  114. package/src/project/ide.ts +80 -0
  115. package/src/project/migration.ts +24 -0
  116. package/src/project/routes/directories.ts +124 -0
  117. package/src/project/routes/git.ts +42 -0
  118. package/src/project/routes/task-defaults.ts +50 -0
  119. package/src/project/session-health.ts +10 -0
  120. package/src/project/startup.ts +28 -0
  121. package/src/project/task-defaults.ts +70 -0
  122. package/src/project/tools/session-control.ts +725 -0
  123. package/src/project/tools/session-control.txt +60 -0
  124. package/src/project/tools/worktree-archive.ts +92 -0
  125. package/src/project/tools/worktree-enter.ts +227 -0
  126. package/src/project/tools/worktree-leave.ts +140 -0
  127. package/src/project/tools/worktree-list.ts +68 -0
  128. package/src/project/tools.ts +29 -0
  129. package/src/project/vcs-branch-watcher.ts +81 -0
  130. package/src/project/vcs.ts +124 -0
  131. package/src/project/worktree-janitor.ts +153 -0
  132. package/src/project/worktrees.ts +110 -0
  133. package/src/push/bridge.ts +182 -0
  134. package/src/push/index.ts +4 -0
  135. package/src/push/routes/push.ts +267 -0
  136. package/src/push/service.ts +108 -0
  137. package/src/push/store.ts +120 -0
  138. package/src/push/types.ts +67 -0
  139. package/src/review/git.ts +221 -0
  140. package/src/review/routes.ts +65 -0
  141. package/src/review/schema.ts +34 -0
  142. package/src/review/state.ts +77 -0
  143. package/src/stats/aggregator.ts +277 -0
  144. package/src/stats/canonical.ts +93 -0
  145. package/src/stats/cli/stats.ts +348 -0
  146. package/src/stats/engine.ts +197 -0
  147. package/src/stats/index.ts +5 -0
  148. package/src/stats/rollup.ts +522 -0
  149. package/src/stats/routes/stats.ts +107 -0
  150. package/src/stats/routes/usage.ts +103 -0
  151. package/src/stats/storage.ts +78 -0
  152. package/src/stats/types.ts +351 -0
  153. package/src/worker.ts +7 -0
@@ -0,0 +1,2149 @@
1
+ import { ModelLimit } from "@ericsanchezok/synergy-util/model-limit"
2
+ import { z } from "zod"
3
+ import { Identifier } from "@ericsanchezok/synergy-harness/id/id"
4
+ import { ObservabilityMetrics } from "@ericsanchezok/synergy-harness/observability/metrics"
5
+ import { SessionInbox } from "@ericsanchezok/synergy-harness/session/inbox"
6
+ import { reconcileContextUsage } from "@ericsanchezok/synergy-harness/context"
7
+ import { Session } from "@ericsanchezok/synergy-harness/session"
8
+ import { SessionEvent } from "@ericsanchezok/synergy-harness/session/event"
9
+ import { MessageV2 } from "@ericsanchezok/synergy-harness/session/message-v2"
10
+ import { SessionHistory } from "@ericsanchezok/synergy-harness/session/history"
11
+ import {
12
+ RolloutAccounting,
13
+ RolloutExecution,
14
+ RolloutEvidence,
15
+ RolloutEvents,
16
+ RolloutSchema,
17
+ RolloutSnapshot,
18
+ } from "@ericsanchezok/synergy-harness/rollout"
19
+ import { Usage, type UsageSchema } from "@ericsanchezok/synergy-harness/usage"
20
+ import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context"
21
+ import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
22
+ import { Bus } from "@ericsanchezok/synergy-harness/bus"
23
+ import { Storage } from "@ericsanchezok/synergy-harness/storage/storage"
24
+ import { Log } from "@ericsanchezok/synergy-harness/util/log"
25
+ import { ObservabilitySpans } from "@ericsanchezok/synergy-harness/observability/spans"
26
+ import { ExecutionQueryError, queryCursor, queryValue } from "./query"
27
+ import { ExecutionSchema } from "./schema"
28
+ import { ExecutionActivity } from "./activity"
29
+ import { ExecutionPresentation } from "./presentation"
30
+ import { ExecutionContent } from "./content"
31
+ import { ExecutionProcess } from "./process"
32
+ import { preview } from "./preview"
33
+
34
+ export namespace ExecutionService {
35
+ const log = Log.create({ service: "execution" })
36
+ type Evidence = { owner: RolloutSchema.Owner; record: RolloutEvents.Record }
37
+ type TimingInput = Parameters<typeof RolloutExecution.summarize>[0]
38
+ type Derived = {
39
+ revision: number
40
+ nodes: ExecutionSchema.Node[]
41
+ activity?: ReturnType<typeof ExecutionActivity.project>
42
+ modes: Map<"process" | "records", ExecutionSchema.Node[]>
43
+ prepared?: { summary: ExecutionSchema.Summary; timings: Map<string, TimingInput> }
44
+ }
45
+ type Data = {
46
+ root: Session.Info
47
+ sessions: Session.Info[]
48
+ snapshots: RolloutSnapshot.Info[]
49
+ verified: Set<string>
50
+ nodes: ExecutionSchema.Node[]
51
+ sessionsByID: Map<string, Session.Info>
52
+ snapshotsByID: Map<string, RolloutSnapshot.Info>
53
+ detail?: {
54
+ evidence: Map<string, Evidence>
55
+ messages: Map<string, { sessionID: string; messageID: string; partID: string }>
56
+ nodePositions: Map<string, number>
57
+ }
58
+ usage: Map<string, UsageSchema.Record>
59
+ contexts: Map<string, NonNullable<MessageV2.Assistant["contextUsage"]>>
60
+ inboxes: Map<string, SessionInbox.Item[]>
61
+ revision: number
62
+ summary: ExecutionSchema.Summary
63
+ touched: number
64
+ pendingMessages: Map<string, string>
65
+ changed: Set<string>
66
+ removed: Set<string>
67
+ refreshOwners: Set<string>
68
+ addedSessions: Map<string, Session.Info>
69
+ changes: Changes
70
+ selections: Map<string, { summary: ExecutionSchema.Summary; touched: number }>
71
+ processRows: Map<string, ExecutionSchema.Node>
72
+ recordRows: Map<string, ExecutionSchema.Node>
73
+ contextRows: Map<string, string>
74
+ publishedRevision: number
75
+ derived: Map<string, Derived>
76
+ }
77
+ type Changes = {
78
+ owners: Set<string>
79
+ events: Map<string, { ownerID: string; apply: (entry: Data) => void }>
80
+ repair: Set<string>
81
+ }
82
+ type Loading = {
83
+ scopeID: string
84
+ promise: Promise<Data>
85
+ changes: Changes
86
+ controller: AbortController
87
+ readers: number
88
+ }
89
+ type RootData = Pick<Data, "root" | "usage" | "contexts" | "revision"> & {
90
+ snapshots: Awaited<ReturnType<typeof RolloutSnapshot.modelCalls>>[]
91
+ infos: Map<string, MessageV2.Info>
92
+ rows: ExecutionSchema.ContextSnapshot[]
93
+ changed: Set<string>
94
+ orderChanged: boolean
95
+ usageByCall: Map<string, UsageSchema.Record[]>
96
+ orderedCalls: RolloutSchema.CallRecord[]
97
+ pendingMessages: Set<string>
98
+ }
99
+ type RootLoading = { promise: Promise<RootData>; controller: AbortController; readers: number }
100
+ type Entry = {
101
+ root: Session.Info
102
+ touched: number
103
+ tree?: Data
104
+ audit?: Loading
105
+ metadata?: RootData
106
+ metadataLoading?: RootLoading
107
+ metadataChanges: Map<string, (value: RootData) => void>
108
+ metadataRepair: boolean
109
+ metadataListeners: Set<(value: RootData) => void>
110
+ progressListeners: Set<(value: SessionHistory.ExecutionProgress) => void>
111
+ metadataTimer?: ReturnType<typeof setTimeout>
112
+ trajectoryReaders: number
113
+ }
114
+ const changesFor = (ids: Iterable<string>): Changes => ({
115
+ owners: new Set(ids),
116
+ events: new Map(),
117
+ repair: new Set(),
118
+ })
119
+ function enqueue(changes: Changes, ownerID: string, key: string, apply: (entry: Data) => void) {
120
+ if (changes.repair.has(ownerID)) return
121
+ if (changes.events.size >= 4096) {
122
+ changes.repair.add(ownerID)
123
+ for (const [id, event] of changes.events) if (event.ownerID === ownerID) changes.events.delete(id)
124
+ return
125
+ }
126
+ changes.events.set(key, { ownerID, apply })
127
+ }
128
+ function checkpoint(signal?: AbortSignal) {
129
+ signal?.throwIfAborted()
130
+ }
131
+ const state = RuntimeContext.state(() => ({
132
+ entries: new Map<string, Entry>(),
133
+ subscriptions: new Map<string, { readers: number; trajectory: number }>(),
134
+ legacyClients: 0,
135
+ pending: new Map<string, Loading>(),
136
+ timers: new Map<string, ReturnType<typeof setTimeout>>(),
137
+ publishing: new Set<string>(),
138
+ refreshRequested: new Set<string>(),
139
+ dirty: new Set<string>(),
140
+ sequence: 0,
141
+ schedule: undefined as ((sessionID: string) => void) | undefined,
142
+ dispose: undefined as (() => void) | undefined,
143
+ }))
144
+ function entryFor(root: Session.Info) {
145
+ const cache = state()
146
+ let entry = cache.entries.get(root.id)
147
+ if (!entry) {
148
+ entry = {
149
+ root,
150
+ touched: Date.now(),
151
+ metadataChanges: new Map(),
152
+ metadataRepair: false,
153
+ metadataListeners: new Set(),
154
+ progressListeners: new Set(),
155
+ trajectoryReaders: cache.subscriptions.get(root.id)?.trajectory ?? 0,
156
+ }
157
+ cache.entries.set(root.id, entry)
158
+ }
159
+ return entry
160
+ }
161
+ function evict() {
162
+ const cache = state()
163
+ while (cache.entries.size > 8) {
164
+ const oldest = [...cache.entries.values()]
165
+ .filter(
166
+ (entry) =>
167
+ !cache.pending.has(entry.root.id) &&
168
+ !cache.publishing.has(entry.root.id) &&
169
+ !entry.audit?.readers &&
170
+ !entry.metadataLoading &&
171
+ !entry.metadataListeners.size &&
172
+ !cache.subscriptions.has(entry.root.id),
173
+ )
174
+ .sort((a, b) => a.touched - b.touched)[0]
175
+ if (!oldest) return
176
+ clearTimeout(oldest.metadataTimer)
177
+ oldest.audit?.controller.abort()
178
+ clearTimeout(cache.timers.get(oldest.root.id))
179
+ cache.entries.delete(oldest.root.id)
180
+ cache.dirty.delete(oldest.root.id)
181
+ cache.timers.delete(oldest.root.id)
182
+ cache.refreshRequested.delete(oldest.root.id)
183
+ }
184
+ }
185
+ const nodeID = (sessionID: string, kind: string, id: string) =>
186
+ Buffer.from(JSON.stringify([sessionID, kind, id])).toString("base64url")
187
+ function rootID(record: RolloutEvents.Record) {
188
+ return record.kind === "run" ? record.value.id : record.value.runID
189
+ }
190
+ function nodesFor(snapshot: RolloutSnapshot.Info, session: Session.Info, detail = false) {
191
+ const result: ExecutionSchema.Node[] = []
192
+ const evidence = new Map<string, Evidence>()
193
+ const add = (record: RolloutEvents.Record) => {
194
+ if (
195
+ record.kind === "segment" ||
196
+ record.kind === "interval" ||
197
+ (record.kind === "run" && record.value.admissionOnly)
198
+ )
199
+ return
200
+ const value = record.value
201
+ const runID = rootID(record)
202
+ const parentID =
203
+ record.kind === "run"
204
+ ? session.parentID
205
+ ? record.value.parent?.runID
206
+ ? nodeID(session.parentID, "run", record.value.parent.runID)
207
+ : null
208
+ : null
209
+ : record.kind === "attempt"
210
+ ? nodeID(session.id, "call", record.value.callID)
211
+ : record.kind === "process"
212
+ ? nodeID(session.id, "tool", record.value.toolExecutionID)
213
+ : nodeID(session.id, "run", runID)
214
+ const kind =
215
+ record.kind === "run"
216
+ ? session.parentID
217
+ ? "subtask"
218
+ : "turn"
219
+ : record.kind === "call"
220
+ ? record.value.usageRole === "compaction"
221
+ ? "compaction"
222
+ : "model"
223
+ : record.kind === "attempt"
224
+ ? record.value.index > 0
225
+ ? "retry"
226
+ : "model"
227
+ : record.kind
228
+ const title =
229
+ record.kind === "call"
230
+ ? record.value.model.modelID
231
+ : record.kind === "attempt"
232
+ ? String(record.value.index + 1)
233
+ : record.kind === "tool"
234
+ ? record.value.tool
235
+ : record.kind === "process"
236
+ ? String(record.value.pid ?? record.value.id)
237
+ : (session.cortex?.description ?? session.title)
238
+ const subtitle =
239
+ record.kind === "call"
240
+ ? record.value.purpose
241
+ : record.kind === "tool"
242
+ ? (record.value.error ?? "")
243
+ : record.kind === "attempt"
244
+ ? (record.value.error ?? (record.value.httpStatus ? String(record.value.httpStatus) : ""))
245
+ : ""
246
+ const node = ExecutionSchema.Node.parse({
247
+ id: nodeID(session.id, record.kind, value.id),
248
+ sessionID: session.id,
249
+ runID,
250
+ parentID,
251
+ kind,
252
+ title,
253
+ preview: preview(subtitle),
254
+ started: value.started,
255
+ ended: value.ended,
256
+ status: record.kind === "run" ? (record.value.execution?.status ?? value.status) : value.status,
257
+ revision: snapshot.revision,
258
+ source: "recorded",
259
+ evidenceKind: record.kind,
260
+ ...(record.kind === "run" && session.parentID
261
+ ? {
262
+ attribution: record.value.parent?.runID ? "known" : "unassigned",
263
+ tokens: RolloutAccounting.summarize({
264
+ calls: snapshot.calls.filter((call) => call.runID === runID),
265
+ attempts: snapshot.attempts.filter((attempt) => attempt.runID === runID),
266
+ gaps: snapshot.gaps,
267
+ }).tokens.total,
268
+ }
269
+ : {}),
270
+ ...(record.kind === "tool" ? { messageID: record.value.messageID, tool: record.value.tool } : {}),
271
+ ...(record.kind === "call"
272
+ ? {
273
+ modelID: record.value.model.modelID,
274
+ modelKind: record.value.kind,
275
+ agent: record.value.agent,
276
+ callID: value.id,
277
+ purpose: record.value.purpose,
278
+ usageRole: record.value.usageRole,
279
+ }
280
+ : {}),
281
+ ...(record.kind === "attempt"
282
+ ? { callID: record.value.callID, modelID: record.value.responseModel, attemptIndex: record.value.index }
283
+ : {}),
284
+ })
285
+ result.push(node)
286
+ if (detail) evidence.set(node.id, { owner: snapshot.owner, record })
287
+ }
288
+ for (const value of snapshot.runs) add({ kind: "run", value })
289
+ for (const value of snapshot.calls) add({ kind: "call", value })
290
+ for (const value of snapshot.attempts) add({ kind: "attempt", value })
291
+ for (const value of snapshot.tools) add({ kind: "tool", value })
292
+ for (const value of snapshot.processes) add({ kind: "process", value })
293
+ return { nodes: result, evidence }
294
+ }
295
+ function messageCalls(info: MessageV2.Info, parts: SessionHistory.ExecutionPart[]) {
296
+ const calls = new Map<string, string>()
297
+ if (info.role !== "assistant") return calls
298
+ const fallback =
299
+ info.accounting?.kind === "rollout" && info.accounting.callIDs.length === 1
300
+ ? info.accounting.callIDs[0]
301
+ : undefined
302
+ let next: string | undefined
303
+ for (let index = parts.length - 1; index >= 0; index--) {
304
+ const part = parts[index]
305
+ const call = next ?? fallback
306
+ if (call) calls.set(part.id, call)
307
+ if (part.type === "step-start") next = undefined
308
+ if (part.type === "step-finish")
309
+ next =
310
+ part.accounting?.kind === "rollout" && part.accounting.callIDs.length === 1
311
+ ? part.accounting.callIDs[0]
312
+ : undefined
313
+ }
314
+ return calls
315
+ }
316
+ function normalizedNode(info: MessageV2.Info, part: SessionHistory.ExecutionPart, revision: number, callID?: string) {
317
+ const tool = part.type === "tool" ? part : undefined
318
+ const kind = tool
319
+ ? "tool"
320
+ : part.type === "reasoning"
321
+ ? "reasoning"
322
+ : part.type === "compaction"
323
+ ? "compaction"
324
+ : part.type === "text"
325
+ ? MessageV2.isSystemPart(part) || (info.role === "user" && !info.isRoot)
326
+ ? "context"
327
+ : info.role === "user"
328
+ ? "input"
329
+ : "output"
330
+ : part.type === "attachment"
331
+ ? "context"
332
+ : undefined
333
+ if (!kind) return
334
+ const runID = info.rootID ?? info.id
335
+ const started = part.time?.start ?? info.time.created
336
+ const ended = part.time?.end ?? (info.role === "assistant" ? info.time.completed : info.time.created)
337
+ const status = tool
338
+ ? tool.status === "error"
339
+ ? "failed"
340
+ : tool.status === "pending" || tool.status === "generating"
341
+ ? "unknown"
342
+ : tool.status
343
+ : info.role === "assistant" && info.error
344
+ ? "failed"
345
+ : info.role === "assistant" && !info.time.completed
346
+ ? "running"
347
+ : "completed"
348
+ return ExecutionSchema.Node.parse({
349
+ id: nodeID(info.sessionID, "message", part.id),
350
+ sessionID: info.sessionID,
351
+ runID,
352
+ parentID: callID ? nodeID(info.sessionID, "call", callID) : nodeID(info.sessionID, "run", runID),
353
+ kind,
354
+ title: tool?.tool ?? part.preview,
355
+ preview: part.preview,
356
+ started,
357
+ ended,
358
+ status,
359
+ revision,
360
+ messageID: info.id,
361
+ tool: tool?.tool,
362
+ agent: info.agent,
363
+ source: "messages",
364
+ })
365
+ }
366
+ async function appendMessages(
367
+ session: Session.Info,
368
+ snapshot: RolloutSnapshot.Info,
369
+ raw: Awaited<ReturnType<typeof SessionHistory.executionMessages>>,
370
+ nodes: ExecutionSchema.Node[],
371
+ contexts: Data["contexts"],
372
+ signal?: AbortSignal,
373
+ ) {
374
+ const tools = toolIndex(nodes, [snapshot])
375
+ let processed = 0
376
+ for (const message of raw) {
377
+ rememberContext(contexts, message.info)
378
+ const calls = messageCalls(message.info, message.parts)
379
+ for (const part of message.parts) {
380
+ if (++processed % 256 === 0) {
381
+ await Bun.sleep(0)
382
+ checkpoint(signal)
383
+ }
384
+ if (part.type === "tool") {
385
+ const tool = tools.get(JSON.stringify([session.id, message.info.id, part.callID]))
386
+ if (tool) {
387
+ tool.preview = part.preview
388
+ const callID = calls.get(part.id)
389
+ if (callID && snapshot.calls.some((call) => call.id === callID))
390
+ tool.parentID = nodeID(session.id, "call", callID)
391
+ tool.agent = message.info.agent
392
+ continue
393
+ }
394
+ }
395
+ const node = normalizedNode(message.info, part, 0, calls.get(part.id))
396
+ if (!node) continue
397
+ nodes.push(node)
398
+ }
399
+ }
400
+ }
401
+ function rememberContext(contexts: Data["contexts"], info: MessageV2.Info, changed?: Set<string>) {
402
+ if (info.role !== "assistant" || !info.contextUsage || info.accounting?.kind !== "rollout") return
403
+ const callID = info.contextUsage.callID ?? info.accounting.callIDs.at(-1)
404
+ if (callID && info.accounting.callIDs.includes(callID)) {
405
+ contexts.set(callID, info.contextUsage)
406
+ changed?.add(nodeID(info.sessionID, "call", callID))
407
+ }
408
+ }
409
+ function selected(data: Data, runID?: string) {
410
+ if (!runID) return data.nodes
411
+ const allowed = new Set<string>([data.root.id + ":" + runID])
412
+ for (const snapshot of data.snapshots) {
413
+ for (const run of snapshot.runs) {
414
+ if (
415
+ run.parent?.owner.kind === "session" &&
416
+ run.parent.runID &&
417
+ allowed.has(run.parent.owner.sessionID + ":" + run.parent.runID)
418
+ )
419
+ allowed.add(snapshot.owner.kind === "session" ? snapshot.owner.sessionID + ":" + run.id : "")
420
+ }
421
+ }
422
+ return data.nodes.filter((node) => allowed.has(node.sessionID + ":" + node.runID))
423
+ }
424
+ async function build(
425
+ root: Session.Info,
426
+ changes = changesFor([root.id]),
427
+ signal?: AbortSignal,
428
+ descendants = true,
429
+ ): Promise<Data> {
430
+ checkpoint(signal)
431
+ ObservabilityMetrics.record({
432
+ name: descendants ? "execution.tree.build" : "execution.owner.build",
433
+ value: 1,
434
+ unit: "count",
435
+ module: "session",
436
+ })
437
+ const sessions = [root]
438
+ const seen = new Set([root.id])
439
+ for (let index = 0; descendants && index < sessions.length; index++) {
440
+ checkpoint(signal)
441
+ for (const child of await Session.children(sessions[index].id)) {
442
+ if (seen.has(child.id)) continue
443
+ seen.add(child.id)
444
+ changes.owners.add(child.id)
445
+ sessions.push(child)
446
+ }
447
+ }
448
+ checkpoint(signal)
449
+ const presentation = await ObservabilitySpans.measure(
450
+ { name: "execution.snapshot", module: "session", kind: "session" },
451
+ () =>
452
+ RolloutSnapshot.presentationAll(
453
+ sessions.map((session) => ({
454
+ kind: "session",
455
+ scopeID: session.scope.id,
456
+ sessionID: session.id,
457
+ })),
458
+ signal,
459
+ ),
460
+ )
461
+ const snapshots = presentation.map((value) => value.snapshot)
462
+ const nodes: ExecutionSchema.Node[] = []
463
+ const contexts: Data["contexts"] = new Map()
464
+ for (const [index, session] of sessions.entries()) {
465
+ checkpoint(signal)
466
+ const snapshot = snapshots[index]
467
+ const projected = nodesFor(snapshot, session)
468
+ nodes.push(...projected.nodes)
469
+ const raw = await ObservabilitySpans.measure(
470
+ { name: "execution.messages", module: "session", kind: "session" },
471
+ () =>
472
+ SessionHistory.executionMessages({
473
+ sessionID: session.id,
474
+ signal,
475
+ progress: (value) => {
476
+ for (const listener of state().entries.get(root.id)?.progressListeners ?? []) listener(value)
477
+ },
478
+ }),
479
+ )
480
+ await appendMessages(session, snapshot, raw, nodes, contexts, signal)
481
+ }
482
+ nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id))
483
+ const usage = new Map<string, UsageSchema.Record>()
484
+ for (const record of await ObservabilitySpans.measure(
485
+ { name: "execution.usage", module: "session", kind: "session" },
486
+ async () =>
487
+ (
488
+ await Promise.all(
489
+ sessions.map((session) =>
490
+ Usage.collect({ scopeID: session.scope.id, sessionID: session.id, includeDescendants: false }, signal),
491
+ ),
492
+ )
493
+ ).flat(),
494
+ ))
495
+ usage.set(record.id, record)
496
+ const inboxes = new Map(
497
+ await Promise.all(sessions.map(async (session) => [session.id, await SessionInbox.list(session.id)] as const)),
498
+ )
499
+ const revision = ++state().sequence
500
+ const data = {
501
+ root,
502
+ sessions,
503
+ snapshots,
504
+ verified: new Set(sessions.filter((_, index) => presentation[index].verified).map((session) => session.id)),
505
+ nodes,
506
+ sessionsByID: new Map(sessions.map((session) => [session.id, session])),
507
+ snapshotsByID: new Map(
508
+ snapshots.map((snapshot) => [snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "", snapshot]),
509
+ ),
510
+ usage,
511
+ contexts,
512
+ inboxes,
513
+ revision,
514
+ touched: Date.now(),
515
+ pendingMessages: new Map<string, string>(),
516
+ changed: new Set<string>(),
517
+ removed: new Set<string>(),
518
+ refreshOwners: new Set<string>(),
519
+ addedSessions: new Map<string, Session.Info>(),
520
+ changes,
521
+ selections: new Map<string, { summary: ExecutionSchema.Summary; touched: number }>(),
522
+ processRows: new Map<string, ExecutionSchema.Node>(),
523
+ recordRows: new Map<string, ExecutionSchema.Node>(),
524
+ contextRows: new Map<string, string>(),
525
+ publishedRevision: revision,
526
+ derived: new Map<string, Derived>(),
527
+ } as Omit<Data, "summary">
528
+ const value = { ...data, summary: summarize(data) }
529
+ value.contextRows = new Map(contextSnapshots(value).map((context) => [context.nodeID, JSON.stringify(context)]))
530
+ return value
531
+ }
532
+ function derived(data: Omit<Data, "summary">, runID?: string) {
533
+ const key = runID ?? ""
534
+ const cached = data.derived.get(key)
535
+ if (cached?.revision === data.revision) return cached
536
+ for (const [id, value] of data.derived) if (value.revision !== data.revision) data.derived.delete(id)
537
+ const value: Derived = { revision: data.revision, nodes: selected(data as Data, runID), modes: new Map() }
538
+ data.derived.set(key, value)
539
+ while (data.derived.size > 5) data.derived.delete([...data.derived.keys()].find((id) => id !== "")!)
540
+ return value
541
+ }
542
+ function activityFor(data: Omit<Data, "summary">, runID?: string) {
543
+ const value = derived(data, runID)
544
+ return (value.activity ??= ExecutionActivity.project(value.nodes, data.root.id))
545
+ }
546
+ function processRows(data: Data, mode: "process" | "records", runID?: string) {
547
+ const value = derived(data, runID)
548
+ let rows = value.modes.get(mode)
549
+ if (!rows) {
550
+ rows = ExecutionProcess.project(value.nodes, mode, data.root.id)
551
+ value.modes.set(mode, rows)
552
+ }
553
+ return rows
554
+ }
555
+ function summarize(data: Omit<Data, "summary">, runID?: string): ExecutionSchema.Summary {
556
+ const cached = derived(data, runID)
557
+ const sample = RolloutExecution.clock()
558
+ if (cached.prepared) {
559
+ const { summary, timings } = cached.prepared
560
+ const timing = (key: string) => {
561
+ const value = RolloutExecution.summarize(timings.get(key)!, sample)
562
+ return { ...value, status: ExecutionSchema.Task.shape.status.parse(value.status) }
563
+ }
564
+ return {
565
+ ...summary,
566
+ computedAt: Date.now(),
567
+ clockID: sample.clockID,
568
+ sampledAt: sample.now,
569
+ ...timing("root"),
570
+ tasks: summary.tasks.map((task) => ({ ...task, ...timing("task:" + task.sessionID) })),
571
+ rounds: summary.rounds.map((round) =>
572
+ timings.has("round:" + round.id) ? { ...round, ...timing("round:" + round.id) } : round,
573
+ ),
574
+ }
575
+ }
576
+ const nodes = cached.nodes
577
+ const identities = new Set(nodes.map((node) => node.sessionID + ":" + node.runID))
578
+ const snapshots = data.snapshots.map((snapshot) => ({
579
+ ...snapshot,
580
+ runs: snapshot.runs.filter((run) =>
581
+ identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + run.id),
582
+ ),
583
+ calls: snapshot.calls.filter((call) =>
584
+ identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + call.runID),
585
+ ),
586
+ intervals: snapshot.intervals.filter((interval) =>
587
+ identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + interval.runID),
588
+ ),
589
+ attempts: snapshot.attempts.filter((attempt) =>
590
+ identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + attempt.runID),
591
+ ),
592
+ }))
593
+ const timings = new Map<string, TimingInput>()
594
+ const projection = (
595
+ scope: RolloutSnapshot.Info[],
596
+ rootRuns = scope[0].runs,
597
+ selectedRunID?: string,
598
+ key = "root",
599
+ ) => {
600
+ const input: TimingInput = {
601
+ roots: rootRuns,
602
+ runs: scope.flatMap((snapshot) => snapshot.runs),
603
+ intervals: scope.flatMap((snapshot) => snapshot.intervals),
604
+ segments: scope.flatMap((snapshot) => snapshot.segments),
605
+ hasHistory: nodes.some(
606
+ (node) =>
607
+ scope.some(
608
+ (snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === node.sessionID,
609
+ ) &&
610
+ (!selectedRunID || node.rootRunID === selectedRunID || node.runID === selectedRunID) &&
611
+ ["output", "model", "tool"].includes(node.kind),
612
+ ),
613
+ paused: data.sessions.some((candidate) => {
614
+ if (!candidate.paused) return false
615
+ const selected = scope.find(
616
+ (snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id,
617
+ )
618
+ if (!selected) return false
619
+ if (!selectedRunID && !runID) return true
620
+ const latest = data.snapshots
621
+ .find((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id)
622
+ ?.runs.filter((run) => !run.admissionOnly)
623
+ .toSorted((a, b) => b.started - a.started || b.id.localeCompare(a.id))[0]
624
+ return (
625
+ !!latest &&
626
+ ["running", "interrupted"].includes(latest.execution?.status ?? latest.status) &&
627
+ selected.runs.some((run) => run.id === latest.id)
628
+ )
629
+ }),
630
+ queued: data.sessions.some(
631
+ (candidate) =>
632
+ scope.some((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id) &&
633
+ ((!selectedRunID && candidate.cortex?.status === "queued") ||
634
+ (data.inboxes.get(candidate.id) ?? []).some(
635
+ (item) => item.mode === "task" && !item.status && (!selectedRunID || item.messageID === selectedRunID),
636
+ )),
637
+ ),
638
+ }
639
+ timings.set(key, input)
640
+ return RolloutExecution.summarize(input, sample)
641
+ }
642
+ const subtree = (sessionID: string) => {
643
+ const ids = new Set([sessionID])
644
+ for (let size = -1; size !== ids.size;) {
645
+ size = ids.size
646
+ for (const session of data.sessions) if (session.parentID && ids.has(session.parentID)) ids.add(session.id)
647
+ }
648
+ return snapshots.filter((snapshot) => snapshot.owner.kind === "session" && ids.has(snapshot.owner.sessionID))
649
+ }
650
+ const accounting = snapshots.map(RolloutAccounting.summarize)
651
+ const usage = Usage.summarize(
652
+ [...data.usage.values()].filter(
653
+ (record) =>
654
+ !runID || (record.owner.kind === "session" && identities.has(record.owner.sessionID + ":" + record.runID)),
655
+ ),
656
+ { sessionID: data.root.id, runID, includeDescendants: true },
657
+ )
658
+ const runs = snapshots.flatMap((snapshot) => snapshot.runs)
659
+ const roots = snapshots[0].runs
660
+ const context = Usage.summarize(
661
+ [...data.usage.values()].filter(
662
+ (record) =>
663
+ record.owner.kind === "session" &&
664
+ record.owner.sessionID === data.root.id &&
665
+ (!runID || record.runID === runID),
666
+ ),
667
+ { sessionID: data.root.id, runID, includeDescendants: false },
668
+ )
669
+ const start = nodes.reduce((value, node) => Math.min(value, node.started), Date.now())
670
+ const end = nodes.reduce((value, node) => Math.max(value, node.ended ?? node.started), start + 1)
671
+ const distribution = context.context && data.contexts.get(context.context.callID)
672
+ const activity = activityFor(data, runID)
673
+ const activities = activity.nodes
674
+ const summary = ExecutionSchema.Summary.parse({
675
+ sessionID: data.root.id,
676
+ revision: data.revision,
677
+ runID,
678
+ computedAt: Date.now(),
679
+ clockID: sample.clockID,
680
+ sampledAt: sample.now,
681
+ ...projection(snapshots, roots, runID),
682
+ accounting: usage.accounting,
683
+ cost: ExecutionPresentation.cost(usage.accounting),
684
+ own: usage.own,
685
+ descendants: usage.descendants,
686
+ rates: usage.rates,
687
+ cache: usage.cache,
688
+ latency: usage.latency,
689
+ outcomes: usage.outcomes,
690
+ tools: usage.tools,
691
+ context: context.context,
692
+ contextDistribution:
693
+ distribution &&
694
+ distribution.modelID === context.context?.modelID &&
695
+ distribution.totalInput === context.context.inputTokens
696
+ ? distribution
697
+ : null,
698
+ tasks: data.sessions
699
+ .slice(1)
700
+ .map((session, index) => ({
701
+ sessionID: session.id,
702
+ parentID: session.parentID ?? null,
703
+ title: session.cortex?.description ?? session.title,
704
+ nodeID: nodes.find((node) => node.kind === "subtask" && node.sessionID === session.id)?.id ?? null,
705
+ ...projection(subtree(session.id), snapshots[index + 1].runs, undefined, "task:" + session.id),
706
+ tokens: accounting[index + 1].tokens.total,
707
+ runs: snapshots[index + 1].runs.map((run) => run.id),
708
+ interaction: session.interaction,
709
+ cortex: session.cortex && {
710
+ taskID: session.cortex.taskID,
711
+ agent: session.cortex.agent,
712
+ status: session.cortex.status,
713
+ visibility: session.cortex.visibility,
714
+ },
715
+ }))
716
+ .filter((task) => !runID || task.runs.length),
717
+ rounds: [
718
+ ...data.snapshots[0].runs
719
+ .filter((run) => !run.admissionOnly)
720
+ .map((run) => {
721
+ const roundIdentities = new Set(
722
+ selected(data as Data, run.id).map((node) => node.sessionID + ":" + node.runID),
723
+ )
724
+ return {
725
+ id: run.id,
726
+ title:
727
+ data.nodes.find(
728
+ (node) => node.runID === run.id && node.sessionID === data.root.id && node.kind === "input",
729
+ )?.preview ?? run.id,
730
+ started: run.started,
731
+ ...projection(
732
+ data.snapshots.map((snapshot) => ({
733
+ ...snapshot,
734
+ runs: snapshot.runs.filter(
735
+ (candidate) =>
736
+ snapshot.owner.kind === "session" &&
737
+ roundIdentities.has(snapshot.owner.sessionID + ":" + candidate.id),
738
+ ),
739
+ intervals: snapshot.intervals.filter(
740
+ (interval) =>
741
+ snapshot.owner.kind === "session" &&
742
+ roundIdentities.has(snapshot.owner.sessionID + ":" + interval.runID),
743
+ ),
744
+ })),
745
+ [run],
746
+ run.id,
747
+ "round:" + run.id,
748
+ ),
749
+ }
750
+ }),
751
+ ...data.nodes
752
+ .filter(
753
+ (node) =>
754
+ node.sessionID === data.root.id &&
755
+ node.kind === "input" &&
756
+ !data.snapshots[0].runs.some((run) => run.id === node.runID),
757
+ )
758
+ .filter((node, index, all) => all.findIndex((other) => other.runID === node.runID) === index)
759
+ .map((node) => ({
760
+ id: node.runID,
761
+ title: node.preview,
762
+ started: node.started,
763
+ status: "unknown",
764
+ elapsedMs: 0,
765
+ elapsedActive: false,
766
+ elapsedLowerBound: true,
767
+ })),
768
+ ],
769
+ coverage: {
770
+ recorded: nodes.filter((node) => node.source === "recorded").length,
771
+ messages: nodes.filter((node) => node.source === "messages").length,
772
+ gaps:
773
+ data.verified.size === data.sessions.length
774
+ ? data.snapshots.reduce((sum, snapshot) => sum + snapshot.gaps.length, 0)
775
+ : null,
776
+ partial:
777
+ data.verified.size !== data.sessions.length ||
778
+ runs.some((run) => run.recording !== "complete") ||
779
+ (!runs.length && !!nodes.length),
780
+ },
781
+ activityTotal: activities.length,
782
+ humanInputs: activity.humanInputs,
783
+ taskInstructions: activity.taskInstructions,
784
+ activitySegments: activity.segments,
785
+ lanes: (["input", "model", "tool"] as const).map((kind) => ({
786
+ kind,
787
+ start,
788
+ end,
789
+ ...(() => {
790
+ const lane = activities.filter((node) =>
791
+ kind === "input"
792
+ ? node.kind === "input" || node.kind === "subtask"
793
+ : kind === "model"
794
+ ? node.evidenceKind === "call" || node.evidenceKind === "attempt"
795
+ : node.kind === "tool",
796
+ )
797
+ return { total: lane.length, nodes: ExecutionActivity.aggregate(activities, kind) }
798
+ })(),
799
+ })),
800
+ })
801
+ cached.prepared = { summary, timings }
802
+ return summary
803
+ }
804
+ async function requireRoot(sessionID: string) {
805
+ const session = await Session.get(sessionID)
806
+ if (session.scope.id !== ScopeContext.current.scope.id)
807
+ throw new Storage.NotFoundError({ message: "Session is outside the selected Scope" })
808
+ return session
809
+ }
810
+ function cancelUnused(sessionID: string, loading: Loading) {
811
+ const cache = state()
812
+ if (!loading.readers && !cache.subscriptions.has(sessionID) && !cache.legacyClients) loading.controller.abort()
813
+ }
814
+ async function consume(sessionID: string, loading: Loading, signal?: AbortSignal) {
815
+ checkpoint(signal)
816
+ loading.readers++
817
+ let abort: (() => void) | undefined
818
+ try {
819
+ if (!signal) return await loading.promise
820
+ return await Promise.race([
821
+ loading.promise,
822
+ new Promise<never>((_, reject) => {
823
+ abort = () => reject(signal.reason)
824
+ signal.addEventListener("abort", abort, { once: true })
825
+ if (signal.aborted) abort()
826
+ }),
827
+ ])
828
+ } finally {
829
+ if (abort) signal?.removeEventListener("abort", abort)
830
+ loading.readers--
831
+ cancelUnused(sessionID, loading)
832
+ }
833
+ }
834
+ async function repair(entry: Data, signal?: AbortSignal) {
835
+ const owners = [...entry.refreshOwners]
836
+ entry.refreshOwners.clear()
837
+ const added = [...entry.addedSessions.values()]
838
+ entry.addedSessions.clear()
839
+ const sessions = new Map(added.map((info) => [info.id, info]))
840
+ for (const id of owners) sessions.set(id, await requireRoot(id))
841
+ const repaired = new Set<string>()
842
+ for (const session of sessions.values()) {
843
+ if (repaired.has(session.id)) continue
844
+ checkpoint(signal)
845
+ const branch = await build(session, entry.changes, signal, !owners.includes(session.id))
846
+ const replaced = new Set(branch.sessions.map((item) => item.id))
847
+ for (const id of replaced) repaired.add(id)
848
+ for (const node of entry.nodes)
849
+ if (replaced.has(node.sessionID)) {
850
+ entry.removed.add(node.id)
851
+ }
852
+ entry.nodes = entry.nodes.filter((node) => !replaced.has(node.sessionID))
853
+ for (const [index, info] of branch.sessions.entries()) {
854
+ if (branch.verified.has(info.id)) entry.verified.add(info.id)
855
+ else entry.verified.delete(info.id)
856
+ const position = entry.sessions.findIndex((item) => item.id === info.id)
857
+ if (position < 0) {
858
+ entry.sessions.push(info)
859
+ entry.snapshots.push(branch.snapshots[index])
860
+ } else {
861
+ entry.sessions[position] = info
862
+ entry.snapshots[position] = branch.snapshots[index]
863
+ }
864
+ }
865
+ for (const [id, record] of entry.usage)
866
+ if (record.owner.kind === "session" && replaced.has(record.owner.sessionID)) entry.usage.delete(id)
867
+ for (const node of branch.nodes) {
868
+ entry.nodes.push(node)
869
+ entry.changed.add(node.id)
870
+ entry.removed.delete(node.id)
871
+ }
872
+ for (const [id, value] of branch.usage) entry.usage.set(id, value)
873
+ for (const [id, value] of branch.contexts) entry.contexts.set(id, value)
874
+ for (const [id, value] of branch.inboxes) entry.inboxes.set(id, value)
875
+ }
876
+ }
877
+ async function data(sessionID: string, access = true, signal?: AbortSignal): Promise<Data> {
878
+ checkpoint(signal)
879
+ const shared = state().pending.get(sessionID)
880
+ if (shared && shared.scopeID === ScopeContext.current.scope.id) {
881
+ if (!shared.controller.signal.aborted) return consume(sessionID, shared, signal)
882
+ await consume(sessionID, shared, signal).catch(() => checkpoint(signal))
883
+ return data(sessionID, access, signal)
884
+ }
885
+ const root = await requireRoot(sessionID)
886
+ checkpoint(signal)
887
+ init()
888
+ const cache = state()
889
+ const entry = entryFor(root)
890
+ const existing = entry.tree
891
+ if (access) entry.touched = Date.now()
892
+ if (existing && access) existing.touched = entry.touched
893
+ const pending = cache.pending.get(sessionID)
894
+ if (pending) {
895
+ if (!pending.controller.signal.aborted) return consume(sessionID, pending, signal)
896
+ await consume(sessionID, pending, signal).catch(() => checkpoint(signal))
897
+ return data(sessionID, access, signal)
898
+ }
899
+ if (existing && !cache.dirty.has(sessionID)) return existing
900
+ const changes = existing?.changes ?? changesFor([sessionID])
901
+ const controller = new AbortController()
902
+ const loading: Loading = {
903
+ scopeID: root.scope.id,
904
+ changes,
905
+ controller,
906
+ readers: 0,
907
+ promise: Promise.resolve()
908
+ .then(async () => {
909
+ const value = existing ?? (await build(root, changes, controller.signal))
910
+ checkpoint(controller.signal)
911
+ // Detach a finite revision cut; arrivals during reads belong to the next refresh.
912
+ const events = changes.events
913
+ const repairs = changes.repair
914
+ changes.events = new Map()
915
+ changes.repair = new Set()
916
+ let applied = 0
917
+ for (const event of events.values()) {
918
+ event.apply(value)
919
+ if (++applied % 256 === 0) {
920
+ await Bun.sleep(0)
921
+ checkpoint(controller.signal)
922
+ }
923
+ }
924
+ for (const owner of repairs) value.refreshOwners.add(owner)
925
+ await repair(value, controller.signal)
926
+ if (existing || events.size || repairs.size) await flush(value, controller.signal)
927
+ checkpoint(controller.signal)
928
+ entry.tree = value
929
+ if (changes.events.size || changes.repair.size) cache.dirty.add(sessionID)
930
+ else cache.dirty.delete(sessionID)
931
+ evict()
932
+ return value
933
+ })
934
+ .catch((error) => {
935
+ for (const owner of changes.owners) changes.repair.add(owner)
936
+ cache.dirty.add(sessionID)
937
+ throw error
938
+ })
939
+ .finally(() => {
940
+ cache.pending.delete(sessionID)
941
+ if (cache.dirty.has(sessionID)) cache.schedule?.(sessionID)
942
+ }),
943
+ }
944
+ cache.pending.set(sessionID, loading)
945
+ return consume(sessionID, loading, signal)
946
+ }
947
+ function toolIndex(nodes: ExecutionSchema.Node[], snapshots: RolloutSnapshot.Info[]) {
948
+ const result = new Map<string, ExecutionSchema.Node>()
949
+ const records = new Map(
950
+ snapshots.flatMap((snapshot) =>
951
+ snapshot.tools.map(
952
+ (tool) => [nodeID(tool.owner.kind === "session" ? tool.owner.sessionID : "", "tool", tool.id), tool] as const,
953
+ ),
954
+ ),
955
+ )
956
+ for (const node of nodes) {
957
+ const record = records.get(node.id)
958
+ if (record) result.set(JSON.stringify([node.sessionID, record.messageID, record.toolCallID]), node)
959
+ }
960
+ return result
961
+ }
962
+ function update<T extends { id: string }>(values: T[], value: T) {
963
+ const index = values.findIndex((item) => item.id === value.id)
964
+ if (index < 0) values.push(value)
965
+ else values[index] = value
966
+ }
967
+ function apply(entry: Data, owner: RolloutSchema.Owner, revision: number, record: RolloutEvents.Record) {
968
+ if (owner.kind !== "session") return
969
+ const snapshot = entry.snapshotsByID.get(owner.sessionID)
970
+ const session = entry.sessionsByID.get(owner.sessionID)
971
+ if (!snapshot || !session || revision <= snapshot.revision) return
972
+ if (revision !== snapshot.revision + 1) {
973
+ entry.refreshOwners.add(session.id)
974
+ return
975
+ }
976
+ snapshot.revision = revision
977
+ entry.detail = undefined
978
+ if (record.kind === "run") update(snapshot.runs, record.value)
979
+ if (record.kind === "call") update(snapshot.calls, record.value)
980
+ if (record.kind === "attempt") update(snapshot.attempts, record.value)
981
+ if (record.kind === "segment") update(snapshot.segments, record.value)
982
+ if (record.kind === "interval") {
983
+ update(snapshot.intervals, record.value)
984
+ entry.changed.add(nodeID(session.id, "run", record.value.runID))
985
+ }
986
+ if (record.kind === "run" && record.value.admissionOnly) {
987
+ const id = nodeID(session.id, "run", record.value.id)
988
+ entry.nodes = entry.nodes.filter((node) => node.id !== id)
989
+ entry.removed.add(id)
990
+ }
991
+ if (record.kind === "tool") update(snapshot.tools, record.value)
992
+ if (record.kind === "process") update(snapshot.processes, record.value)
993
+ const projected = nodesFor(
994
+ {
995
+ ...snapshot,
996
+ runs: record.kind === "run" ? [record.value] : [],
997
+ calls: record.kind === "call" ? [record.value] : [],
998
+ attempts: record.kind === "attempt" ? [record.value] : [],
999
+ tools: record.kind === "tool" ? [record.value] : [],
1000
+ processes: record.kind === "process" ? [record.value] : [],
1001
+ },
1002
+ session,
1003
+ )
1004
+ for (const node of projected.nodes) {
1005
+ const position = entry.nodes.findIndex((value) => value.id === node.id)
1006
+ const old = position < 0 ? undefined : entry.nodes[position]
1007
+ if (node.kind === "tool" && !node.preview && old?.preview) node.preview = old.preview
1008
+ if (
1009
+ node.kind === "tool" &&
1010
+ old?.parentID &&
1011
+ snapshot.calls.some((call) => nodeID(session.id, "call", call.id) === old.parentID)
1012
+ )
1013
+ node.parentID = old.parentID
1014
+ if (node.kind === "tool" && old?.agent) node.agent = old.agent
1015
+ if (position < 0) {
1016
+ entry.nodes.push(node)
1017
+ } else entry.nodes[position] = node
1018
+ entry.changed.add(node.id)
1019
+ }
1020
+ }
1021
+ async function flush(entry: Data, signal?: AbortSignal) {
1022
+ entry.detail = undefined
1023
+ const messages = entry.pendingMessages
1024
+ entry.pendingMessages = new Map()
1025
+ const tools = toolIndex(entry.nodes, entry.snapshots)
1026
+ entry.nodes = entry.nodes.filter((node) => {
1027
+ if (node.source !== "messages" || !node.messageID || !messages.has(node.messageID)) return true
1028
+ entry.removed.add(node.id)
1029
+ return false
1030
+ })
1031
+ let processed = 0
1032
+ for (const [messageID, sessionID] of messages) {
1033
+ checkpoint(signal)
1034
+ const message = await SessionHistory.executionMessages({ sessionID, messageID, signal })
1035
+ .then((messages) => messages[0])
1036
+ .catch((error) => {
1037
+ if (error instanceof Storage.NotFoundError) return undefined
1038
+ throw error
1039
+ })
1040
+ if (!message) continue
1041
+ const info = MessageV2.deriveSemantics([message])[0].info
1042
+ rememberContext(entry.contexts, info, entry.changed)
1043
+ const calls = messageCalls(info, message.parts)
1044
+ for (const part of message.parts) {
1045
+ if (++processed % 256 === 0) {
1046
+ await Bun.sleep(0)
1047
+ checkpoint(signal)
1048
+ }
1049
+ if (part.type === "tool") {
1050
+ const node = tools.get(JSON.stringify([sessionID, messageID, part.callID]))
1051
+ if (node) {
1052
+ node.preview = part.preview
1053
+ const callID = calls.get(part.id)
1054
+ if (callID && entry.snapshotsByID.get(sessionID)?.calls.some((call) => call.id === callID))
1055
+ node.parentID = nodeID(sessionID, "call", callID)
1056
+ node.agent = info.agent
1057
+ entry.changed.add(node.id)
1058
+ continue
1059
+ }
1060
+ }
1061
+ const node = normalizedNode(info, part, entry.revision + 1, calls.get(part.id))
1062
+ if (!node) continue
1063
+ entry.nodes.push(node)
1064
+ entry.changed.add(node.id)
1065
+ entry.removed.delete(node.id)
1066
+ }
1067
+ }
1068
+ checkpoint(signal)
1069
+ entry.nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id))
1070
+ entry.sessionsByID = new Map(entry.sessions.map((session) => [session.id, session]))
1071
+ entry.snapshotsByID = new Map(
1072
+ entry.snapshots.map((snapshot) => [snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "", snapshot]),
1073
+ )
1074
+ entry.revision = ++state().sequence
1075
+ entry.summary = summarize(entry)
1076
+ for (const [runID, selection] of entry.selections) {
1077
+ selection.summary = summarize(entry, runID)
1078
+ }
1079
+ return entry
1080
+ }
1081
+ export function subscribe(sessionID: string, options?: { trajectory?: boolean }) {
1082
+ const cache = state()
1083
+ const entry = cache.entries.get(sessionID)
1084
+ const trajectory = options?.trajectory !== false
1085
+ if (entry && trajectory) entry.trajectoryReaders++
1086
+ const subscription = cache.subscriptions.get(sessionID) ?? { readers: 0, trajectory: 0 }
1087
+ subscription.readers++
1088
+ if (trajectory) subscription.trajectory++
1089
+ cache.subscriptions.set(sessionID, subscription)
1090
+ let released = false
1091
+ return () => {
1092
+ if (released) return
1093
+ released = true
1094
+ const retained = cache.entries.get(sessionID)
1095
+ if (retained && trajectory) retained.trajectoryReaders--
1096
+ subscription.readers--
1097
+ if (trajectory) subscription.trajectory--
1098
+ if (subscription.readers) cache.subscriptions.set(sessionID, subscription)
1099
+ else {
1100
+ cache.subscriptions.delete(sessionID)
1101
+ const loading = cache.pending.get(sessionID)
1102
+ if (loading) cancelUnused(sessionID, loading)
1103
+ if (!cache.legacyClients) {
1104
+ clearTimeout(cache.timers.get(sessionID))
1105
+ cache.timers.delete(sessionID)
1106
+ if (!cache.publishing.has(sessionID)) cache.refreshRequested.delete(sessionID)
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ export function legacyClient() {
1112
+ const cache = state()
1113
+ cache.legacyClients++
1114
+ let released = false
1115
+ return () => {
1116
+ if (released) return
1117
+ released = true
1118
+ cache.legacyClients--
1119
+ for (const [sessionID, loading] of cache.pending) cancelUnused(sessionID, loading)
1120
+ if (!cache.legacyClients)
1121
+ for (const [sessionID, timer] of cache.timers)
1122
+ if (!cache.subscriptions.has(sessionID)) {
1123
+ clearTimeout(timer)
1124
+ cache.timers.delete(sessionID)
1125
+ if (!cache.publishing.has(sessionID)) cache.refreshRequested.delete(sessionID)
1126
+ }
1127
+ }
1128
+ }
1129
+ export function init() {
1130
+ const cache = state()
1131
+ if (cache.dispose) return cache.dispose
1132
+ const schedule = (sessionID: string) => {
1133
+ for (const owner of cache.entries.values()) {
1134
+ const entry = owner.tree
1135
+ if (!entry) continue
1136
+ if (!entry.changes.owners.has(sessionID)) continue
1137
+ cache.dirty.add(entry.root.id)
1138
+ if (!cache.subscriptions.has(entry.root.id) && !cache.legacyClients) continue
1139
+ cache.refreshRequested.add(entry.root.id)
1140
+ if (
1141
+ (!cache.subscriptions.has(entry.root.id) && Date.now() - entry.touched > 600_000) ||
1142
+ cache.timers.has(entry.root.id) ||
1143
+ cache.publishing.has(entry.root.id)
1144
+ )
1145
+ continue
1146
+ const timer = setTimeout(async () => {
1147
+ cache.timers.delete(entry.root.id)
1148
+ if (!cache.subscriptions.has(entry.root.id) && !cache.legacyClients) return
1149
+ cache.publishing.add(entry.root.id)
1150
+ cache.refreshRequested.delete(entry.root.id)
1151
+ await ScopeContext.provide({
1152
+ scope: entry.root.scope,
1153
+ fn: async () => {
1154
+ const previousRevision = entry.publishedRevision
1155
+ const next = await data(entry.root.id, false)
1156
+ const revision = next.revision
1157
+ const detail = owner.trajectoryReaders > 0 || cache.legacyClients > 0
1158
+ const process = detail ? processRows(next, "process") : []
1159
+ const processUpserts = process.filter(
1160
+ (node) =>
1161
+ JSON.stringify({ ...node, revision: 0 }) !==
1162
+ JSON.stringify({ ...entry.processRows.get(node.id), revision: 0 }),
1163
+ )
1164
+ const processIDs = new Set(process.map((node) => node.id))
1165
+ const processRemoved = [...entry.processRows.keys()].filter((id) => !processIDs.has(id))
1166
+ const records = detail ? processRows(next, "records") : []
1167
+ const upserts = records.filter(
1168
+ (node) =>
1169
+ JSON.stringify({ ...node, revision: 0 }) !==
1170
+ JSON.stringify({ ...entry.recordRows.get(node.id), revision: 0 }),
1171
+ )
1172
+ const ids = new Set(next.nodes.map((node) => node.id))
1173
+ const contexts = contextSnapshots(next).filter(
1174
+ (context) =>
1175
+ (next !== entry || next.changed.has(context.nodeID)) &&
1176
+ JSON.stringify(context) !== next.contextRows.get(context.nodeID),
1177
+ )
1178
+ await Bus.publish(ExecutionSchema.Updated, {
1179
+ sessionID: entry.root.id,
1180
+ revision,
1181
+ previousRevision,
1182
+ summary: next.summary,
1183
+ roundSummaries: [...next.selections.values()].map((selection) => selection.summary),
1184
+ contextUpserts: contexts,
1185
+ upserts: upserts.map((node) => ({ ...node, revision })),
1186
+ processUpserts: processUpserts.map((node) => ({ ...node, revision })),
1187
+ processRemoved,
1188
+ removed:
1189
+ next === entry
1190
+ ? [...next.removed]
1191
+ : entry.nodes.filter((node) => !ids.has(node.id)).map((node) => node.id),
1192
+ })
1193
+ next.processRows = new Map(process.map((node) => [node.id, node]))
1194
+ next.recordRows = new Map(records.map((node) => [node.id, node]))
1195
+ for (const context of contexts) next.contextRows.set(context.nodeID, JSON.stringify(context))
1196
+ for (const id of next.contextRows.keys()) if (!ids.has(id)) next.contextRows.delete(id)
1197
+ next.publishedRevision = revision
1198
+ if (next.revision === revision) {
1199
+ next.changed.clear()
1200
+ next.removed.clear()
1201
+ }
1202
+ },
1203
+ })
1204
+ .catch((error) => {
1205
+ if (error instanceof Storage.NotFoundError) {
1206
+ cache.entries.delete(entry.root.id)
1207
+ cache.dirty.delete(entry.root.id)
1208
+ return
1209
+ }
1210
+ log.warn("execution snapshot update failed", { error })
1211
+ const current = cache.entries.get(entry.root.id)?.tree
1212
+ if (current) for (const owner of current.changes.owners) current.changes.repair.add(owner)
1213
+ cache.dirty.add(entry.root.id)
1214
+ })
1215
+ .finally(() => {
1216
+ cache.publishing.delete(entry.root.id)
1217
+ if (cache.entries.get(entry.root.id) === owner && cache.refreshRequested.delete(entry.root.id))
1218
+ schedule(entry.root.id)
1219
+ })
1220
+ }, 750)
1221
+ timer.unref()
1222
+ cache.timers.set(entry.root.id, timer)
1223
+ }
1224
+ }
1225
+ cache.schedule = schedule
1226
+ const deliver = (ownerID: string, key: string, apply: (entry: Data) => void, parentID?: string) => {
1227
+ const buffers = new Set<Changes>([
1228
+ ...[...cache.entries.values()].flatMap((entry) => (entry.tree ? [entry.tree.changes] : [])),
1229
+ ...[...cache.pending.values()].map((loading) => loading.changes),
1230
+ ])
1231
+ for (const changes of buffers) {
1232
+ if (parentID && changes.owners.has(parentID)) changes.owners.add(ownerID)
1233
+ if (!changes.owners.has(ownerID)) continue
1234
+ enqueue(changes, ownerID, key, apply)
1235
+ }
1236
+ schedule(parentID ?? ownerID)
1237
+ }
1238
+ const message = (sessionID: string, messageID: string) =>
1239
+ deliver(sessionID, "message:" + messageID, (entry) => entry.pendingMessages.set(messageID, sessionID))
1240
+ const disposers = [
1241
+ Bus.subscribeGlobal(RolloutEvents.Updated, (event) => {
1242
+ const { owner, revision, record } = event.properties
1243
+ if (owner.kind !== "session") return
1244
+ queueRoot(owner.sessionID, "rollout:" + revision, (value) => {
1245
+ const snapshot = value.snapshots[0]
1246
+ if (revision <= snapshot.revision) return
1247
+ if (revision !== snapshot.revision + 1) {
1248
+ const entry = cache.entries.get(owner.sessionID)
1249
+ if (entry) entry.metadataRepair = true
1250
+ return
1251
+ }
1252
+ snapshot.revision = revision
1253
+ if (record.kind === "run") update(snapshot.runs, record.value)
1254
+ if (record.kind === "call") {
1255
+ const prior = snapshot.calls.find((call) => call.id === record.value.id)
1256
+ value.orderChanged ||=
1257
+ !prior ||
1258
+ prior.started !== record.value.started ||
1259
+ prior.runID !== record.value.runID ||
1260
+ prior.usageRole !== record.value.usageRole ||
1261
+ record.value.usageRole === "compaction"
1262
+ update(snapshot.calls, record.value)
1263
+ value.changed.add(record.value.id)
1264
+ }
1265
+ })
1266
+ deliver(owner.sessionID, `rollout:${owner.sessionID}:${revision}`, (entry) =>
1267
+ apply(entry, owner, revision, record),
1268
+ )
1269
+ }),
1270
+ Bus.subscribeGlobal(SessionInbox.Event.Updated, (event) => {
1271
+ const { sessionID, items } = event.properties
1272
+ deliver(sessionID, "inbox:" + sessionID, (entry) => entry.inboxes.set(sessionID, items))
1273
+ }),
1274
+ Bus.subscribeGlobal(Usage.Updated, (event) => {
1275
+ const { owner, record } = event.properties
1276
+ if (owner.kind !== "session") return
1277
+ queueRoot(owner.sessionID, "usage:" + record.id, (value) => {
1278
+ if ((value.usage.get(record.id)?.revision ?? -1) >= record.revision) return
1279
+ value.usage.set(record.id, record)
1280
+ if (record.kind === "call" || record.kind === "attempt") {
1281
+ const id = record.kind === "call" ? record.entityID : record.callID
1282
+ value.changed.add(id)
1283
+ const records = value.usageByCall.get(id) ?? []
1284
+ update(records, record)
1285
+ value.usageByCall.set(id, records)
1286
+ }
1287
+ })
1288
+ deliver(owner.sessionID, "usage:" + record.id, (entry) => {
1289
+ entry.usage.set(record.id, record)
1290
+ if (record.kind === "call" || record.kind === "attempt")
1291
+ entry.changed.add(nodeID(owner.sessionID, "call", record.kind === "call" ? record.entityID : record.callID))
1292
+ })
1293
+ }),
1294
+ Bus.subscribeGlobal(MessageV2.Event.Updated, (event) => {
1295
+ const info = event.properties.info
1296
+ queueRoot(info.sessionID, "message:" + info.id, (value) => {
1297
+ value.pendingMessages.add(info.id)
1298
+ })
1299
+ message(info.sessionID, info.id)
1300
+ }),
1301
+ Bus.subscribeGlobal(MessageV2.Event.PartUpdated, (event) =>
1302
+ message(event.properties.part.sessionID, event.properties.part.messageID),
1303
+ ),
1304
+ Bus.subscribeGlobal(MessageV2.Event.Removed, (event) => {
1305
+ const { sessionID, messageID } = event.properties
1306
+ queueRoot(sessionID, "message:" + messageID, (value) => {
1307
+ value.pendingMessages.add(messageID)
1308
+ })
1309
+ message(sessionID, messageID)
1310
+ }),
1311
+ Bus.subscribeGlobal(SessionEvent.Deleted, (event) => {
1312
+ const id = event.properties.info.id
1313
+ const entry = cache.entries.get(id)
1314
+ entry?.metadataLoading?.controller.abort()
1315
+ entry?.audit?.controller.abort()
1316
+ cache.pending.get(id)?.controller.abort()
1317
+ clearTimeout(entry?.metadataTimer)
1318
+ clearTimeout(cache.timers.get(id))
1319
+ cache.entries.delete(id)
1320
+ cache.dirty.delete(id)
1321
+ cache.timers.delete(id)
1322
+ cache.refreshRequested.delete(id)
1323
+ }),
1324
+ Bus.subscribeGlobal(SessionEvent.Updated, (event) => {
1325
+ const info = event.properties.info
1326
+ queueRoot(info.id, "session", (value) => {
1327
+ value.root = info
1328
+ })
1329
+ deliver(
1330
+ info.id,
1331
+ "session:" + info.id,
1332
+ (entry) => {
1333
+ const index = entry.sessions.findIndex((session) => session.id === info.id)
1334
+ if (index >= 0) {
1335
+ const previous = entry.sessions[index]
1336
+ const title = info.cortex?.description ?? info.title
1337
+ if (title !== (previous.cortex?.description ?? previous.title))
1338
+ for (const node of entry.nodes)
1339
+ if (node.sessionID === info.id && (node.kind === "turn" || node.kind === "subtask")) {
1340
+ node.title = title
1341
+ entry.changed.add(node.id)
1342
+ }
1343
+ entry.sessions[index] = info
1344
+ entry.sessionsByID.set(info.id, info)
1345
+ } else entry.addedSessions.set(info.id, info)
1346
+ if (info.id === entry.root.id) entry.root = info
1347
+ },
1348
+ info.parentID,
1349
+ )
1350
+ }),
1351
+ ]
1352
+ cache.dispose = () => {
1353
+ for (const dispose of disposers) dispose()
1354
+ for (const timer of cache.timers.values()) clearTimeout(timer)
1355
+ cache.timers.clear()
1356
+ cache.refreshRequested.clear()
1357
+ for (const loading of cache.pending.values()) loading.controller.abort()
1358
+ for (const entry of cache.entries.values()) {
1359
+ entry.metadataLoading?.controller.abort()
1360
+ entry.audit?.controller.abort()
1361
+ clearTimeout(entry.metadataTimer)
1362
+ entry.metadataListeners.clear()
1363
+ entry.progressListeners.clear()
1364
+ }
1365
+ cache.schedule = undefined
1366
+ cache.entries.clear()
1367
+ cache.subscriptions.clear()
1368
+ cache.legacyClients = 0
1369
+ cache.dispose = undefined
1370
+ }
1371
+ return cache.dispose
1372
+ }
1373
+ function contextSnapshots(
1374
+ value: Pick<Data, "root" | "usage" | "contexts"> & {
1375
+ snapshots: Pick<RolloutSnapshot.Info, "owner" | "runs" | "calls">[]
1376
+ },
1377
+ runID?: string,
1378
+ retained?: Pick<RootData, "rows" | "changed" | "orderChanged" | "usageByCall" | "orderedCalls">,
1379
+ ): ExecutionSchema.ContextSnapshot[] {
1380
+ const snapshot = value.snapshots.find(
1381
+ (entry) => entry.owner.kind === "session" && entry.owner.sessionID === value.root.id,
1382
+ )
1383
+ if (!snapshot) return []
1384
+ const records = retained?.usageByCall ?? new Map<string, UsageSchema.Record[]>()
1385
+ for (const record of retained ? [] : value.usage.values()) {
1386
+ const id = record.kind === "call" ? record.entityID : record.kind === "attempt" ? record.callID : undefined
1387
+ if (!id) continue
1388
+ const group = records.get(id) ?? []
1389
+ group.push(record)
1390
+ records.set(id, group)
1391
+ }
1392
+ const cached = new Map(retained?.rows.map((row) => [row.callID, row]))
1393
+ let previous = -Infinity
1394
+ const rounds = new Map<string, number>()
1395
+ const compactions = snapshot.calls.filter((call) => call.usageRole === "compaction" && call.status === "completed")
1396
+ const ordered =
1397
+ retained && !retained.orderChanged
1398
+ ? retained.orderedCalls
1399
+ : snapshot.calls
1400
+ .filter((call) => call.usageRole === "conversation")
1401
+ .sort((a, b) => a.started - b.started || a.id.localeCompare(b.id))
1402
+ if (retained) retained.orderedCalls = ordered
1403
+ const currentCalls = new Map(snapshot.calls.map((call) => [call.id, call]))
1404
+ return ordered
1405
+ .map((call, index) => {
1406
+ call = currentCalls.get(call.id)!
1407
+ if (!rounds.has(call.runID)) rounds.set(call.runID, rounds.size + 1)
1408
+ const existing = cached.get(call.id)
1409
+ if (existing && retained && !retained.orderChanged && !retained.changed.has(call.id)) {
1410
+ previous = call.started
1411
+ return existing
1412
+ }
1413
+ const candidate = value.contexts.get(call.id)
1414
+ const accounting = Usage.summarize(records.get(call.id) ?? [], {
1415
+ sessionID: value.root.id,
1416
+ includeDescendants: false,
1417
+ })
1418
+ const context = accounting.context
1419
+ const attempt = (records.get(call.id) ?? []).find(
1420
+ (entry) => entry.kind === "attempt" && entry.entityID === context?.attemptID,
1421
+ )
1422
+ const measured = attempt?.kind === "attempt" ? attempt.usage : undefined
1423
+ const estimate =
1424
+ call.contextEstimate?.modelID === call.model.modelID &&
1425
+ call.contextEstimate.providerID === call.model.providerID
1426
+ ? call.contextEstimate
1427
+ : undefined
1428
+ const usage =
1429
+ candidate &&
1430
+ candidate.providerID === call.model.providerID &&
1431
+ candidate.modelID === call.model.modelID &&
1432
+ (context?.inputTokens == null || candidate.totalInput === context.inputTokens)
1433
+ ? candidate
1434
+ : estimate && context?.inputTokens != null
1435
+ ? reconcileContextUsage(estimate, context.inputTokens, estimate.capturedAt)
1436
+ : null
1437
+ const result = {
1438
+ sessionID: value.root.id,
1439
+ callID: call.id,
1440
+ nodeID: nodeID(value.root.id, "call", call.id),
1441
+ runID: call.runID,
1442
+ started: call.started,
1443
+ requestNumber: index + 1,
1444
+ roundNumber: rounds.get(call.runID)!,
1445
+ status: call.status,
1446
+ modelID: call.model.modelID,
1447
+ providerID: call.model.providerID,
1448
+ inputTokens: usage?.totalInput ?? context?.inputTokens ?? null,
1449
+ contextLimit:
1450
+ usage?.usableInputLimit ??
1451
+ usage?.contextLimit ??
1452
+ estimate?.usableInputLimit ??
1453
+ estimate?.contextLimit ??
1454
+ (ModelLimit.usableInput(call.model.limits) || null),
1455
+ outputTokens: measured?.output.total ?? null,
1456
+ cacheHit:
1457
+ measured?.input.total && measured.input.cacheRead != null
1458
+ ? Math.min(1, measured.input.cacheRead / measured.input.total)
1459
+ : null,
1460
+ elapsedMs: call.ended == null ? null : Math.max(0, call.ended - call.started),
1461
+ retries: accounting.outcomes.retries,
1462
+ usage,
1463
+ estimate,
1464
+ compactedBefore: compactions.some(
1465
+ (entry) => (entry.ended ?? entry.started) > previous && (entry.ended ?? entry.started) <= call.started,
1466
+ ),
1467
+ requestAvailable: !!call.request,
1468
+ }
1469
+ previous = call.started
1470
+ return result
1471
+ })
1472
+ .filter((entry) => !runID || entry.runID === runID)
1473
+ .reverse()
1474
+ }
1475
+ function contextPosition(cursor: string | undefined, identity: string) {
1476
+ if (!cursor) return undefined
1477
+ const parsed = queryCursor(z.object({ identity: z.string(), id: z.string() }), cursor)
1478
+ if (parsed.identity !== identity) throw new ExecutionQueryError("Context cursor does not match this selection")
1479
+ return parsed.id
1480
+ }
1481
+ const contextCursor = (identity: string, id: string) =>
1482
+ Buffer.from(JSON.stringify({ identity, id })).toString("base64url")
1483
+ async function bootstrapRoot(root: Session.Info, signal: AbortSignal): Promise<RootData> {
1484
+ const sessionID = root.id
1485
+ const [snapshot, records, infos] = await Promise.all([
1486
+ RolloutSnapshot.modelCalls({ kind: "session", scopeID: root.scope.id, sessionID }, signal),
1487
+ Usage.collect({ scopeID: root.scope.id, sessionID, includeDescendants: false }, signal),
1488
+ MessageV2.readInfoList({
1489
+ scopeID: Identifier.asScopeID(root.scope.id),
1490
+ sessionID: Identifier.asSessionID(sessionID),
1491
+ signal,
1492
+ }),
1493
+ ])
1494
+ checkpoint(signal)
1495
+ const contexts: Data["contexts"] = new Map()
1496
+ for (const info of infos) rememberContext(contexts, info)
1497
+ const usageByCall = new Map<string, UsageSchema.Record[]>()
1498
+ for (const record of records) {
1499
+ const id = record.kind === "call" ? record.entityID : record.kind === "attempt" ? record.callID : undefined
1500
+ if (!id) continue
1501
+ const group = usageByCall.get(id) ?? []
1502
+ group.push(record)
1503
+ usageByCall.set(id, group)
1504
+ }
1505
+ return {
1506
+ root,
1507
+ snapshots: [snapshot],
1508
+ usage: new Map(records.map((record) => [record.id, record])),
1509
+ infos: new Map(infos.map((info) => [info.id, info])),
1510
+ contexts,
1511
+ rows: [],
1512
+ revision: 0,
1513
+ changed: new Set(),
1514
+ orderChanged: true,
1515
+ usageByCall,
1516
+ orderedCalls: [],
1517
+ pendingMessages: new Set(),
1518
+ }
1519
+ }
1520
+ function refreshContexts(value: RootData) {
1521
+ const next: Data["contexts"] = new Map()
1522
+ for (const info of [...value.infos.values()].sort(
1523
+ (a, b) => a.time.created - b.time.created || a.id.localeCompare(b.id),
1524
+ ))
1525
+ rememberContext(next, info)
1526
+ for (const id of new Set([...next.keys(), ...value.contexts.keys()]))
1527
+ if (JSON.stringify(next.get(id)) !== JSON.stringify(value.contexts.get(id))) value.changed.add(id)
1528
+ value.contexts = next
1529
+ }
1530
+ function queueRoot(sessionID: string, key: string, apply: (value: RootData) => void) {
1531
+ const entry = state().entries.get(sessionID)
1532
+ if (!entry || (!entry.metadata && !entry.metadataLoading)) return
1533
+ if (entry.metadataChanges.size >= 4096) {
1534
+ entry.metadataChanges.clear()
1535
+ entry.metadataRepair = true
1536
+ } else entry.metadataChanges.set(key, apply)
1537
+ if (!entry.metadataListeners.size || entry.metadataTimer) return
1538
+ entry.metadataTimer = setTimeout(() => {
1539
+ entry.metadataTimer = undefined
1540
+ if (!entry.metadataListeners.size) return
1541
+ void ScopeContext.provide({
1542
+ scope: entry.root.scope,
1543
+ fn: async () => {
1544
+ const value = await contextData(sessionID, undefined, false)
1545
+ for (const listener of entry.metadataListeners) listener(value)
1546
+ },
1547
+ }).catch((error) =>
1548
+ log.warn("execution root update failed", { kind: error instanceof Error ? error.name : "unknown" }),
1549
+ )
1550
+ }, 50)
1551
+ entry.metadataTimer.unref()
1552
+ }
1553
+ async function contextData(sessionID: string, signal?: AbortSignal, access = true): Promise<RootData> {
1554
+ checkpoint(signal)
1555
+ const root = await requireRoot(sessionID)
1556
+ init()
1557
+ const entry = entryFor(root)
1558
+ if (access) entry.touched = Date.now()
1559
+ if (!entry.metadataLoading && entry.metadata && !entry.metadataChanges.size && !entry.metadataRepair)
1560
+ return entry.metadata
1561
+ if (entry.metadataLoading?.controller.signal.aborted) {
1562
+ await entry.metadataLoading.promise.catch(() => checkpoint(signal))
1563
+ return contextData(sessionID, signal, access)
1564
+ }
1565
+ if (!entry.metadataLoading) {
1566
+ const controller = new AbortController()
1567
+ const loading: RootLoading = {
1568
+ controller,
1569
+ readers: 0,
1570
+ promise: Promise.resolve()
1571
+ .then(async () => {
1572
+ let value = entry.metadata
1573
+ do {
1574
+ if (!value || entry.metadataRepair) {
1575
+ entry.metadataRepair = false
1576
+ value = await bootstrapRoot(root, controller.signal)
1577
+ }
1578
+ const changes = entry.metadataChanges
1579
+ entry.metadataChanges = new Map()
1580
+ for (const apply of changes.values()) {
1581
+ checkpoint(controller.signal)
1582
+ apply(value)
1583
+ }
1584
+ } while (entry.metadataRepair)
1585
+ for (const messageID of value.pendingMessages) {
1586
+ checkpoint(controller.signal)
1587
+ try {
1588
+ const info = await MessageV2.readInfo({ scopeID: root.scope.id, sessionID, messageID })
1589
+ value.infos.set(messageID, info)
1590
+ } catch (error) {
1591
+ if (!(error instanceof Storage.NotFoundError)) throw error
1592
+ value.infos.delete(messageID)
1593
+ }
1594
+ }
1595
+ if (value.pendingMessages.size) refreshContexts(value)
1596
+ value.pendingMessages.clear()
1597
+ checkpoint(controller.signal)
1598
+ value.rows = contextSnapshots(value, undefined, value)
1599
+ value.changed.clear()
1600
+ value.orderChanged = false
1601
+ value.revision = ++state().sequence
1602
+ entry.metadata = value
1603
+ return value
1604
+ })
1605
+ .catch((error) => {
1606
+ entry.metadataRepair = true
1607
+ throw error
1608
+ })
1609
+ .finally(() => {
1610
+ if (entry.metadataLoading === loading) entry.metadataLoading = undefined
1611
+ evict()
1612
+ }),
1613
+ }
1614
+ entry.metadataLoading = loading
1615
+ }
1616
+ const loading = entry.metadataLoading
1617
+ loading.readers++
1618
+ let abort: (() => void) | undefined
1619
+ try {
1620
+ if (!signal) return await loading.promise
1621
+ return await Promise.race([
1622
+ loading.promise,
1623
+ new Promise<never>((_, reject) => {
1624
+ abort = () => reject(signal.reason)
1625
+ signal.addEventListener("abort", abort, { once: true })
1626
+ if (signal.aborted) abort()
1627
+ }),
1628
+ ])
1629
+ } finally {
1630
+ if (abort) signal?.removeEventListener("abort", abort)
1631
+ if (--loading.readers === 0 && !entry.metadataListeners.size) loading.controller.abort()
1632
+ }
1633
+ }
1634
+ export async function observeRoot(
1635
+ sessionID: string,
1636
+ listener: (value: { revision: number; contexts: ExecutionSchema.ContextSnapshot[] }) => void,
1637
+ signal?: AbortSignal,
1638
+ ) {
1639
+ const root = await requireRoot(sessionID)
1640
+ init()
1641
+ const entry = entryFor(root)
1642
+ const update = (value: RootData) => listener({ revision: value.revision, contexts: value.rows })
1643
+ entry.metadataListeners.add(update)
1644
+ let disposed = false
1645
+ const dispose = () => {
1646
+ if (disposed) return
1647
+ disposed = true
1648
+ entry.metadataListeners.delete(update)
1649
+ signal?.removeEventListener("abort", dispose)
1650
+ if (!entry.metadataListeners.size) {
1651
+ clearTimeout(entry.metadataTimer)
1652
+ entry.metadataTimer = undefined
1653
+ if (entry.metadataLoading && !entry.metadataLoading.readers) entry.metadataLoading.controller.abort()
1654
+ }
1655
+ }
1656
+ signal?.addEventListener("abort", dispose, { once: true })
1657
+ try {
1658
+ const value = await contextData(sessionID, signal)
1659
+ checkpoint(signal)
1660
+ update(value)
1661
+ return dispose
1662
+ } catch (error) {
1663
+ dispose()
1664
+ throw error
1665
+ }
1666
+ }
1667
+ export async function observePreparation(
1668
+ sessionID: string,
1669
+ listener: (value: SessionHistory.ExecutionProgress) => void,
1670
+ ) {
1671
+ const entry = entryFor(await requireRoot(sessionID))
1672
+ entry.progressListeners.add(listener)
1673
+ return () => {
1674
+ entry.progressListeners.delete(listener)
1675
+ }
1676
+ }
1677
+ export async function contextHistory(
1678
+ sessionID: string,
1679
+ input: z.infer<typeof ExecutionSchema.ContextQuery>,
1680
+ signal?: AbortSignal,
1681
+ ) {
1682
+ const value = await contextData(sessionID, signal)
1683
+ const identity = JSON.stringify([ScopeContext.current.scope.id, sessionID, input.runID ?? null])
1684
+ if (input.runID && !value.snapshots[0].runs.some((run) => !run.admissionOnly && run.id === input.runID))
1685
+ throw new Storage.NotFoundError({ message: "Context round was not found" })
1686
+ const entries = value.rows.filter((row) => !input.runID || row.runID === input.runID)
1687
+ const id = contextPosition(input.cursor, identity)
1688
+ const position = id ? entries.findIndex((entry) => entry.callID === id) : -1
1689
+ if (id && position < 0) throw new ExecutionQueryError("Context cursor request was not found")
1690
+ const items = entries.slice(position + 1, position + 1 + input.limit)
1691
+ return ExecutionSchema.ContextHistory.parse({
1692
+ sessionID,
1693
+ revision: value.revision,
1694
+ total: entries.length,
1695
+ items,
1696
+ nextCursor: position + 1 + items.length < entries.length ? contextCursor(identity, items.at(-1)!.callID) : null,
1697
+ })
1698
+ }
1699
+ export async function contextSnapshot(sessionID: string, callID: string, runID?: string, signal?: AbortSignal) {
1700
+ const value = await contextData(sessionID, signal)
1701
+ const entry = value.rows.find((entry) => entry.callID === callID && (!runID || entry.runID === runID))
1702
+ if (!entry) throw new Storage.NotFoundError({ message: "Context request was not found in this session" })
1703
+ return ExecutionSchema.ContextSnapshot.parse(entry)
1704
+ }
1705
+ export async function contextItems(
1706
+ sessionID: string,
1707
+ callID: string,
1708
+ input: z.infer<typeof ExecutionSchema.ContextItemsQuery>,
1709
+ signal?: AbortSignal,
1710
+ ) {
1711
+ const snapshot = await contextSnapshot(sessionID, callID, input.runID, signal)
1712
+ const empty = {
1713
+ callID,
1714
+ nodeID: snapshot.nodeID,
1715
+ contentVersion: null,
1716
+ status: "unavailable",
1717
+ items: [],
1718
+ total: 0,
1719
+ nextCursor: null,
1720
+ truncated: false,
1721
+ }
1722
+ if (!snapshot.requestAvailable) return ExecutionSchema.ContextItems.parse(empty)
1723
+ const source = await contentSource(sessionID, snapshot.nodeID, "request", input.runID, input.version, signal)
1724
+ const manifest = z
1725
+ .array(MessageV2.ContextSource)
1726
+ .safeParse(await ExecutionContent.value(source, ["contextSources"], signal))
1727
+ if (!manifest.success)
1728
+ return ExecutionSchema.ContextItems.parse({ ...empty, contentVersion: source.contentVersion, status: "legacy" })
1729
+ const sections = new Map<string, z.infer<typeof ExecutionContent.Section>>()
1730
+ let next: string | undefined
1731
+ let truncated = false
1732
+ do {
1733
+ const page = await ExecutionContent.sections(source, { field: "request", limit: 500, cursor: next }, signal)
1734
+ for (const section of page.items) sections.set(JSON.stringify(section.path), section)
1735
+ next = page.nextCursor ?? undefined
1736
+ truncated ||= page.truncated
1737
+ } while (next)
1738
+ const entries = manifest.data.flatMap((entry, index) => {
1739
+ const section = sections.get(JSON.stringify(entry.path))
1740
+ if (!section) {
1741
+ truncated = true
1742
+ return []
1743
+ }
1744
+ if (input.category && input.category !== entry.category) return []
1745
+ if (input.query && !entry.source.toLocaleLowerCase().includes(input.query.toLocaleLowerCase())) return []
1746
+ return [{ ...entry, id: String(index), offset: section.offset, bytes: section.bytes }]
1747
+ })
1748
+ const identity = JSON.stringify([
1749
+ ScopeContext.current.scope.id,
1750
+ sessionID,
1751
+ callID,
1752
+ source.contentVersion,
1753
+ input.category,
1754
+ input.query,
1755
+ input.runID,
1756
+ ])
1757
+ const id = contextPosition(input.cursor, identity)
1758
+ const offset = id ? entries.findIndex((entry) => entry.id === id) + 1 : 0
1759
+ if (id && !offset) throw new ExecutionQueryError("Context item cursor was not found")
1760
+ const items = entries.slice(offset, offset + input.limit)
1761
+ return ExecutionSchema.ContextItems.parse({
1762
+ callID,
1763
+ nodeID: snapshot.nodeID,
1764
+ contentVersion: source.contentVersion,
1765
+ status: "available",
1766
+ items,
1767
+ total: entries.length,
1768
+ nextCursor: offset + items.length < entries.length ? contextCursor(identity, items.at(-1)!.id) : null,
1769
+ truncated,
1770
+ })
1771
+ }
1772
+ export async function summary(sessionID: string, runID?: string, signal?: AbortSignal) {
1773
+ const value = await data(sessionID, true, signal)
1774
+ if (!runID) return structuredClone(summarize(value))
1775
+ if (!value.summary.rounds.some((round) => round.id === runID))
1776
+ throw new Storage.NotFoundError({ message: "Execution round was not found" })
1777
+ const summary = summarize(value, runID)
1778
+ value.selections.delete(runID)
1779
+ value.selections.set(runID, { summary, touched: Date.now() })
1780
+ if (value.selections.size > 4) value.selections.delete(value.selections.keys().next().value!)
1781
+ return structuredClone(summary)
1782
+ }
1783
+ export async function verifyJournal(sessionID: string, signal?: AbortSignal): Promise<void> {
1784
+ const value = await data(sessionID, true, signal)
1785
+ const entry = entryFor(value.root)
1786
+ if (entry.audit?.controller.signal.aborted) {
1787
+ await entry.audit.promise.catch(() => checkpoint(signal))
1788
+ return verifyJournal(sessionID, signal)
1789
+ }
1790
+ if (entry.audit) {
1791
+ await consume(sessionID, entry.audit, signal)
1792
+ return
1793
+ }
1794
+ const owners = value.snapshots.filter(
1795
+ (snapshot) => snapshot.owner.kind === "session" && !value.verified.has(snapshot.owner.sessionID),
1796
+ )
1797
+ if (!owners.length) return
1798
+ const controller = new AbortController()
1799
+ const loading: Loading = {
1800
+ scopeID: value.root.scope.id,
1801
+ changes: value.changes,
1802
+ readers: 0,
1803
+ controller,
1804
+ promise: Promise.resolve()
1805
+ .then(async () => {
1806
+ const snapshots = await RolloutSnapshot.currentAll(
1807
+ owners.map((snapshot) => snapshot.owner),
1808
+ controller.signal,
1809
+ )
1810
+ checkpoint(controller.signal)
1811
+ for (const verified of snapshots) {
1812
+ if (verified.owner.kind !== "session") continue
1813
+ const current = value.snapshotsByID.get(verified.owner.sessionID)
1814
+ if (!current || current.revision !== verified.revision) continue
1815
+ current.gaps = verified.gaps
1816
+ value.verified.add(verified.owner.sessionID)
1817
+ }
1818
+ value.revision = ++state().sequence
1819
+ value.summary = summarize(value)
1820
+ for (const [runID, selection] of value.selections) selection.summary = summarize(value, runID)
1821
+ return value
1822
+ })
1823
+ .finally(() => {
1824
+ if (entry.audit === loading) entry.audit = undefined
1825
+ }),
1826
+ }
1827
+ entry.audit = loading
1828
+ await consume(sessionID, loading, signal)
1829
+ }
1830
+ export async function trajectory(sessionID: string, input: ExecutionSchema.Query, signal?: AbortSignal) {
1831
+ const value = await data(sessionID, true, signal)
1832
+ if (input.runID && !value.summary.rounds.some((round) => round.id === input.runID))
1833
+ throw new Storage.NotFoundError({ message: "Execution round was not found" })
1834
+ if (input.session && !value.sessions.some((session) => session.id === input.session))
1835
+ throw new Storage.NotFoundError({ message: "Executor does not belong to this task" })
1836
+ const query = input.query?.trim().toLocaleLowerCase()
1837
+ const kinds = input.kinds
1838
+ ?.split(",")
1839
+ .filter(Boolean)
1840
+ .map((kind) => queryValue(ExecutionSchema.Node.shape.kind, kind))
1841
+ const statuses = input.statuses
1842
+ ?.split(",")
1843
+ .filter(Boolean)
1844
+ .map((status) => queryValue(ExecutionSchema.Node.shape.status, status))
1845
+ const actualActivities = activityFor(value, input.runID).nodes
1846
+ const activityNodes = new Map(actualActivities.map((node) => [node.id, node.activity]))
1847
+ const activityPositions = new Map(actualActivities.map((node, index) => [node.id, index]))
1848
+ const rows = processRows(value, input.mode, input.runID)
1849
+ .map((node) => ({ ...node, activity: activityNodes.get(node.id) }))
1850
+ .filter(
1851
+ (node) =>
1852
+ (input.session
1853
+ ? node.sessionID === input.session
1854
+ : !!query ||
1855
+ input.actor === "all" ||
1856
+ node.sessionID === sessionID ||
1857
+ (node.kind === "subtask" && value.sessionsByID.get(node.sessionID)?.parentID === sessionID)) &&
1858
+ (!input.kind || node.kind === input.kind) &&
1859
+ (!input.status || node.status === input.status) &&
1860
+ (!kinds?.length || kinds.includes(node.kind)) &&
1861
+ (!statuses?.length || statuses.includes(node.status)) &&
1862
+ (!input.anomalies || node.kind === "retry" || ["failed", "cancelled", "interrupted"].includes(node.status)) &&
1863
+ (input.activityFrom === undefined || (activityPositions.get(node.id) ?? -1) >= input.activityFrom) &&
1864
+ (input.activityTo === undefined || (activityPositions.get(node.id) ?? Infinity) <= input.activityTo) &&
1865
+ (input.from === undefined || node.started >= input.from) &&
1866
+ (input.to === undefined || node.started <= input.to) &&
1867
+ (!query ||
1868
+ (node.title + " " + node.preview + " " + (node.tool ?? "") + " " + (node.agent ?? ""))
1869
+ .toLocaleLowerCase()
1870
+ .includes(query)),
1871
+ )
1872
+ if (input.order !== "time") {
1873
+ const rounds = new Map(value.summary.rounds.map((round, index) => [round.id, index]))
1874
+ rows.sort(
1875
+ (a, b) =>
1876
+ (rounds.get(a.rootRunID ?? "") ?? Number.MAX_SAFE_INTEGER) -
1877
+ (rounds.get(b.rootRunID ?? "") ?? Number.MAX_SAFE_INTEGER) ||
1878
+ (input.order === "call" ? (a.group?.started ?? a.started) - (b.group?.started ?? b.started) : 0) ||
1879
+ a.started - b.started ||
1880
+ a.id.localeCompare(b.id),
1881
+ )
1882
+ }
1883
+ const fingerprint = JSON.stringify({
1884
+ ...input,
1885
+ cursor: undefined,
1886
+ anchor: undefined,
1887
+ position: undefined,
1888
+ limit: undefined,
1889
+ })
1890
+ let offset = 0
1891
+ if (input.cursor) {
1892
+ const parsed = queryCursor(
1893
+ z.object({ filter: z.string(), after: z.string().nullable(), before: z.string().nullable() }),
1894
+ input.cursor,
1895
+ )
1896
+ if (parsed.filter !== fingerprint) throw new ExecutionQueryError("Trajectory cursor does not match its filters")
1897
+ if (parsed.after) offset = Math.max(0, rows.findIndex((node) => node.id === parsed.after) + 1)
1898
+ if (parsed.before) offset = Math.max(0, rows.findIndex((node) => node.id === parsed.before) - input.limit)
1899
+ } else if (input.anchor === "latest") {
1900
+ offset = Math.max(0, rows.length - input.limit)
1901
+ } else if (input.anchor) {
1902
+ const index = rows.findIndex(
1903
+ (node) => node.id === input.anchor || node.runID === input.anchor || node.messageID === input.anchor,
1904
+ )
1905
+ offset = Math.max(
1906
+ 0,
1907
+ input.position === "after"
1908
+ ? index + 1
1909
+ : index - (input.position === "before" ? input.limit : Math.floor(input.limit / 2)),
1910
+ )
1911
+ }
1912
+ const items = rows.slice(offset, offset + input.limit)
1913
+ const cursor = (after: string | null, before: string | null) =>
1914
+ Buffer.from(JSON.stringify({ filter: fingerprint, after, before })).toString("base64url")
1915
+ return ExecutionSchema.Page.parse({
1916
+ sessionID,
1917
+ revision: value.revision,
1918
+ total: rows.length,
1919
+ items,
1920
+ nextCursor: offset + items.length < rows.length && items.length ? cursor(items.at(-1)!.id, null) : null,
1921
+ previousCursor: offset > 0 && items.length ? cursor(null, items[0].id) : null,
1922
+ })
1923
+ }
1924
+ async function find(sessionID: string, id: string, runID?: string, signal?: AbortSignal) {
1925
+ const value = await data(sessionID, true, signal)
1926
+ if (runID && !value.summary.rounds.some((round) => round.id === runID))
1927
+ throw new Storage.NotFoundError({ message: "Execution round was not found" })
1928
+ const node = selected(value, runID).find((node) => node.id === id)
1929
+ if (!node) throw new Storage.NotFoundError({ message: "Trajectory node was not found in this session" })
1930
+ const detail = details(value)
1931
+ return {
1932
+ value,
1933
+ node,
1934
+ evidence: detail.evidence.get(id),
1935
+ evidenceByID: detail.evidence,
1936
+ message: detail.messages.get(id),
1937
+ }
1938
+ }
1939
+ function details(value: Data) {
1940
+ if (value.detail) return value.detail
1941
+ const evidence = new Map<string, Evidence>()
1942
+ const messages = new Map<string, { sessionID: string; messageID: string; partID: string }>()
1943
+ for (const [index, snapshot] of value.snapshots.entries())
1944
+ for (const [id, record] of nodesFor(snapshot, value.sessions[index], true).evidence) evidence.set(id, record)
1945
+ for (const node of value.nodes) {
1946
+ if (node.source !== "messages" || !node.messageID) continue
1947
+ const [, , partID] = JSON.parse(Buffer.from(node.id, "base64url").toString()) as [string, string, string]
1948
+ messages.set(node.id, { sessionID: node.sessionID, messageID: node.messageID, partID })
1949
+ }
1950
+ value.detail = { evidence, messages, nodePositions: new Map(value.nodes.map((node, index) => [node.id, index])) }
1951
+ return value.detail
1952
+ }
1953
+ async function requestNode(sessionID: string, id: string, runID?: string, signal?: AbortSignal) {
1954
+ if (state().entries.get(sessionID)?.tree) return
1955
+ let identity: unknown
1956
+ try {
1957
+ identity = JSON.parse(Buffer.from(id, "base64url").toString())
1958
+ } catch {
1959
+ return
1960
+ }
1961
+ const parsed = z.tuple([z.string(), z.literal("call"), z.string()]).safeParse(identity)
1962
+ if (!parsed.success || parsed.data[0] !== sessionID) return
1963
+ const metadata = await contextData(sessionID, signal)
1964
+ if (metadata.root.parentID) return
1965
+ const call = metadata.snapshots[0].calls.find(
1966
+ (call) => call.id === parsed.data[2] && (!runID || call.runID === runID),
1967
+ )
1968
+ if (!call) throw new Storage.NotFoundError({ message: "Execution call was not found in this session" })
1969
+ const snapshot = await RolloutSnapshot.projected(metadata.snapshots[0].owner, call.runID, () => checkpoint(signal))
1970
+ const projected = nodesFor(snapshot, metadata.root, true)
1971
+ const messages = await SessionHistory.executionMessages({ sessionID, signal })
1972
+ await appendMessages(
1973
+ metadata.root,
1974
+ snapshot,
1975
+ messages.filter((message) => (message.info.rootID ?? message.info.id) === call.runID),
1976
+ projected.nodes,
1977
+ new Map(),
1978
+ signal,
1979
+ )
1980
+ projected.nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id))
1981
+ const node = projected.nodes.find((node) => node.id === id)
1982
+ if (!node) throw new Storage.NotFoundError({ message: "Execution call was removed" })
1983
+ return {
1984
+ value: { root: metadata.root, nodes: projected.nodes },
1985
+ node,
1986
+ evidence: projected.evidence.get(id),
1987
+ evidenceByID: projected.evidence,
1988
+ message: undefined,
1989
+ }
1990
+ }
1991
+ export async function node(sessionID: string, id: string, runID?: string, signal?: AbortSignal) {
1992
+ const found = (await requestNode(sessionID, id, runID, signal)) ?? (await find(sessionID, id, runID, signal))
1993
+ const call =
1994
+ found.evidence?.record.kind === "call"
1995
+ ? found.evidence
1996
+ : found.node.parentID
1997
+ ? found.evidenceByID.get(found.node.parentID)
1998
+ : undefined
1999
+ const definitions =
2000
+ call?.record.kind === "call" && (found.node.kind === "tool" || found.evidence?.record.kind === "call")
2001
+ ? await ExecutionContent.value(
2002
+ await contentSource(
2003
+ sessionID,
2004
+ nodeID(found.node.sessionID, "call", call.record.value.id),
2005
+ "request",
2006
+ runID,
2007
+ undefined,
2008
+ signal,
2009
+ ),
2010
+ ["tools"],
2011
+ signal,
2012
+ )
2013
+ : undefined
2014
+ const toolDefinitions =
2015
+ found.node.kind === "tool" && Array.isArray(definitions)
2016
+ ? definitions.filter(
2017
+ (definition) =>
2018
+ definition &&
2019
+ typeof definition === "object" &&
2020
+ (("id" in definition && definition.id === found.node.tool) ||
2021
+ ("name" in definition && definition.name === found.node.tool) ||
2022
+ ("function" in definition &&
2023
+ definition.function &&
2024
+ typeof definition.function === "object" &&
2025
+ "name" in definition.function &&
2026
+ definition.function.name === found.node.tool)),
2027
+ )
2028
+ : definitions
2029
+ const process = ExecutionProcess.project(found.value.nodes, "process", found.value.root.id)
2030
+ const projected =
2031
+ process.find((node) => node.id === id) ??
2032
+ ExecutionProcess.project(found.value.nodes, "records", found.value.root.id).find((node) => node.id === id) ??
2033
+ found.node
2034
+ const grouped =
2035
+ (projected.group?.callCount ?? 0) > 1 ? ExecutionProcess.auxiliaryMembers(found.value.nodes, projected) : []
2036
+ return ExecutionSchema.Detail.parse({
2037
+ node: projected,
2038
+ execution: ["turn", "subtask"].includes(found.node.kind)
2039
+ ? await summary(found.node.sessionID, found.node.kind === "subtask" ? undefined : found.node.runID, signal)
2040
+ : undefined,
2041
+ record: found.evidence?.record.value ?? found.message ?? null,
2042
+ sources: found.evidence ? RolloutEvidence.sources(found.evidence.record) : [],
2043
+ definitions: toolDefinitions ?? null,
2044
+ related: [
2045
+ ...new Map(
2046
+ [
2047
+ ...grouped,
2048
+ ...found.value.nodes.filter((node) => node.parentID === id || node.id === found.node.parentID),
2049
+ ].map((node) => [node.id, node]),
2050
+ ).values(),
2051
+ ].slice(0, 500),
2052
+ })
2053
+ }
2054
+ export async function content(
2055
+ sessionID: string,
2056
+ id: string,
2057
+ field: string,
2058
+ offset: number,
2059
+ limit: number,
2060
+ runID?: string,
2061
+ version?: string,
2062
+ signal?: AbortSignal,
2063
+ ) {
2064
+ const source = await contentSource(sessionID, id, field, runID, version, signal)
2065
+ checkpoint(signal)
2066
+ return source.read(offset, limit)
2067
+ }
2068
+ export async function contentSource(
2069
+ sessionID: string,
2070
+ id: string,
2071
+ field: string,
2072
+ runID?: string,
2073
+ version?: string,
2074
+ signal?: AbortSignal,
2075
+ ): Promise<ExecutionContent.Source> {
2076
+ const identity = z.tuple([z.string(), z.string(), z.string()]).safeParse(
2077
+ (() => {
2078
+ try {
2079
+ return JSON.parse(Buffer.from(id, "base64url").toString())
2080
+ } catch {
2081
+ return null
2082
+ }
2083
+ })(),
2084
+ )
2085
+ const direct = identity.success && identity.data[0] === sessionID && identity.data[1] === "call"
2086
+ const found = direct
2087
+ ? await (async () => {
2088
+ const value = await contextData(sessionID, signal)
2089
+ const snapshot = value.snapshots[0]
2090
+ const call = snapshot.calls.find((call) => call.id === identity.data[2] && (!runID || call.runID === runID))
2091
+ if (!call) throw new Storage.NotFoundError({ message: "Execution call was not found in this session" })
2092
+ return {
2093
+ evidence: { owner: snapshot.owner, record: { kind: "call", value: call } } as Evidence,
2094
+ message: undefined,
2095
+ }
2096
+ })()
2097
+ : await find(sessionID, id, runID, signal)
2098
+ if (found.evidence) {
2099
+ const { owner, record } = found.evidence
2100
+ const { ref, contentVersion } = await RolloutEvidence.source(owner, record, field, version)
2101
+ return {
2102
+ ...ref,
2103
+ contentVersion,
2104
+ read: (offset, limit) => RolloutEvidence.content(owner, record, field, offset, limit, contentVersion),
2105
+ stream: async function* () {
2106
+ yield* (await RolloutEvidence.stream(owner, record, field, contentVersion)).chunks
2107
+ },
2108
+ }
2109
+ }
2110
+ if (field !== "message") throw new Storage.NotFoundError({ message: "Message content field was not found" })
2111
+ const message =
2112
+ found.message && (await MessageV2.get({ sessionID: found.message.sessionID, messageID: found.message.messageID }))
2113
+ const part = message?.parts.find((value) => value.id === found.message!.partID)
2114
+ const body = part && "text" in part && typeof part.text === "string" ? part.text : null
2115
+ const text = body ?? JSON.stringify(message && { info: message.info, part }, null, 2)
2116
+ if (text === undefined) throw new Storage.NotFoundError({ message: "Message content was not found" })
2117
+ const mediaType = body === null ? "application/json" : "text/plain"
2118
+ const bytes = Buffer.from(text)
2119
+ const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex")
2120
+ const contentVersion = "message:" + id + ":" + sha256
2121
+ if (version && version !== contentVersion) throw new ExecutionQueryError("Execution message version changed")
2122
+ return {
2123
+ mediaType,
2124
+ bytes: bytes.length,
2125
+ status: "complete",
2126
+ sha256,
2127
+ contentVersion,
2128
+ read: async (offset, limit) => {
2129
+ queryValue(z.number().int().nonnegative().safe(), offset)
2130
+ queryValue(z.number().int().min(1).max(65_536), limit)
2131
+ if (offset > bytes.length) throw new ExecutionQueryError("Content offset exceeds the recorded message")
2132
+ const end = offset + RolloutEvidence.textBoundary(bytes.subarray(offset, offset + limit + 4), limit)
2133
+ return ExecutionSchema.Content.parse({
2134
+ mediaType,
2135
+ text: bytes.subarray(offset, end).toString(),
2136
+ offset,
2137
+ nextOffset: end < bytes.length ? end : null,
2138
+ bytes: bytes.length,
2139
+ status: "complete",
2140
+ contentVersion,
2141
+ sha256,
2142
+ })
2143
+ },
2144
+ stream: async function* () {
2145
+ for (let offset = 0; offset < bytes.length; offset += 65_536) yield bytes.subarray(offset, offset + 65_536)
2146
+ },
2147
+ }
2148
+ }
2149
+ }