@dxos/react-ui-trace 0.11.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 (148) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +3 -0
  3. package/dist/lib/index.mjs +2868 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/testing.mjs +338 -0
  6. package/dist/lib/testing.mjs.map +1 -0
  7. package/dist/lib/translations.mjs +19 -0
  8. package/dist/lib/translations.mjs.map +1 -0
  9. package/dist/types/src/components/Gantt/Gantt.d.ts +68 -0
  10. package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -0
  11. package/dist/types/src/components/Gantt/Gantt.stories.d.ts +30 -0
  12. package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Gantt/index.d.ts +2 -0
  14. package/dist/types/src/components/Gantt/index.d.ts.map +1 -0
  15. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +32 -0
  16. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -0
  17. package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts +8 -0
  18. package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/ProcessTree/index.d.ts +2 -0
  20. package/dist/types/src/components/ProcessTree/index.d.ts.map +1 -0
  21. package/dist/types/src/components/Timeline/Timeline.d.ts +50 -0
  22. package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -0
  23. package/dist/types/src/components/Timeline/Timeline.stories.d.ts +24 -0
  24. package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -0
  25. package/dist/types/src/components/Timeline/index.d.ts +4 -0
  26. package/dist/types/src/components/Timeline/index.d.ts.map +1 -0
  27. package/dist/types/src/components/Timeline/timeline-options.d.ts +9 -0
  28. package/dist/types/src/components/Timeline/timeline-options.d.ts.map +1 -0
  29. package/dist/types/src/components/Timeline/timeline-printer.d.ts +7 -0
  30. package/dist/types/src/components/Timeline/timeline-printer.d.ts.map +1 -0
  31. package/dist/types/src/components/Timeline/timeline-printer.test.d.ts +2 -0
  32. package/dist/types/src/components/Timeline/timeline-printer.test.d.ts.map +1 -0
  33. package/dist/types/src/components/TracePanel/TracePanel.d.ts +28 -0
  34. package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -0
  35. package/dist/types/src/components/TracePanel/dxn-extractor.d.ts +19 -0
  36. package/dist/types/src/components/TracePanel/dxn-extractor.d.ts.map +1 -0
  37. package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts +2 -0
  38. package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts.map +1 -0
  39. package/dist/types/src/components/TracePanel/index.d.ts +4 -0
  40. package/dist/types/src/components/TracePanel/index.d.ts.map +1 -0
  41. package/dist/types/src/components/TracePanel/trace-filter.d.ts +39 -0
  42. package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -0
  43. package/dist/types/src/components/TracePanel/trace-filter.test.d.ts +2 -0
  44. package/dist/types/src/components/TracePanel/trace-filter.test.d.ts.map +1 -0
  45. package/dist/types/src/components/TracePanel/useTraceMenu.d.ts +15 -0
  46. package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -0
  47. package/dist/types/src/components/index.d.ts +5 -0
  48. package/dist/types/src/components/index.d.ts.map +1 -0
  49. package/dist/types/src/execution-graph/execution-graph.d.ts +139 -0
  50. package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -0
  51. package/dist/types/src/execution-graph/execution-graph.test.d.ts +2 -0
  52. package/dist/types/src/execution-graph/execution-graph.test.d.ts.map +1 -0
  53. package/dist/types/src/execution-graph/index.d.ts +4 -0
  54. package/dist/types/src/execution-graph/index.d.ts.map +1 -0
  55. package/dist/types/src/execution-graph/pending-block-status.d.ts +26 -0
  56. package/dist/types/src/execution-graph/pending-block-status.d.ts.map +1 -0
  57. package/dist/types/src/execution-graph/pending-block-status.test.d.ts +2 -0
  58. package/dist/types/src/execution-graph/pending-block-status.test.d.ts.map +1 -0
  59. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +2 -0
  60. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +1 -0
  61. package/dist/types/src/execution-graph/span-tree.d.ts +106 -0
  62. package/dist/types/src/execution-graph/span-tree.d.ts.map +1 -0
  63. package/dist/types/src/execution-graph/span-tree.test.d.ts +2 -0
  64. package/dist/types/src/execution-graph/span-tree.test.d.ts.map +1 -0
  65. package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts +2 -0
  66. package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts.map +1 -0
  67. package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts +40 -0
  68. package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts.map +1 -0
  69. package/dist/types/src/execution-graph/testing/index.d.ts +2 -0
  70. package/dist/types/src/execution-graph/testing/index.d.ts.map +1 -0
  71. package/dist/types/src/hooks/index.d.ts +4 -0
  72. package/dist/types/src/hooks/index.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useExecutionGraph.d.ts +17 -0
  74. package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useSessionTimeline.d.ts +17 -0
  76. package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTraceMessages.d.ts +17 -0
  78. package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -0
  79. package/dist/types/src/index.d.ts +5 -0
  80. package/dist/types/src/index.d.ts.map +1 -0
  81. package/dist/types/src/session-timeline/index.d.ts +3 -0
  82. package/dist/types/src/session-timeline/index.d.ts.map +1 -0
  83. package/dist/types/src/session-timeline/session-timeline.d.ts +19 -0
  84. package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -0
  85. package/dist/types/src/session-timeline/session-timeline.test.d.ts +2 -0
  86. package/dist/types/src/session-timeline/session-timeline.test.d.ts.map +1 -0
  87. package/dist/types/src/session-timeline/types.d.ts +129 -0
  88. package/dist/types/src/session-timeline/types.d.ts.map +1 -0
  89. package/dist/types/src/testing/index.d.ts +5 -0
  90. package/dist/types/src/testing/index.d.ts.map +1 -0
  91. package/dist/types/src/testing/simulated-agent.d.ts +63 -0
  92. package/dist/types/src/testing/simulated-agent.d.ts.map +1 -0
  93. package/dist/types/src/testing/snapshot-playback.d.ts +12 -0
  94. package/dist/types/src/testing/snapshot-playback.d.ts.map +1 -0
  95. package/dist/types/src/testing/test-trace.d.ts +5 -0
  96. package/dist/types/src/testing/test-trace.d.ts.map +1 -0
  97. package/dist/types/src/translations.d.ts +19 -0
  98. package/dist/types/src/translations.d.ts.map +1 -0
  99. package/dist/types/tsconfig.tsbuildinfo +1 -0
  100. package/package.json +77 -0
  101. package/src/components/Gantt/Gantt.stories.tsx +241 -0
  102. package/src/components/Gantt/Gantt.tsx +589 -0
  103. package/src/components/Gantt/index.ts +5 -0
  104. package/src/components/ProcessTree/ProcessTree.stories.tsx +95 -0
  105. package/src/components/ProcessTree/ProcessTree.tsx +265 -0
  106. package/src/components/ProcessTree/index.ts +5 -0
  107. package/src/components/Timeline/Timeline.stories.tsx +326 -0
  108. package/src/components/Timeline/Timeline.tsx +534 -0
  109. package/src/components/Timeline/git.drawio.svg +821 -0
  110. package/src/components/Timeline/index.ts +7 -0
  111. package/src/components/Timeline/timeline-options.ts +27 -0
  112. package/src/components/Timeline/timeline-printer.test.ts +76 -0
  113. package/src/components/Timeline/timeline-printer.ts +223 -0
  114. package/src/components/TracePanel/TracePanel.tsx +248 -0
  115. package/src/components/TracePanel/dxn-extractor.test.ts +153 -0
  116. package/src/components/TracePanel/dxn-extractor.ts +166 -0
  117. package/src/components/TracePanel/index.ts +7 -0
  118. package/src/components/TracePanel/trace-filter.test.ts +137 -0
  119. package/src/components/TracePanel/trace-filter.ts +120 -0
  120. package/src/components/TracePanel/useTraceMenu.ts +95 -0
  121. package/src/components/index.ts +8 -0
  122. package/src/execution-graph/SPEC.md +749 -0
  123. package/src/execution-graph/execution-graph.test.ts +1205 -0
  124. package/src/execution-graph/execution-graph.ts +1076 -0
  125. package/src/execution-graph/index.ts +35 -0
  126. package/src/execution-graph/pending-block-status.test.ts +118 -0
  127. package/src/execution-graph/pending-block-status.ts +88 -0
  128. package/src/execution-graph/remote-snapshot.test.ts +116 -0
  129. package/src/execution-graph/span-tree.test.ts +370 -0
  130. package/src/execution-graph/span-tree.ts +404 -0
  131. package/src/execution-graph/sub-agent-delegation.test.ts +65 -0
  132. package/src/execution-graph/testing/collect-trace-events.ts +76 -0
  133. package/src/execution-graph/testing/index.ts +5 -0
  134. package/src/execution-graph/testing/sub-agent-delegation.json +1285 -0
  135. package/src/hooks/index.ts +7 -0
  136. package/src/hooks/useExecutionGraph.ts +107 -0
  137. package/src/hooks/useSessionTimeline.ts +53 -0
  138. package/src/hooks/useTraceMessages.ts +58 -0
  139. package/src/index.ts +8 -0
  140. package/src/session-timeline/index.ts +6 -0
  141. package/src/session-timeline/session-timeline.test.ts +662 -0
  142. package/src/session-timeline/session-timeline.ts +605 -0
  143. package/src/session-timeline/types.ts +100 -0
  144. package/src/testing/index.ts +8 -0
  145. package/src/testing/simulated-agent.ts +293 -0
  146. package/src/testing/snapshot-playback.ts +45 -0
  147. package/src/testing/test-trace.ts +22 -0
  148. package/src/translations.ts +29 -0
