@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.0

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,230 @@
1
+ export namespace ExecutionJson {
2
+ export type Section = {
3
+ path: string[]
4
+ kind: "object" | "array" | "string" | "number" | "boolean" | "null"
5
+ offset: number
6
+ bytes: number
7
+ role?: string
8
+ preview: string
9
+ }
10
+ type Frame = {
11
+ path: string[]
12
+ kind: "object" | "array"
13
+ start: number
14
+ state: "key" | "colon" | "value" | "comma"
15
+ empty: boolean
16
+ key?: string
17
+ index: number
18
+ role?: string
19
+ }
20
+ type Token = {
21
+ start: number
22
+ path: string[]
23
+ key: boolean
24
+ kind: "string" | "scalar"
25
+ escaped: boolean
26
+ unicode: number
27
+ sample: number[]
28
+ overflow: boolean
29
+ }
30
+
31
+ export async function index(source: AsyncIterable<Uint8Array>, signal?: AbortSignal) {
32
+ const stack: Frame[] = []
33
+ const items: Section[] = []
34
+ let token: Token | undefined
35
+ let offset = 0
36
+ let done = false
37
+ let truncated = false
38
+ let indexBytes = 0
39
+ const utf8 = new TextDecoder("utf-8", { fatal: true })
40
+ const invalid = () => {
41
+ throw new SyntaxError("Execution content is not complete JSON")
42
+ }
43
+ const path = () => {
44
+ const parent = stack.at(-1)
45
+ if (!parent) return []
46
+ return [...parent.path, parent.kind === "array" ? String(parent.index) : parent.key!]
47
+ }
48
+ const finishValue = () => {
49
+ const parent = stack.at(-1)
50
+ if (!parent) {
51
+ done = true
52
+ return
53
+ }
54
+ if (parent.state !== "value") invalid()
55
+ parent.state = "comma"
56
+ parent.empty = false
57
+ parent.index++
58
+ }
59
+ const sectionBytes = (value: Section) =>
60
+ 96 + value.path.reduce((sum, key) => sum + key.length * 4, 0) + value.preview.length * 4
61
+ const section = (value: Section) => {
62
+ if (value.path.length > 2) return
63
+ const bytes = sectionBytes(value)
64
+ while (value.path.length <= 1 && (items.length >= 10_000 || indexBytes + bytes > 2 * 1024 * 1024)) {
65
+ const index = items.findLastIndex((item) => item.path.length > 1)
66
+ if (index < 0) break
67
+ indexBytes -= sectionBytes(items[index])
68
+ items.splice(index, 1)
69
+ truncated = true
70
+ }
71
+ if (items.length >= 10_000 || indexBytes + bytes > 2 * 1024 * 1024) {
72
+ truncated = true
73
+ return
74
+ }
75
+ indexBytes += bytes
76
+ items.push(value)
77
+ }
78
+ const finishToken = (end: number) => {
79
+ const current = token!
80
+ const raw = Buffer.from(current.sample).toString()
81
+ if (current.key) {
82
+ if (current.overflow) throw new RangeError("Execution JSON key exceeds the section index budget")
83
+ stack.at(-1)!.key = JSON.parse(raw)
84
+ stack.at(-1)!.state = "colon"
85
+ } else {
86
+ let kind: Section["kind"] = "string"
87
+ let preview = ""
88
+ if (current.kind === "scalar") {
89
+ if (current.overflow || !/^(?:null|true|false|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)$/.test(raw))
90
+ invalid()
91
+ kind = raw === "null" ? "null" : raw === "true" || raw === "false" ? "boolean" : "number"
92
+ preview = raw
93
+ } else if (!current.overflow) {
94
+ const value: unknown = JSON.parse(raw)
95
+ if (typeof value === "string") preview = value.slice(0, 160)
96
+ if (current.path.at(-1) === "role" && typeof value === "string") stack.at(-1)!.role = value.slice(0, 64)
97
+ }
98
+ section({ path: current.path, kind, offset: current.start, bytes: end - current.start, preview })
99
+ finishValue()
100
+ }
101
+ token = undefined
102
+ }
103
+ const sample = (byte: number) => {
104
+ const current = token!
105
+ const budget = current.key ? 65_536 : current.kind === "scalar" ? 1024 : 512
106
+ if (current.sample.length < budget) current.sample.push(byte)
107
+ else current.overflow = true
108
+ }
109
+ const consume = (byte: number): boolean => {
110
+ if (token) {
111
+ if (token.kind === "scalar") {
112
+ if (byte === 32 || byte === 9 || byte === 10 || byte === 13 || byte === 44 || byte === 93 || byte === 125) {
113
+ finishToken(offset)
114
+ return false
115
+ }
116
+ sample(byte)
117
+ return true
118
+ }
119
+ sample(byte)
120
+ if (token.unicode) {
121
+ if (!((byte >= 48 && byte <= 57) || (byte >= 65 && byte <= 70) || (byte >= 97 && byte <= 102))) invalid()
122
+ token.unicode--
123
+ } else if (token.escaped) {
124
+ if (byte === 117) token.unicode = 4
125
+ else if (![34, 92, 47, 98, 102, 110, 114, 116].includes(byte)) invalid()
126
+ token.escaped = false
127
+ } else if (byte === 92) token.escaped = true
128
+ else if (byte === 34) finishToken(offset + 1)
129
+ else if (byte < 32) invalid()
130
+ return true
131
+ }
132
+ if (byte === 32 || byte === 9 || byte === 10 || byte === 13) return true
133
+ if (done) invalid()
134
+ const parent = stack.at(-1)
135
+ if (byte === 125 || byte === 93) {
136
+ if (
137
+ !parent ||
138
+ parent.kind !== (byte === 125 ? "object" : "array") ||
139
+ !(parent.state === "comma" || (parent.empty && parent.state === (parent.kind === "object" ? "key" : "value")))
140
+ )
141
+ invalid()
142
+ const frame = stack.pop()!
143
+ section({
144
+ path: frame.path,
145
+ kind: frame.kind,
146
+ offset: frame.start,
147
+ bytes: offset + 1 - frame.start,
148
+ role: frame.role,
149
+ preview: "",
150
+ })
151
+ finishValue()
152
+ return true
153
+ }
154
+ if (parent?.state === "comma") {
155
+ if (byte !== 44) invalid()
156
+ parent.state = parent.kind === "object" ? "key" : "value"
157
+ return true
158
+ }
159
+ if (parent?.state === "colon") {
160
+ if (byte !== 58) invalid()
161
+ parent.state = "value"
162
+ return true
163
+ }
164
+ if (parent?.state === "key") {
165
+ if (byte !== 34) invalid()
166
+ token = {
167
+ start: offset,
168
+ path: parent.path,
169
+ kind: "string",
170
+ key: true,
171
+ escaped: false,
172
+ unicode: 0,
173
+ sample: [byte],
174
+ overflow: false,
175
+ }
176
+ return true
177
+ }
178
+ if (parent && parent.state !== "value") invalid()
179
+ const currentPath = path()
180
+ if (byte === 123 || byte === 91) {
181
+ if (stack.length >= 128) throw new RangeError("Execution JSON nesting exceeds the section index budget")
182
+ stack.push({
183
+ path: currentPath,
184
+ kind: byte === 123 ? "object" : "array",
185
+ start: offset,
186
+ state: byte === 123 ? "key" : "value",
187
+ empty: true,
188
+ index: 0,
189
+ })
190
+ } else if (byte === 34) {
191
+ token = {
192
+ start: offset,
193
+ path: currentPath,
194
+ kind: "string",
195
+ key: false,
196
+ escaped: false,
197
+ unicode: 0,
198
+ sample: [byte],
199
+ overflow: false,
200
+ }
201
+ } else {
202
+ if (!(byte === 45 || (byte >= 48 && byte <= 57) || byte === 116 || byte === 102 || byte === 110)) invalid()
203
+ token = {
204
+ start: offset,
205
+ path: currentPath,
206
+ kind: "scalar",
207
+ key: false,
208
+ escaped: false,
209
+ unicode: 0,
210
+ sample: [byte],
211
+ overflow: false,
212
+ }
213
+ }
214
+ return true
215
+ }
216
+ for await (const chunk of source) {
217
+ signal?.throwIfAborted()
218
+ utf8.decode(chunk, { stream: true })
219
+ for (const byte of chunk) {
220
+ while (!consume(byte)) {}
221
+ offset++
222
+ }
223
+ await new Promise<void>((resolve) => setImmediate(resolve))
224
+ }
225
+ utf8.decode()
226
+ if (token?.kind === "scalar") finishToken(offset)
227
+ if (token || stack.length || !done) invalid()
228
+ return { items: items.toSorted((a, b) => a.offset - b.offset || b.bytes - a.bytes), truncated }
229
+ }
230
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod"
2
+ import { RolloutAccounting } from "@ericsanchezok/synergy-harness/rollout"
3
+
4
+ export namespace ExecutionPresentation {
5
+ const Estimate = z.object({
6
+ basis: z.enum(["api", "unclassified", "subscription", "historical"]),
7
+ currency: z.literal("USD"),
8
+ known: z.number(),
9
+ maximum: z.number(),
10
+ unknown: z.number().int().nonnegative(),
11
+ })
12
+ export const Cost = z
13
+ .object({
14
+ state: z.enum(["unrecorded", "local", "subscription", "reported", "estimated", "mixed", "partial", "unknown"]),
15
+ reported: z.array(z.object({ currency: z.string(), amount: z.number() })),
16
+ estimates: z.array(Estimate),
17
+ equivalent: Estimate.nullable(),
18
+ missing: z.number().int().nonnegative(),
19
+ historical: z.number().int().nonnegative(),
20
+ knownUSD: z.number(),
21
+ })
22
+ .meta({ ref: "ExecutionCostPresentation" })
23
+ export type Cost = z.infer<typeof Cost>
24
+
25
+ export function cost(summary: RolloutAccounting.Summary): Cost {
26
+ const reported = Object.entries(summary.reported.currencies)
27
+ .toSorted(([a], [b]) => a.localeCompare(b))
28
+ .map(([currency, amount]) => ({ currency, amount }))
29
+ const coverage = summary.costCoverage
30
+ const estimates: z.infer<typeof Estimate>[] = []
31
+ const entry = (
32
+ basis: z.infer<typeof Estimate>["basis"],
33
+ metric: RolloutAccounting.Metric,
34
+ maximum = metric.known,
35
+ ) => ({ basis, currency: "USD" as const, known: metric.known, maximum, unknown: metric.unknown })
36
+ if (coverage) {
37
+ for (const basis of ["api", "unclassified"] as const) {
38
+ const bucket = coverage[basis]
39
+ if (
40
+ (bucket.unreportedRequests ??
41
+ Number(bucket.attempts && (bucket.unreported.known || bucket.unreported.unknown || !reported.length))) > 0
42
+ )
43
+ estimates.push(entry(basis, bucket.unreported, bucket.maximum.known))
44
+ }
45
+ }
46
+ const historicalAmount =
47
+ (coverage
48
+ ? (coverage.historicalAmount ?? 0)
49
+ : !reported.length
50
+ ? summary.apiEstimate.known + summary.unclassifiedEquivalent.known
51
+ : 0) + summary.legacy.cost
52
+ if (historicalAmount)
53
+ estimates.push(entry("historical", { known: historicalAmount, total: historicalAmount, unknown: 0 }))
54
+ const equivalent = coverage?.subscription.attempts
55
+ ? entry(
56
+ "subscription",
57
+ summary.subscriptionEquivalent,
58
+ summary.subscriptionEquivalent.known +
59
+ Math.max(0, coverage.subscription.maximum.known - coverage.subscription.unreported.known),
60
+ )
61
+ : null
62
+ const historical = coverage
63
+ ? coverage.historical + summary.legacy.messages
64
+ : summary.attempts + summary.unobservedCalls + summary.legacy.messages
65
+ const missing = estimates.reduce((count, estimate) => count + estimate.unknown, historical)
66
+ const state: Cost["state"] = missing
67
+ ? reported.length || estimates.some((estimate) => estimate.known)
68
+ ? "partial"
69
+ : "unknown"
70
+ : reported.length
71
+ ? estimates.length
72
+ ? "mixed"
73
+ : "reported"
74
+ : estimates.length
75
+ ? "estimated"
76
+ : equivalent
77
+ ? "subscription"
78
+ : coverage?.local
79
+ ? "local"
80
+ : "unrecorded"
81
+ return {
82
+ state,
83
+ reported,
84
+ estimates,
85
+ equivalent,
86
+ missing,
87
+ historical,
88
+ knownUSD: RolloutAccounting.knownExpense(summary),
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,12 @@
1
+ // Provenance: ../../../../docs/decisions/implemented/bug-fix/2026-10-22-detach-execution-previews.md
2
+ // Local adaptation: build bounded UTF-16 output without retaining source-string backing storage.
3
+ export function preview(value: string): string {
4
+ const codes: number[] = []
5
+ let whitespace = false
6
+ for (let index = 0; index < value.length && codes.length < 240; index++) {
7
+ const space = /\s/.test(value[index]!)
8
+ if (!space || !whitespace) codes.push(space ? 32 : value.charCodeAt(index))
9
+ whitespace = space
10
+ }
11
+ return String.fromCharCode(...codes)
12
+ }
@@ -0,0 +1,118 @@
1
+ import type { ExecutionSchema } from "./schema"
2
+
3
+ export namespace ExecutionProcess {
4
+ function auxiliaryKey(node: ExecutionSchema.Node) {
5
+ if (node.evidenceKind !== "call" || node.kind === "compaction") return
6
+ if (node.usageRole !== "auxiliary" && (!node.modelKind || node.modelKind === "chat")) return
7
+ return JSON.stringify([node.sessionID, node.runID, node.modelKind ?? "chat", node.purpose?.trim() || null])
8
+ }
9
+ export function auxiliaryMembers(nodes: readonly ExecutionSchema.Node[], node: ExecutionSchema.Node) {
10
+ const key = auxiliaryKey(node)
11
+ return key
12
+ ? nodes
13
+ .filter((row) => auxiliaryKey(row) === key)
14
+ .toSorted((a, b) => a.started - b.started || a.id.localeCompare(b.id))
15
+ : []
16
+ }
17
+ export function project(nodes: readonly ExecutionSchema.Node[], mode: "process" | "records", root?: string) {
18
+ const byID = new Map(nodes.map((node) => [node.id, node]))
19
+ const children = new Map<string, ExecutionSchema.Node[]>()
20
+ const auxiliary = new Map<string, ExecutionSchema.Node[]>()
21
+ for (const node of nodes) {
22
+ const key = auxiliaryKey(node)
23
+ if (key) {
24
+ const rows = auxiliary.get(key) ?? []
25
+ rows.push(node)
26
+ auxiliary.set(key, rows)
27
+ }
28
+ if (!node.parentID) continue
29
+ const rows = children.get(node.parentID) ?? []
30
+ rows.push(node)
31
+ children.set(node.parentID, rows)
32
+ }
33
+ const groups = new Map(
34
+ nodes
35
+ .filter((node) => node.evidenceKind === "call")
36
+ .map((node) => {
37
+ const members = children.get(node.id) ?? []
38
+ const attempts = members.filter((child) => child.evidenceKind === "attempt")
39
+ return [
40
+ node.id,
41
+ {
42
+ id: node.id,
43
+ started: node.started,
44
+ callCount: 1,
45
+ memberCount: members.length,
46
+ attemptCount: attempts.length,
47
+ retryCount: attempts.filter((attempt) => (attempt.attemptIndex ?? 0) > 0).length,
48
+ anomalies: members.filter((child) => ["failed", "cancelled", "interrupted"].includes(child.status))
49
+ .length,
50
+ purpose: node.purpose?.trim() || null,
51
+ },
52
+ ] as const
53
+ }),
54
+ )
55
+ const heads = new Map<string, ExecutionSchema.Node>()
56
+ const hidden = new Set<string>()
57
+ if (mode === "process")
58
+ for (const members of auxiliary.values()) {
59
+ if (members.length < 2) continue
60
+ members.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id))
61
+ const first = members[0]!
62
+ for (const member of members.slice(1)) hidden.add(member.id)
63
+ const counters = members.map((node) => groups.get(node.id)!)
64
+ const status = members.some((node) => node.status === "running")
65
+ ? "running"
66
+ : (members.find((node) => ["failed", "cancelled", "interrupted", "unknown"].includes(node.status))?.status ??
67
+ "completed")
68
+ heads.set(first.id, {
69
+ ...first,
70
+ status,
71
+ ended: members.every((node) => node.ended !== undefined)
72
+ ? Math.max(...members.map((node) => node.ended!))
73
+ : undefined,
74
+ group: {
75
+ id: first.id,
76
+ started: first.started,
77
+ callCount: members.length,
78
+ memberCount: counters.reduce((sum, value) => sum + value.memberCount, 0),
79
+ attemptCount: counters.reduce((sum, value) => sum + value.attemptCount, 0),
80
+ retryCount: counters.reduce((sum, value) => sum + value.retryCount, 0),
81
+ anomalies: counters.reduce((sum, value) => sum + value.anomalies, 0),
82
+ purpose: first.purpose?.trim() || null,
83
+ },
84
+ })
85
+ }
86
+ return nodes
87
+ .filter((node) => {
88
+ if (mode === "records") return true
89
+ if (hidden.has(node.id)) return false
90
+ if (node.evidenceKind === "attempt") return (node.attemptIndex ?? 0) > 0
91
+ if (node.evidenceKind !== "call" || node.kind === "compaction" || auxiliaryKey(node)) return true
92
+ return !(children.get(node.id) ?? []).some((child) => ["reasoning", "output", "tool"].includes(child.kind))
93
+ })
94
+ .map((original) => {
95
+ const node = heads.get(original.id) ?? original
96
+ const ancestors: NonNullable<ExecutionSchema.Node["ancestors"]> = []
97
+ const seen = new Set<string>([node.id])
98
+ let parent = node.parentID && byID.get(node.parentID)
99
+ while (parent && !seen.has(parent.id)) {
100
+ seen.add(parent.id)
101
+ ancestors.unshift({ id: parent.id, title: parent.title, kind: parent.kind })
102
+ parent = parent.parentID && byID.get(parent.parentID)
103
+ }
104
+ const rootRunID =
105
+ !root || node.sessionID === root
106
+ ? node.runID
107
+ : (ancestors.map((ancestor) => byID.get(ancestor.id)).find((ancestor) => ancestor?.sessionID === root)
108
+ ?.runID ?? null)
109
+ return {
110
+ ...node,
111
+ ancestors,
112
+ rootRunID,
113
+ attribution: node.attribution ?? (rootRunID === null ? ("unassigned" as const) : ("known" as const)),
114
+ group: node.group ?? groups.get(node.id) ?? (node.parentID ? groups.get(node.parentID) : undefined),
115
+ }
116
+ })
117
+ }
118
+ }