@@ -0,0 +1,749 @@
1
+ # Trace Execution Graph — Specification
2
+
3
+ This document specifies the pure-data layer that powers the `TracePanel` Timeline view.
4
+ Source: `packages/plugins/plugin-assistant/src/execution-graph/`.
5
+
6
+ ## 1. Purpose & scope
7
+
8
+ The module converts a flat list of `Trace.Message` records (and a snapshot of active
9
+ processes) into a Mercurial/Git-style commit graph consumable by the
10
+ `@dxos/react-ui-components` `Timeline` component.
11
+
12
+ In scope:
13
+
14
+ - Hierarchical grouping of trace events into spans (`buildSpanTree`).
15
+ - Translation of the span tree into commits + branches with explicit parent edges
16
+ (`buildExecutionGraph` → `spanTreeToCommits`).
17
+ - A commit-query DSL (`CommitSelector`) used internally for parent computation.
18
+ - A `details` map exposing the underlying `Trace.FlatEvent` for each commit
19
+ (used by the inspector / Syntax pane).
20
+
21
+ Out of scope:
22
+
23
+ - React rendering, styling, layout.
24
+ - Trace transport, persistence, or feed queries (the panel reads these via ECHO atoms).
25
+ - Filtering / search of commits.
26
+ - Mutating, ack-ing, or replaying trace events.
27
+
28
+ Inputs: `traceMessages: Trace.Message[]`, `activeProcesses?: Process.Info[]`, `eventLimit?: number`,
29
+ `spanTimeoutMs?: number`, `now?: number`.
30
+ Output: `ExecutionGraph = { branches, commits, spanTree, details }`.
31
+
32
+ ## 2. Inputs
33
+
34
+ ### 2.1 `Trace.Message`
35
+
36
+ Defined in `packages/core/compute/compute/src/Trace.ts#L154`.
37
+ A `Message` is `{ meta: Meta, isEphemeral: boolean, events: Event[] }`. The builder
38
+ flattens via `Trace.flatten` (`Trace.ts#L178`) so every event inherits its message's
39
+ `meta` — there is no per-event meta override.
40
+
41
+ `Meta` fields (`Trace.ts#L102`):
42
+
43
+ | Field | Read by the builder | Purpose |
44
+ | ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
45
+ | `pid` | Yes | Identifies the span the event belongs to and the branch the commit lives on. |
46
+ | `parentPid` | Yes | Links a span to its parent span at begin time. |
47
+ | `processName` | No (carried in `SpanMeta` for consumers) | — |
48
+ | `space` | No | — |
49
+ | `conversationId` | No (carried in `SpanMeta`) | — |
50
+ | `triggerId` | No (carried in `SpanMeta`) | — |
51
+ | `toolCallId` | No (carried in `SpanMeta`) | — |
52
+ | `runtimeName` | No (carried in `SpanMeta`) | — |
53
+
54
+ Only `pid` and `parentPid` participate in graph topology. The remaining fields are
55
+ forwarded as `SpanMeta` for downstream display but never branch or anchor logic.
56
+
57
+ `isEphemeral` is preserved on `FlatEvent` but is not consulted; ephemeral events
58
+ participate exactly like persistent events.
59
+
60
+ ### 2.2 `Process.Info` (active processes)
61
+
62
+ Defined in `packages/core/compute/compute/src/Process.ts#L365`.
63
+ Optional input via `activeProcesses` (defaults to `[]`). Fields the builder reads:
64
+
65
+ - `pid` — used both as a commit-id suffix and as the branch name for the spinner.
66
+ - `key` — compared against `AGENT_PROCESS_KEY` (`'org.dxos.testing.process.agent'`,
67
+ `packages/core/compute/functions-runtime/src/agent-service/agent-process.ts#L54`).
68
+ - `state` — gated on `Process.State.RUNNING` and `Process.State.HYBERNATING`
69
+ (spelling matches the runtime; `Process.ts#L321`).
70
+ - `params.target` — non-null required for the agent special case.
71
+
72
+ `parentPid`, `error`, `startedAt`, `completedAt`, `metrics` are ignored.
73
+
74
+ ### 2.3 Configuration
75
+
76
+ - `buildExecutionGraph` defaults `eventLimit = 500`
77
+ (`execution-graph.ts#L94`).
78
+ - `TracePanel`'s `getExecutionGraph` defaults `eventLimit = 300`
79
+ (`TracePanel.tsx#L153`).
80
+ - The cap targets `Timeline` rendering cost, not memory; see §11 for what it bounds.
81
+ - `buildSpanTree` defaults `spanTimeoutMs = DEFAULT_SPAN_TIMEOUT_MS` (20 minutes); `now` has
82
+ no default and force-closing is skipped unless a caller passes it. See §5.4.
83
+
84
+ ## 3. Outputs
85
+
86
+ ### 3.1 `ExecutionGraph`
87
+
88
+ ```ts
89
+ export type ExecutionGraph = {
90
+ branches: string[];
91
+ commits: Commit[];
92
+ spanTree: Span;
93
+ details: ExecutionGraphDetailsMap;
94
+ };
95
+ export type ExecutionGraphDetailsMap = Record<string, Trace.FlatEvent | undefined>;
96
+ ```
97
+
98
+ - `branches` — every branch name a commit was placed on; `'main'` is always present
99
+ (the builder seeds it). Order is insertion order from the underlying `Set`.
100
+ - `commits` — flat list, may be unordered (consumers should not rely on order; the
101
+ `Timeline` sorts by parent topology). Deduped by id.
102
+ - `spanTree` — the hierarchical intermediate representation; exposed for debug views.
103
+ - `details` — commit id → originating `Trace.FlatEvent`. Used by `TracePanel` to
104
+ render the Syntax pane when a commit is selected (`TracePanel.tsx#L118`). Only
105
+ commits that were emitted via `presentEvent` appear here.
106
+
107
+ ### 3.2 `Commit` (set fields only)
108
+
109
+ From `packages/ui/react-ui-components/src/components/Timeline/Timeline.tsx#L40`. The
110
+ builder sets:
111
+
112
+ - `id` — see §9.
113
+ - `branch` — pid-derived (see §6).
114
+ - `parents` — see §8.
115
+ - `timestamp` — `new Date(event.timestamp)` (events) or `new Date()` (spinners).
116
+ - `icon`, `level`, `message` — from `presentEvent`.
117
+
118
+ The builder does not set `tags` or `link`. `CommitSelector.tag` / `.anyTags` exist
119
+ for consumers that add their own tags downstream.
120
+
121
+ ## 4. Event classification
122
+
123
+ Two type sets defined in `span-tree.ts#L47`:
124
+
125
+ ```ts
126
+ export const BEGIN_EVENT_TYPES = new Set([Trace.OperationStart.key, AgentRequestBegin.key]);
127
+ export const END_EVENT_TYPES = new Set([Trace.OperationEnd.key, AgentRequestEnd.key]);
128
+ ```
129
+
130
+ - **Begin events**: `operation.start`, `assistant.agentRequestBegin`. Open a span.
131
+ - **End events**: `operation.end`, `assistant.agentRequestEnd`. Close a span.
132
+ - **Middle events**: every other event type carried by a `meta.pid`. Attached to the
133
+ currently-open span for that pid.
134
+ - **Pid-less events**: any event whose `meta.pid` is `undefined`. Attached to the
135
+ synthetic root span.
136
+
137
+ ### 4.1 Which events produce commits (`presentEvent`)
138
+
139
+ `execution-graph.ts#L118`. Each branch validates `event.data` against the
140
+ event-type schema; on validation failure the event is silently dropped (log only).
141
+
142
+ | Event type | Condition | Icon | Level | Message | `idSuffix` |
143
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------- | ------------------------------- | ------------------- |
144
+ | `AgentRequestBegin` | — | `ph--atom--regular` | `VERBOSE` | `"Agent processing request..."` | — |
145
+ | `AgentRequestEnd` | — | `ph--atom--regular` | `INFO` | `"Agent completed request"` | — |
146
+ | `CompleteBlock` text | `role === 'user'` | `ph--user--regular` | `VERBOSE` | trimmed `block.text` | — |
147
+ | `CompleteBlock` text | `role !== 'user'` | — (drop) | — | — | — |
148
+ | `CompleteBlock` status | — | `ph--info--regular` | `VERBOSE` | trimmed `block.statusText` | — |
149
+ | `CompleteBlock` other | — | — (drop) | — | — | — |
150
+ | `Trace.OperationStart` | — | `ph--function--regular` | `VERBOSE` | `data.name ?? data.key` | `${data.key}:start` |
151
+ | `Trace.OperationEnd` (success) | `outcome === 'success'` | `ph--function--regular` | `INFO` | `${name ?? key} - Success` | `${data.key}:end` |
152
+ | `Trace.OperationEnd` (incomplete) | `outcome === 'failure' && (errorCode === RUN_AGAIN_ERROR_CODE \|\| (errorCode === undefined && error === RUN_AGAIN_MESSAGE))` | `ph--arrows-clockwise--regular` | `WARN` | `${name ?? key} - Incomplete` | `${data.key}:end` |
153
+ | `Trace.OperationEnd` (failure) | otherwise | `ph--function--regular` | `ERROR` | `${name ?? key} - Error` | `${data.key}:end` |
154
+ | anything else | — | — (drop) | — | — | — |
155
+
156
+ `trimText` clips to first 100 chars, trims whitespace, drops everything after the
157
+ first newline (`execution-graph.ts#L438`).
158
+
159
+ Dropped events (non-`presentEvent` types or filtered roles) never appear in
160
+ `commits` or `details`, but they remain in `spanTree.events`.
161
+
162
+ ## 5. Span tree construction (`buildSpanTree`)
163
+
164
+ `span-tree.ts#L112`. Algorithm:
165
+
166
+ 1. **Flatten** every message: `messages.flatMap(Trace.flatten)`. Each output is a
167
+ `FlatEvent` carrying the message's `meta` and `isEphemeral`.
168
+ 2. **Sort** the flat stream chronologically by `event.timestamp` ascending. The
169
+ sort is in-place and uses subtraction (no NaN guard); identical timestamps
170
+ keep their relative order as `Array.prototype.sort` is stable in modern V8.
171
+ 3. **Apply `eventLimit`** (see §5.2). Boundary events are immune.
172
+ 4. **Linear scan** the (possibly trimmed) stream:
173
+ - Pid-less event → push onto root.
174
+ - Begin event with pid `P`, parentPid `Q`:
175
+ - Parent span = `openSpans.get(Q)` if `Q` is defined and open, else root.
176
+ - Allocate a fresh id `${P}#${spanCounter++}` and create the span.
177
+ - Push the begin event as the span's first event.
178
+ - **`openSpans.set(P, newSpan)`** — supersedes any previously open span for
179
+ this pid.
180
+ - End event with pid `P`:
181
+ - If a span is open for `P`: push the end event onto it and `openSpans.delete(P)`.
182
+ - Else: push the end event onto root (orphan end).
183
+ - Other event with pid `P`:
184
+ - If a span is open for `P`: push onto it.
185
+ - Else: push onto root.
186
+ 5. **Sort children** of every span by `firstTimestamp` ascending (depth-first).
187
+ Determinism guarantee.
188
+
189
+ The `MutableSpan.firstTimestamp` is updated on every push via `noteTimestamp`
190
+ (`Math.min` against the current value, init `+Infinity`). Used only for sibling
191
+ ordering.
192
+
193
+ Spans are frozen via `freezeSpan` into the public `Span` shape; the
194
+ `firstTimestamp` field is internal and not exposed.
195
+
196
+ ### 5.1 Same-pid sequential spans
197
+
198
+ Multiple complete begin/end pairs sharing a `pid` become **sibling spans under the
199
+ same parent**. The second begin orphans the first because `openSpans.set(pid, …)`
200
+ unconditionally supersedes. This is intended for _sequential_ execution within
201
+ one process (e.g. successive agent requests in a session) — not interleaved.
202
+ Each sibling receives a distinct synthetic id (`${pid}#${n}`); the shared `pid`
203
+ is preserved in `SpanMeta` and used for branch derivation (§6) so siblings share
204
+ a lane.
205
+
206
+ Test reference: `span-tree.test.ts#L80` ('sequential agent requests in one process
207
+ become sibling spans').
208
+
209
+ ### 5.2 `eventLimit` semantics
210
+
211
+ `applyEventLimit` (`span-tree.ts#L211`) — runs _after_ chronological sort, _before_
212
+ tree construction.
213
+
214
+ Rule: only **non-boundary** events count toward `eventLimit`. Span boundaries
215
+ (`OperationStart` / `OperationEnd` / `AgentRequestBegin` / `AgentRequestEnd`) are
216
+ **always retained** regardless of the cap.
217
+
218
+ Why: dropping a begin event detaches every descendant whose `parentPid` lookup
219
+ fails — those children would fall back to root and re-render as top-level spans.
220
+ Dropping an end leaves a span open and corrupts the open-span map for any later
221
+ same-pid sibling. The motivating regression is documented in `remote-snapshot.test.ts`
222
+ (commit `eae07dad83`, "enhance event limit handling").
223
+
224
+ When the non-boundary count exceeds `eventLimit`, the _oldest_ non-boundary events
225
+ are dropped while preserving chronological order. Test reference:
226
+ `span-tree.test.ts#L134` ('eventLimit drops oldest non-boundary events but always
227
+ retains span boundaries') and `#L159` (regression for parent-child preservation).
228
+
229
+ ### 5.3 Children ordering
230
+
231
+ Sibling spans are sorted by `firstTimestamp` ascending so output is deterministic
232
+ across runs that share inputs. Test: `span-tree.test.ts#L120`.
233
+
234
+ ### 5.4 Timeout-based force-close
235
+
236
+ Runs after the linear scan, before children are sorted, over `allSpans` — every span ever
237
+ opened, not just the ones still in `openSpans`. This matters because a span superseded by a
238
+ later same-pid begin event (§15 "interleaved same-pid begin/end pairs") is removed from
239
+ `openSpans` but never receives an end event either; walking `allSpans` and checking each
240
+ span's own last event (rather than map membership) reaches it too.
241
+
242
+ Skipped entirely when `options.now` is omitted (see below). Otherwise, any span whose **last**
243
+ event is not itself an end event (`isSpanEndEvent`) and is older than `now - spanTimeoutMs` is
244
+ force-closed with a synthetic end event appended to `span.events`:
245
+
246
+ - Span opened by `AgentRequestBegin` → synthetic `AgentRequestEnd` with
247
+ `{ status: 'interrupted', error: SPAN_TIMEOUT_MESSAGE }`.
248
+ - Span opened by `Trace.OperationStart` → synthetic `Trace.OperationEnd` with
249
+ `{ key, name, icon, outcome: 'failure', error: SPAN_TIMEOUT_MESSAGE }` (fields copied
250
+ from the begin event's payload).
251
+
252
+ The synthetic event's `timestamp` is `lastEvent.timestamp + spanTimeoutMs` (the exact
253
+ moment the span crossed the threshold), not `now` — this keeps the emitted commit's
254
+ position stable across repeated `buildSpanTree` calls with an advancing `now`, rather
255
+ than jumping forward on every recompute.
256
+
257
+ Rationale: the runtime may fail to write the closing event (crash, dropped connection,
258
+ process killed) — see DX-1080. Without this, `isCompletedSpan` never returns `true` for
259
+ that span (§7), so it never collapses and keeps rendering with the "in progress" shimmer
260
+ forever. Appending a synthetic end event makes it indistinguishable downstream from a
261
+ span that completed normally, except for the `outcome: 'failure'` / `status: 'interrupted'`
262
+ result and the `SPAN_TIMEOUT_MESSAGE` text surfaced by `presentEvent` (§4.1).
263
+
264
+ `spanTimeoutMs` defaults to `DEFAULT_SPAN_TIMEOUT_MS` (20 minutes) when `now` is provided.
265
+ `now` has **no default** — force-closing only runs when a caller explicitly passes it, so
266
+ deterministic callers (tests, `TracePanel.stories.tsx`'s fixture snapshot) that pass fixture
267
+ timestamps unrelated to wall-clock time are unaffected unless they opt in. `TracePanel` opts
268
+ in by ticking a `now` state every 60s (`TracePanel.tsx`); `span-tree.test.ts`'s "force-closed"
269
+ tests pass an explicit `now` for determinism.
270
+
271
+ Only spans with **zero** events never happen (a span always has at least its begin
272
+ event), so `span.events[span.events.length - 1]` is always defined when force-closing.
273
+ Nested children of a force-closed span are evaluated independently — a still-active
274
+ child is not force-closed just because its parent timed out.
275
+
276
+ ## 6. Span branch assignment (`branchOf`)
277
+
278
+ `execution-graph.ts#L263`. Pure function on a span:
279
+
280
+ 1. `span === null` or `span.id === ROOT_SPAN_ID` → `MAIN_BRANCH = 'main'`.
281
+ 2. Else → `span.meta.pid ?? span.id`.
282
+
283
+ Consequence: sequential same-pid sibling spans share a branch (their `meta.pid`
284
+ matches). Spans whose meta lacks a `pid` (synthetic / unusual cases) fall back to
285
+ the synthetic span id — guaranteeing a non-empty branch name.
286
+
287
+ The synthetic root id is `ROOT_SPAN_ID = '<main>'` (`span-tree.ts#L12`).
288
+
289
+ ## 7. Collapsibility (`isCollapsibleSpan`)
290
+
291
+ `execution-graph.ts#L206`. A span is collapsible iff **all** of:
292
+
293
+ 1. `span.id !== ROOT_SPAN_ID`.
294
+ 2. `span.events.length === 2`.
295
+ 3. `span.children.length === 0`.
296
+ 4. `events[0]` is a begin event (`isSpanBeginEvent`).
297
+ 5. `events[events.length-1]` is an end event (`isSpanEndEvent`).
298
+ 6. `parent !== null` (i.e. the span has a parent in the tree).
299
+
300
+ Implication: a span whose end event hasn't arrived yet (pending) **never**
301
+ collapses, because its trailing event is a middle event, not an end. This matters
302
+ because the trailing middle event would otherwise be promoted to the parent
303
+ branch as a fake "end" commit — corrupting the visual. See test
304
+ `execution-graph.test.ts#L296` ('pending span with user message → user lands on
305
+ own branch, not parent').
306
+
307
+ Top-level spans (parent === root) **are** eligible — guard #6 only forbids
308
+ parent-less spans. A top-level operation with no inner events collapses to a
309
+ single end commit on `main` (test `execution-graph.test.ts#L194`).
310
+
311
+ ## 8. Commit anchoring rules (`spanTreeToCommits`)
312
+
313
+ `execution-graph.ts#L239`. Walks the entire tree once via `walkSpanTree`, flattens
314
+ into a single chronological stream (sorted by `timestamp`, ties broken by
315
+ `globalIndex` = insertion order), then emits commits in that order. Each
316
+ emission depends on the event's role and the host span's collapsibility:
317
+
318
+ ### 8.1 Root-level events (pid-less or orphans)
319
+
320
+ ```
321
+ branch = 'main'
322
+ parents = lastInSpanContext(null) // i.e. tail of main
323
+ ```
324
+
325
+ ### 8.2 Collapsible span — begin event
326
+
327
+ Skipped entirely. Only the end commit is emitted.
328
+
329
+ ### 8.3 Collapsible span — end event
330
+
331
+ ```
332
+ branch = parentBranch
333
+ parents = lastInSpanContext(parentSpan)
334
+ ```
335
+
336
+ Anchors to the parent's structural context (own-branch tail → parent's begin →
337
+ recurse). Critically, it does _not_ anchor to the immediate parent-branch tail,
338
+ because that tail could be a sibling span's commit; structural anchoring keeps
339
+ the fork visually attached to the correct span identity.
340
+
341
+ ### 8.4 Non-collapsible begin event
342
+
343
+ ```
344
+ isProcessBoundary = span.meta.pid != null
345
+ && parentSpan.meta.pid != null
346
+ && span.meta.pid !== parentSpan.meta.pid
347
+ branch = isProcessBoundary ? ownBranch : parentBranch
348
+ parents = lastInSpanContext(parentSpan)
349
+ ```
350
+
351
+ For a nested sub-span within the same process (or a span with no pid of its own),
352
+ the begin commit lives on the **parent** branch — the fork only becomes visible at
353
+ the span's first middle commit. For a **child process** (its own pid, differing
354
+ from the parent's — e.g. a delegated sub-agent), the begin commit instead forks
355
+ onto the span's **own** branch immediately, so a concurrent process gets its own
356
+ lane from its first event rather than sharing the parent's lane until its first
357
+ middle commit (which otherwise made the lane "snap" across as events streamed in).
358
+ Test: `execution-graph.test.ts` → "child process (sub-agent) forks onto its own branch…".
359
+
360
+ After emission: `beginCommitIdBySpan.set(span.id, commitId)` so any future
361
+ `lastInSpanContext(span)` call can fall back to this commit.
362
+
363
+ ### 8.5 Non-collapsible end event — two parents
364
+
365
+ ```
366
+ branch = parentBranch
367
+ parents = unionAll(
368
+ last commit on parentBranch, // chronological continuity on parent
369
+ last commit on ownBranch, // merge sub-flow back in
370
+ )
371
+ ```
372
+
373
+ Parent #1 is the **immediate predecessor on the parent branch**, not the matching
374
+ begin commit. Pointing back to the begin would draw an edge that skips over any
375
+ sibling spans' commits emitted between begin and end (e.g. two top-level Routine
376
+ spans overlapping on `main`: R1.end would otherwise arrow back to R1.begin
377
+ crossing over R2.begin). Anchoring to the previous parent-branch commit keeps
378
+ main linear. Regression: commit `ff8db16d9c`, test
379
+ `remote-snapshot.test.ts#L49`.
380
+
381
+ ### 8.6 Middle event
382
+
383
+ ```
384
+ branch = ownBranch
385
+ parents = lastInSpanContext(span)
386
+ ```
387
+
388
+ The first middle event of a span thereby forks from its own begin commit
389
+ (via `lastInSpanContext` fallback #2) rather than from a sibling's commit on
390
+ some shared ancestor.
391
+
392
+ ### 8.7 `lastInSpanContext(span)` helper
393
+
394
+ `execution-graph.ts#L282`. Structural (pid-keyed) walk, _not_ topological:
395
+
396
+ ```ts
397
+ lastInSpanContext(null) = branch('main').compose(last());
398
+ lastInSpanContext(span) = firstOf(
399
+ branch(branchOf(span)).compose(last()), // (a) own-branch tail
400
+ beginCommitIdBySpan.get(span.id), // (b) span's own begin commit
401
+ lastInSpanContext(parent(span)), // (c) recurse to parent
402
+ );
403
+ ```
404
+
405
+ Why structural and not topological: if a sub-span of agent A fires before A has
406
+ emitted any middle commits, walking the parent's shared branch topologically
407
+ could land on a commit belonging to an unrelated sibling B (a different top-level
408
+ span sharing `main`). Structural anchoring guarantees the fork edge attaches to
409
+ A's identity (`A.begin`) regardless of B's interleaving. Commit `ff8db16d9c`.
410
+
411
+ ## 9. Commit id format (`formatCommitId`)
412
+
413
+ `execution-graph.ts#L433`:
414
+
415
+ ```
416
+ ${span.id}:${globalIndex} // base
417
+ ${span.id}:${globalIndex}:${suffix} // when EventPresentation.idSuffix is set
418
+ ```
419
+
420
+ `globalIndex` is the index of the event in the post-sort flat stream — unique by
421
+ construction. `idSuffix` comes from `presentEvent`:
422
+
423
+ - `OperationStart` → `${data.key}:start`
424
+ - `OperationEnd` → `${data.key}:end`
425
+ - everything else → none
426
+
427
+ The suffix exists so a span hosting multiple sub-operations sharing the same
428
+ parent span id but different operation keys still produces distinct commit ids,
429
+ which the `Timeline` requires.
430
+
431
+ ## 10. Active-process "running" indicators
432
+
433
+ After commit emission, `spanTreeToCommits` appends spinner commits for each
434
+ process in `activeProcesses` (`execution-graph.ts#L397`):
435
+
436
+ ### 10.1 Agent special case
437
+
438
+ When **all** of:
439
+
440
+ - `process.key === AGENT_PROCESS_KEY` (`'org.dxos.testing.process.agent'`),
441
+ - `process.params.annotations` carries a `Process.TargetAnnotation` value,
442
+ - `process.state ∈ { RUNNING, HYBERNATING }`,
443
+
444
+ emit:
445
+
446
+ ```ts
447
+ {
448
+ id: `running:${process.pid}`,
449
+ branch: process.pid,
450
+ parents: last commit on branch(process.pid),
451
+ icon: 'ph--spinner-gap--regular',
452
+ level: VERBOSE,
453
+ message: 'Generating...',
454
+ timestamp: new Date(),
455
+ }
456
+ ```
457
+
458
+ The branch is added by `addCommit` if it didn't already exist. This is the only
459
+ case where a spinner can introduce a new branch from thin air — the agent
460
+ process's pid always identifies a known conversational lane.
461
+
462
+ ### 10.2 Generic running process
463
+
464
+ For any other process with `state === RUNNING` **and** `builder.hasBranch(process.pid)`,
465
+ emit the same shape with `message: 'Running...'`.
466
+
467
+ The branch-exists guard prevents fabricating empty branches for processes that
468
+ have not emitted any trace events yet. (The agent case bypasses this guard
469
+ because the agent's branch is its conversation lane, which we want surfaced
470
+ even when no events have arrived.)
471
+
472
+ `HYBERNATING` (note the runtime spelling) qualifies for the agent spinner but
473
+ not for the generic one — the generic case demands strictly `RUNNING`.
474
+
475
+ ## 11. Graph hygiene (`GraphBuilder.doctor`)
476
+
477
+ `execution-graph.ts#L610`. Run once at `build()`:
478
+
479
+ 1. **Dedupe commits by `id`** via `Array.dedupeWith`. The first occurrence wins;
480
+ subsequent commits with the same id are dropped. `addCommit` is purely
481
+ append-only, so dedupe is the safety net when an event slips past the unique
482
+ `globalIndex` guarantee (e.g. spinner ids reused if two snapshots collide on
483
+ the same pid — not currently possible but cheap to guard).
484
+ 2. **Drop dangling parents** — for each surviving commit, filter `parents` to
485
+ ids that exist in the deduped commit set.
486
+
487
+ This matters when `eventLimit` trimming leaves an end commit whose own-branch
488
+ tail (the merge parent) was dropped — without doctor, the timeline component
489
+ would receive a parent id that doesn't resolve. Also defensive against partial
490
+ fixtures used in tests / Storybook stories.
491
+
492
+ `addBranch` is invoked by `addCommit` (`#L592`), so a branch is only present in
493
+ `branches` if at least one commit was written to it. `addBranch` is also called
494
+ explicitly for `MAIN_BRANCH` at the very start so `branches` always contains
495
+ `'main'`.
496
+
497
+ ## 12. `CommitSelector` DSL
498
+
499
+ `execution-graph.ts#L442`. A `CommitSelector` is a pipeable wrapper around
500
+ `(commits: Commit[]) => Commit[]`. All selectors are pure and never mutate the
501
+ input.
502
+
503
+ | Combinator | Semantics |
504
+ | ----------------------- | ------------------------------------------------------------------------------------------------- |
505
+ | `make(fn)` | Wrap an arbitrary selector function; sets up `.pipe`. |
506
+ | `identity()` | Returns the input array unchanged. |
507
+ | `filter(pred)` | Keeps commits where `pred(commit)` is truthy. |
508
+ | `id(id)` | `filter(c => c.id === id)`. |
509
+ | `tag(t)` | `filter(c => t && c.tags?.includes(t))`. Falsy `t` (`undefined` / `null` / `false`) yields empty. |
510
+ | `anyTags(ts)` | `filter(c => ts.some(t => t && c.tags?.includes(t)))`. |
511
+ | `branch(b)` | `filter(c => b && c.branch === b)`. Falsy `b` yields empty (same foot-gun as `tag`). |
512
+ | `compose(next)` | Pipe: `next(prev(commits))`. Used as `prev.pipe(compose(next))`. |
513
+ | `orElse(next)` | If `prev` is non-empty return `prev`, else `next`. Equivalent to `firstOf(prev, next)`. |
514
+ | `andAlso(next)` | Alias for `unionAll(prev, next)`. |
515
+ | `first(n=1)` | `commits.slice(0, n)`. |
516
+ | `last(n=1)` | `commits.toReversed().slice(0, n)` — **note reversed order**. |
517
+ | `not(sel)` | `commits.filter(c => !sel(commits).includes(c))`. |
518
+ | `unionAll(...sels)` | Concat results, dedupe by `id`. |
519
+ | `intersectAll(...sels)` | First selector's hits that also appear in every other selector's hits (by id). |
520
+ | `firstOf(...sels)` | First selector that returns a non-empty result; else empty. |
521
+
522
+ Foot-guns:
523
+
524
+ - `last(n)` returns commits in **reverse chronological order** (most recent first).
525
+ Callers wanting the "single most recent commit on branch X" use
526
+ `branch(X).pipe(compose(last()))` and the array's first element. Internally
527
+ `computeParents` only consumes the ids and dedupes (`#L599`), so reverse-order
528
+ is invisible there.
529
+ - `tag` / `branch` accept falsy inputs intentionally (the union type is
530
+ `string | Falsy`) so `tag(maybeUndefined)` is a no-op rather than a type error.
531
+ - All selectors are immutable; `identity().select(input)` does not mutate
532
+ `input` (`execution-graph.test.ts#L56`).
533
+
534
+ ## 13. Edge cases (exhaustive)
535
+
536
+ Each entry: **scenario → expected output → rationale**.
537
+
538
+ 1. **Empty `traceMessages`**
539
+ - Tree: root span with no events, no children.
540
+ - `commits = []`, `branches = ['main']`.
541
+ - Why: `MAIN_BRANCH` is seeded by `addBranch` (`execution-graph.ts#L245`)
542
+ even when no commits exist. Test: `execution-graph.test.ts#L188`.
543
+
544
+ 2. **Orphan end event** (end with no matching begin)
545
+ - Attached to root in `spanTree`; produces a single root-level commit on
546
+ `main`. Test: `span-tree.test.ts#L114`.
547
+
548
+ 3. **Orphan middle event** (no open span for pid)
549
+ - Attached to root; commit emitted on `main` (if `presentEvent` returns
550
+ non-undefined). Test: `span-tree.test.ts#L107`.
551
+
552
+ 4. **Two top-level spans overlapping in time, different pids**
553
+ - Two distinct branches off `main`; `main` stays linear; each end commit
554
+ merges its own-branch tail without skip-back edges across the sibling's
555
+ begin. Regression: commit `ff8db16d9c`, test `remote-snapshot.test.ts#L49`.
556
+
557
+ 5. **Two top-level spans sequential, same pid**
558
+ - Sibling spans under root sharing the same branch (`branchOf` returns the
559
+ shared `pid`). The second begin's parent commit is the tail of that shared
560
+ branch — i.e. the first end commit on `main` plus the own-branch tail
561
+ (which is the first end on the same lane). Test:
562
+ `span-tree.test.ts#L80`; visual contract in
563
+ `trace-timeline.node.test.ts#L51` ('create objects via AgentService').
564
+
565
+ 6. **Multiple sequential agent requests on one pid; sub-ops in the second session**
566
+ - Sub-op commits in the second session chain off the first session's
567
+ own-branch tail (because `branchOf` returns the shared pid). Visual
568
+ contract: `trace-timeline.node.test.ts#L122` ('sequential prompts in a
569
+ session') — note all three sessions share the `agent-1` branch lane.
570
+
571
+ 7. **Parent begin dropped by `eventLimit` (pre-fix)**
572
+ - Before commit `eae07dad83`: the parent's begin event was sliced away when
573
+ verbose noise pushed it past the window. All descendants then re-parented
574
+ to root and rendered as top-level collapsed end commits on `main`.
575
+ - After the fix: boundaries are immune; the parent stays open and children
576
+ remain nested. Regression: `remote-snapshot.test.ts#L27`.
577
+
578
+ 8. **Sub-op fires before parent has any middle commits**
579
+ - Sub-op's begin event anchors to its parent's `lastInSpanContext`. Lookup
580
+ order: parent's own-branch tail (absent) → parent's `beginCommitIdBySpan`
581
+ (present, this wins) → recurse to grandparent.
582
+ - Net effect: fork edge attaches to parent's begin commit.
583
+
584
+ 9. **Sub-op fires after a sibling top-level span begins**
585
+ - Sub-op of A still anchors structurally to A (via `lastInSpanContext`),
586
+ not to whatever B-belonging commit happens to be the topological tail on
587
+ the shared ancestor branch. Commit `ff8db16d9c`.
588
+
589
+ 10. **Collapsible span at root**
590
+ - `isCollapsibleSpan` returns `true` for top-level completed spans (parent
591
+ is root, not null). The span collapses to a single end commit on `main`.
592
+ Test: `execution-graph.test.ts#L194` ('top-level operation with no inner
593
+ events → collapsed to single end commit on main').
594
+
595
+ 11. **Active process emits no trace events but appears in `activeProcesses`**
596
+ - Not the agent special case → no spinner (the branch doesn't exist, so the
597
+ `hasBranch` guard rejects). The process is invisible to the timeline.
598
+ - Is the agent special case → spinner appears on a brand-new branch named
599
+ after the pid.
600
+
601
+ 12. **Identical timestamps on multiple events**
602
+ - Tie-break by `globalIndex` (insertion order before sort). Two layers:
603
+ `buildSpanTree` sorts the entire flat stream by `timestamp` only (stable
604
+ sort preserves insertion order); `spanTreeToCommits` sorts its event
605
+ stream by `timestamp` with explicit `globalIndex` tie-break
606
+ (`execution-graph.ts#L309`).
607
+ - Determinism is therefore guaranteed when input order is deterministic.
608
+
609
+ 13. **`Trace.flatten` meta inheritance**
610
+ - Every `FlatEvent` carries its message's `meta` verbatim. There is no
611
+ per-event override mechanism. Events that need different meta must be
612
+ emitted as separate `Trace.Message`s.
613
+
614
+ 14. **Pending span (begin without end)**
615
+ - Span is not collapsible (no trailing end event). Begin produces a fork
616
+ commit on the parent branch; middle events produce commits on the span's
617
+ own branch; no end commit is emitted. Test:
618
+ `execution-graph.test.ts#L296`.
619
+
620
+ 15. **`CompleteBlock` with `role !== 'user'` text block**
621
+ - Dropped — no commit, no `details` entry. The event remains in
622
+ `spanTree.events`.
623
+
624
+ 16. **`CompleteBlock` with unhandled block `_tag`**
625
+ - Dropped (neither `text` nor `status`).
626
+
627
+ 17. **Schema-invalid event payload**
628
+ - Logged via `log('invalid trace event', …)` and dropped. The event still
629
+ occupies a position in `spanTree.events`.
630
+
631
+ 18. **Span open longer than `spanTimeoutMs` with no end event** (DX-1080)
632
+ - Force-closed with a synthetic failure/interrupted end event (§5.4); becomes
633
+ `isCompletedSpan` and collapses/renders like a normal completed span instead of
634
+ hanging with the shimmer effect forever. Test: `span-tree.test.ts`
635
+ ("an open operation span past the timeout is force-closed…").
636
+
637
+ 19. **Span open but still within `spanTimeoutMs`**
638
+ - Left untouched — stays open and continues to render as in-progress. Test:
639
+ `span-tree.test.ts` ("an open span within the timeout window stays open").
640
+
641
+ ## 14. Performance & complexity
642
+
643
+ - `buildSpanTree`: O(E log E) for the chronological sort, O(E) for the linear
644
+ scan, O(S log S) total over child sorts (S = span count). Memory O(E).
645
+ - `spanTreeToCommits`: O(E) walk of the tree + O(E log E) sort of the flat
646
+ stream. Commit emission is O(E) wall, but each emission's parent computation
647
+ is O(C) where C = current commit count (the selector linearly scans all
648
+ commits). Worst-case overall: O(E·C) — but in practice C ≪ E because most
649
+ events are dropped middle events with no `presentEvent` mapping, and
650
+ `lastInSpanContext` recursion depth equals tree depth D (typically ≤ 3-4).
651
+ - Expected scale: hundreds of commits, thousands of events. `eventLimit`
652
+ exists to bound `Timeline` rendering cost (DOM nodes, layout), not memory.
653
+
654
+ ## 15. Known limitations & future work
655
+
656
+ - **Pending spans + active processes**: an end-commit-less span surfaces as
657
+ "Generating..." only for processes matching `AGENT_PROCESS_KEY`. Other
658
+ long-running spans must wait for their begin commit to land and the
659
+ generic `RUNNING + hasBranch` heuristic to trigger.
660
+ - **Interleaved same-pid begin/end pairs**: the data model assumes _sequential_
661
+ spans per pid. If a process emits `B1 B2 E1 E2` interleaved, the second `B2`
662
+ orphans the still-open `B1` (no `E1` ever closes the first span). The first
663
+ span receives only its begin event; the second receives `B2 E1 E2`.
664
+ Outcome: incorrect. Not currently supported. `B1` is at least reachable by the
665
+ §5.4 timeout sweep (via `allSpans`) so it eventually force-closes rather than
666
+ hanging forever — but the interleaving itself is still not modeled correctly.
667
+ - **Event types beyond `presentEvent`**: silently dropped from the commit
668
+ stream but retained in `spanTree.events`. Consumers wanting the full event
669
+ stream must read `spanTree`. Examples: `OperationInput`, `OperationOutput`,
670
+ `StatusUpdate`, `process.spawned`, `process.exited`, `assistant.mcpServerError`.
671
+ - **`details` only stores presented events**. Selecting a commit in TracePanel
672
+ shows that single event; there is no per-span aggregation surfaced via
673
+ `details`.
674
+ - **Same-pid sub-spans of one process**: a process that re-uses its own pid
675
+ for an inner operation (without supplying a distinct child pid via
676
+ `parentPid`) cannot be distinguished from sequential top-level spans.
677
+ - **Schema validation is silent**: invalid event payloads are dropped without
678
+ surfacing to the UI; only `log` records the drop.
679
+
680
+ ## Appendix A: Source citations
681
+
682
+ | Symbol | File | Line |
683
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------- |
684
+ | `buildSpanTree` | `packages/plugins/plugin-assistant/src/execution-graph/span-tree.ts` | L112 |
685
+ | `applyEventLimit` | same | L211 |
686
+ | `makeTimeoutEndEvent` / `DEFAULT_SPAN_TIMEOUT_MS` | same | L252 / L13 |
687
+ | `ROOT_SPAN_ID` | same | L12 |
688
+ | `BEGIN_EVENT_TYPES`/`END_EVENT_TYPES` | same | L47 |
689
+ | `walkSpanTree`/`flattenSpanTree` | same | L245 |
690
+ | `buildExecutionGraph` | `packages/plugins/plugin-assistant/src/execution-graph/execution-graph.ts` | L91 |
691
+ | `presentEvent` | same | L118 |
692
+ | `isCollapsibleSpan` | same | L206 |
693
+ | `spanTreeToCommits` | same | L239 |
694
+ | `lastInSpanContext` | same | L282 |
695
+ | `formatCommitId` | same | L433 |
696
+ | `CommitSelector` | same | L446 |
697
+ | `GraphBuilder` / `doctor` | same | L579 / L610 |
698
+ | `Trace.Meta` / `Trace.flatten` | `packages/core/compute/compute/src/Trace.ts` | L102 / L178 |
699
+ | `Process.Info` / `Process.State` | `packages/core/compute/compute/src/Process.ts` | L365 / L321 |
700
+ | `AGENT_PROCESS_KEY` | `packages/core/compute/functions-runtime/src/agent-service/agent-process.ts` | L54 |
701
+ | `Commit` type | `packages/ui/react-ui-components/src/components/Timeline/Timeline.tsx` | L40 |
702
+ | `TracePanel` `useExecutionGraph` / default `eventLimit` | `packages/plugins/plugin-assistant/src/containers/TracePanel/TracePanel.tsx` | L138 / L153 |
703
+
704
+ ## Appendix B: Visual contract examples
705
+
706
+ Top-level operation with no inner events (collapsed) —
707
+ `execution-graph.test.ts#L194`:
708
+
709
+ ```
710
+ ● [function] Reply - Success
711
+ ```
712
+
713
+ Nested operation under an agent (sub-op collapsed) —
714
+ `execution-graph.test.ts#L223`:
715
+
716
+ ```
717
+ ● [atom] Agent processing request...
718
+ ├──● [user] hello
719
+ │ ● [function] Lookup - Success
720
+ ◆──╯ [atom] Agent completed request
721
+ ```
722
+
723
+ Pending agent span with user message —
724
+ `execution-graph.test.ts#L296`:
725
+
726
+ ```
727
+ ● [atom] Agent processing request...
728
+ ├──● [user] hello
729
+ ```
730
+
731
+ Sequential agent requests on one pid (single shared lane) —
732
+ `trace-timeline.node.test.ts#L141`:
733
+
734
+ ```
735
+ ● [atom] Agent processing request...
736
+ ├──● [user] List all available schemas. Tell me what typenames are available.
737
+ │ ● [function] List schemas - Success
738
+ ◆──╯ [atom] Agent completed request
739
+ ● │ [atom] Agent processing request...
740
+ │ ● [user] Create an organization called "DXOS" and a person named "Alice".
741
+ │ ● [function] Create object - Success
742
+ │ ● [function] Create object - Success
743
+ ◆──╯ [atom] Agent completed request
744
+ ● │ [atom] Agent processing request...
745
+ │ ● [user] Search for all organizations and persons.
746
+ │ ● [function] Query - Success
747
+ │ ● [function] Query - Success
748
+ ◆──╯ [atom] Agent completed request
749
+ ```