agent-tracegraph 0.4.0__tar.gz → 0.6.0__tar.gz

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 (43) hide show
  1. agent_tracegraph-0.6.0/CHANGELOG.md +774 -0
  2. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/PKG-INFO +167 -17
  3. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/README.md +166 -16
  4. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/SECURITY.md +56 -10
  5. agent_tracegraph-0.6.0/contracts/check-report.schema.json +396 -0
  6. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/pyproject.toml +5 -1
  7. agent_tracegraph-0.6.0/src/tracegraph/_errors.py +35 -0
  8. agent_tracegraph-0.6.0/src/tracegraph/adapters/base.py +59 -0
  9. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/langgraph_checkpoint.py +328 -33
  10. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/otlp_spans.py +115 -33
  11. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/phoenix_export.py +7 -2
  12. agent_tracegraph-0.6.0/src/tracegraph/analysis/check.py +603 -0
  13. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/diagnose.py +61 -5
  14. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/memo.py +4 -1
  15. agent_tracegraph-0.6.0/src/tracegraph/analysis/motifs.py +272 -0
  16. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/patterns.py +179 -87
  17. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/artifact.py +83 -31
  18. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/cli.py +920 -128
  19. agent_tracegraph-0.6.0/src/tracegraph/graph_ref.py +136 -0
  20. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/input_validation.py +8 -1
  21. agent_tracegraph-0.6.0/src/tracegraph/model.py +452 -0
  22. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/sqlite_snapshot.py +7 -2
  23. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/ladybug.py +48 -8
  24. agent_tracegraph-0.4.0/CHANGELOG.md +0 -354
  25. agent_tracegraph-0.4.0/src/tracegraph/adapters/base.py +0 -22
  26. agent_tracegraph-0.4.0/src/tracegraph/model.py +0 -242
  27. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/.gitignore +0 -0
  28. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/LICENSE +0 -0
  29. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/analysis-report.schema.json +0 -0
  30. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/phoenix-runtime.json +0 -0
  31. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/review-candidates.schema.json +0 -0
  32. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/__init__.py +0 -0
  33. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/_atomic_write.py +0 -0
  34. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/__init__.py +0 -0
  35. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/__init__.py +0 -0
  36. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/ahu.py +0 -0
  37. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/explain.py +0 -0
  38. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/normalize.py +0 -0
  39. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/py.typed +0 -0
  40. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/review_candidates.py +0 -0
  41. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/__init__.py +0 -0
  42. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/base.py +0 -0
  43. {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/in_memory.py +0 -0
@@ -0,0 +1,774 @@
1
+ # Changelog
2
+
3
+ All notable changes to Tracegraph are documented here.
4
+
5
+ The **artifact** schema version and the **report**/**review-candidate** JSON schemas are
6
+ separate contracts from this version number; changes to any of them are called out
7
+ explicitly.
8
+
9
+ ## 0.6.0 - 2026-10-20
10
+
11
+ ### Added
12
+
13
+ - **`tracegraph summarize <preset> <artifacts...>`** (#60) groups a preset's matches across an
14
+ artifact batch by observed shape, so an operator can pick the recurring failure shape to look at
15
+ first. A group's signature (`motif-signature/v1`) is the matched steps' kind, status, source and
16
+ safe-v1 name, the `CAUSED_BY` origins between them, and the trace's source kind, causal
17
+ fidelity, link setting and whether it discloses heuristically chosen edges (per trace, not per
18
+ edge). It has no ids, error text or artifact file paths; an identifier-shaped name that looks
19
+ like a path is kept, as in `analyze` reports. Each group reports its match count and trace count
20
+ separately, plus up to three representatives (artifact digest and step ids). Only matches
21
+ confirmed by `check`'s rule are grouped; the rest are counted as unconfirmed. Fields that are
22
+ not known are kept as unknown. A coverage section always reports skipped artifacts, traces that
23
+ may hide a match, disclosed capture gaps, traces with no capture record and unconfirmed matches.
24
+ `--out` writes the `tracegraph.motif-summary` schema-v1 JSON with the same input protection as
25
+ `export-review-candidates`. Only strictly adjacent presets are accepted; the three gap presets
26
+ exit 2. Memory holds one trace and one entry per group, never the matches. A 160-wide retry fan
27
+ (25,600 matches) adds about 27 KiB over a 40-wide one, where collecting the matches adds about
28
+ 2 MiB. The issue suggested validating demand first; this is the minimal version.
29
+
30
+ ### Changed
31
+
32
+ - **`query --limit` no longer holds every match of the trace it is reading** (#79). Each
33
+ trace's matches were collected and sorted in full before the cap was applied, so one trace
34
+ with millions of matches took about 1 GiB at `--limit 1`. The matchers now produce matches
35
+ one at a time and `query` keeps only those still under the cap. The footer's total stays
36
+ exact, so every match is still counted. Measured on macOS/arm64 at `--limit 1` with
37
+ synthetic traces: a 2500-step chain of failing `x` agents (`repeated-agent-failure`, 3.1M
38
+ matches), and 1500 tools feeding one `retry:x` step that feeds 1500 failing tools
39
+ (`tool-retry-failure` and `tool-retry-failure-near`, 2.25M matches each):
40
+
41
+ | trace, preset | memory backend | `--backend ladybug` |
42
+ | --- | --- | --- |
43
+ | chain, `repeated-agent-failure` | 976 → 70 MiB, 5.5 → 2.8 s | 1226 → 308 MiB (Python fallback) |
44
+ | fan, `tool-retry-failure` | 1097 → 74 MiB, 2.8 → 1.6 s | 1753 → 757 MiB (Cypher) |
45
+ | fan, `tool-retry-failure-near` | 927 → 74 MiB, 3.2 → 2.1 s | 1470 → 613 MiB (Cypher) |
46
+
47
+ The Cypher rows stay high because LadybugDB buffers the full result set; an empty store
48
+ already takes about 300 MiB. The matches selected, their order, the footer total, exit codes
49
+ and error precedence are unchanged, and `find_matches` returns the same list for every caller.
50
+ Time still grows with the match
51
+ count. A mode that stops early and reports no total was considered and not built.
52
+ `LadybugStore` gains `first_matches(pattern, n)`; `tracegraph.analysis.patterns` gains
53
+ `iter_matches` and `first_matches`.
54
+
55
+ ### Fixed
56
+
57
+ - **Boundaries that promise `ValueError` no longer leak an error from describing the one they
58
+ caught** (#109). Naming a caught exception runs caller code: `str(exc)` calls its `__str__`,
59
+ and a metaclass can make the type's `__name__` a property. When that raised, its error
60
+ escaped instead: from LangGraph ingest when the saver's listing or a supplied `schedule_graph`
61
+ fails, and from `--graph` when the module fails to import. The validated copy that
62
+ `artifact.dumps()` and `analyze()` use since #82 and #93 (both in this release) named its
63
+ caught exception the same way and is hardened too. These now describe the exception through
64
+ one internal guard (`tracegraph._errors`) that falls back to fixed text, and uses only a name or
65
+ message that is a plain `str`.
66
+ Ordinary exceptions are described exactly as before. `artifact.from_obj()` on a schema-v1
67
+ payload also refuses, as `ValueError`, an edge sequence that raises `SystemExit` or another
68
+ `BaseException` (the current schema already did for `SystemExit`), or whose type check
69
+ raises; `KeyboardInterrupt` still propagates.
70
+
71
+ - **`--graph` loads a module file whose path is not valid UTF-8** (#110). On POSIX such a path
72
+ arrives surrogate-escaped, and naming the module from its strict UTF-8 encoding refused it:
73
+ `ingest --graph` exited 2 with a `UnicodeEncodeError` for a file it could import. Such a path
74
+ is now named from its file-system bytes, under its own prefix so it cannot take another
75
+ file's name. Every path that loaded before keeps its module name.
76
+
77
+ - **Cost totals and their baseline delta are exact** (#111). They were computed at `Decimal`'s
78
+ default precision of 28 significant digits and rounded without a note, while the total was
79
+ still reported as complete: a cost of `0.12345678901234567890123456789` was totalled as
80
+ `0.1234567890123456789012345679`, and `10000000000000000000000000000` plus `0.01` as
81
+ `10000000000000000000000000000`. They are now computed at a precision of 160, which is exact
82
+ for any sum of costs within the 64-character bound up to about 10^32 steps. Past that, the total
83
+ (or only the delta) is withheld with a note instead of rounded. Ordinary costs report the same
84
+ digits as before (`1.00 + 1.00` is still `2.00`).
85
+
86
+ - **A cost too long to write out is no longer summed or stored** (#100). A few characters of
87
+ exponent stand for a number of any size. A model-valid `total_cost` of `"1e1000000"` made
88
+ `analyze` fail with `decimal.Overflow`, an `ArithmeticError` rather than the promised
89
+ `ValueError`, with or without a baseline; `"9e999999"` did not overflow but wrote a
90
+ million-digit total into the report. OTLP ingest wrote such a value out in full, so a 10-byte
91
+ attribute `"1e99999999"` became a 100,000,000-character `total_cost` in the artifact. A cost is
92
+ now read only if, written out in full, it is at most 64 characters (`model.MAX_COST_CHARS`),
93
+ checked from its digits and exponent without writing it out (`model.cost_is_bounded(Decimal)`).
94
+ `analyze` withholds the total
95
+ with its own note, as for an unreadable cost; OTLP ingest treats a longer cost as unreadable,
96
+ so the total and component costs are disclosed as `otlp.unreadable_total_cost` and
97
+ `otlp.unreadable_component_cost` as before, and a readable alias is still used. Artifacts are
98
+ not refused at load. The bound is on each cost read, not on the total: summed exactly (#111),
99
+ two 64-character costs can make a 127-character total, and a million of them 133.
100
+
101
+ - **A lone surrogate in a producer string no longer ends a command in a traceback** (#101). JSON
102
+ can carry one (`"\ud800"`), and the artifact loads and validates, but strict UTF-8 refused it
103
+ in several places: `analyze` (and `check` and the new `summarize`, which share the helper)
104
+ hashing a name into its `redacted:` alias; `ingest-otlp` without `--out` hashing the trace id
105
+ into the default file
106
+ name; `export-incident-memo` hashing the run id; `inspect`, `explain` and `query` writing the
107
+ name to the terminal; and LangGraph ingest hashing a routing channel into a candidate task id.
108
+ The three hashes now encode with `surrogatepass`, so every other string hashes to the same
109
+ alias, file name and digest as before. The LangGraph candidate is skipped, because LangGraph
110
+ could not have produced a task id from it. The CLI escapes what its output streams cannot
111
+ encode instead of failing.
112
+
113
+ - **CLI output prints an emoji code in a producer string as written** (#102). The CLI's Rich
114
+ consoles kept emoji substitution on, and escaping markup does not turn it off, so a step named
115
+ `:ok:` printed as 🆗 in `query`, `inspect`, `explain` and `analyze` while the artifact and the
116
+ JSON reports held `:ok:`. Both consoles now have it off, so the new `summarize` prints the same
117
+ way. No text the CLI prints itself used an emoji code.
118
+
119
+ - **`artifact.from_obj()` on a schema-v1 payload raises only `ValueError`** (#103). When reading
120
+ a caller-supplied lazy edge sequence failed, the migration formatted the exception into its
121
+ message, so an exception whose `__str__` raises escaped as that error instead. The message no
122
+ longer formats the exception at all (its type's name can run caller code too, through a
123
+ metaclass); the original stays on the chain as `__cause__`. `loads()` and the CLI parse JSON
124
+ into lists, so neither could reach this.
125
+
126
+ - **`analyze()`, `check.evaluate()` and `build_incident_memo()` raise only `ValueError` on a
127
+ trace the model refuses** (#93). On a trace changed in place after it was built they raised a
128
+ raw `TypeError` or `AttributeError` (`seq = "zero"`, `steps = None`, `trace = None`, a step
129
+ `kind = "bogus"`, an integer `error_msg`), mostly from analysis code past their structural
130
+ check, and the memo rendered a step `status = "maybe"` without complaint. Each now analyzes the
131
+ copy the model validates, the same one `artifact.dumps()` and the writers use, so a value the
132
+ model coerces (`"0"` for a `seq`) is analyzed as it would be read and the caller's trace is
133
+ left unchanged. `analyze()` and the memo still accept structurally sound but non-canonical
134
+ traces; `check.evaluate()` still requires the canonical form. The validated copy does not change
135
+ the report for a valid trace; cost totals change separately in this release (#100, #111). That
136
+ copy is now made from a Python-mode dump: the JSON-mode dump
137
+ turned a NaN in `trace.run_id` or `trace.links_preserved` into `null`, which validated, so
138
+ `dumps()` and both writers erased the value instead of refusing it. It no longer depends on
139
+ the warnings filter either: under `-W error` a coercible value used to be refused through
140
+ pydantic's serializer warning. Cost on a 6000-step chain, best of 7, measured in PR #95 against
141
+ its parent `2eaa403` (which already had #82): `analyze()` 0.25–0.27 s
142
+ → 0.30–0.32 s, with a baseline 0.57–0.58 s → 0.69–0.70 s, `check.evaluate()` 0.65–0.67 s →
143
+ 0.78–0.79 s, `build_incident_memo()` 0.016 s → 0.044 s.
144
+
145
+ - **`artifact.dumps()` raises only `ValueError`** (#82). A trace changed in place after it was
146
+ built (pydantic does not revalidate on attribute assignment) could make it raise a raw
147
+ `TypeError` (`seq = "zero"`, `steps = None`, `edges = None`) or write a field the readers
148
+ reject (`status = "maybe"`, an integer `step_id`). `dumps()` now serializes a copy the model
149
+ has validated, as the writers already did, and raises `ValueError` for anything the model
150
+ refuses or cannot serialize: pydantic's `ValidationError` for a mistyped field, a
151
+ serialization error (also a `ValueError`) for a value it cannot dump at all, such as
152
+ `trace = None`. A value the model coerces (`"0"` for a `seq`) is written as it would be read.
153
+ A lazy sequence that fails while being serialized, through `dumps()` or either writer, is now
154
+ reported as a `ValueError` too, instead of whatever it raised. `dumps()` still checks fields,
155
+ not structure: a dangling edge or a stale trace status is written. Checking structure there
156
+ would refuse the structurally sound but non-canonical traces that `analyze()` digests, such as
157
+ one with a stale trace status (`analyze()` itself refuses a dangling edge). Unknown-field flags
158
+ without a capture record now reach a `dumps()`
159
+ caller as `ValidationError`, as they already did for the writers. A valid trace is
160
+ serialized byte for byte as before. Cost on a 6000-step chain, measured in PR #94 (its helper
161
+ was changed again by #93): `dumps()` 0.15 s → 0.20 s;
162
+ `analyze()` digests the current trace through it, a baseline comparison and `check` digest
163
+ both.
164
+
165
+ - **An error already in the parent graph is no longer recorded again around a subgraph** (#85).
166
+ A LangGraph checkpoint is an error where the error channel holds one and none of the
167
+ checkpoints it was built from did, but the check read only a checkpoint's causal parents, and
168
+ three shapes hid the earlier error from it. A subgraph's `__start__` was compared only with
169
+ the subgraph's input checkpoint, which holds its values as pending writes. A persistent
170
+ subgraph (`checkpointer=True`) entered again was compared with its previous invocation, not
171
+ with the outer checkpoint that launched it. And the parent-graph checkpoint after a subgraph
172
+ was compared only with the subgraph's exit, so a subgraph whose state has no error channel, or
173
+ that cleared its own copy, made the outer error look new. One failure was recorded two or more
174
+ times: three with nested subgraphs, or when the subgraph cleared its copy. The check now also
175
+ reads each checkpoint's declared parents, and for a `__start__`, its input checkpoint's
176
+ declared parent and its launch parent (`metadata.parents`). A former duplicate written by a
177
+ node that rewrote the still-set channel is disclosed as `langgraph.rewritten_error_channel`
178
+ instead, with its stderr line, as for any such write since #47; `check` reads that gap as
179
+ before.
180
+
181
+ - **A fork copy is no longer a second failure.** `update_state(..., as_node="__copy__")`
182
+ copies a checkpoint and runs no node, but it is linked to the copied checkpoint's parent, so
183
+ copying the checkpoint where an error first appeared (in the root graph, or a subgraph's
184
+ `__start__`) recorded that error again. A fork now never records an error.
185
+
186
+ For both: steps, edges, names and kinds are unchanged; what changes is which steps are errors
187
+ (with their `error_msg`) and the disclosure above. An error first written inside a subgraph, a
188
+ `Send` payload error below a clean state, and an error a caller's `update_state` writes are
189
+ recorded as before. Artifacts change only for histories that held such a duplicate. The
190
+ schema stays v7, and the committed fixtures are byte-identical. One shape still records the
191
+ error twice: a fork copy of a subgraph's *input* checkpoint, then resumed (#90).
192
+
193
+ ### Known limitations
194
+
195
+ - Resuming after a fork copy of a subgraph's input checkpoint
196
+ (`update_state(..., as_node="__copy__")` on it) records an error again at the subgraph's next
197
+ start: one the parent graph held (#90) or one a `Send` payload carried (#96). Four ways of
198
+ recognizing such a copy were tried. Three classified the fork by its content: an unread
199
+ `__start__` value, a `__start__` version absent from `versions_seen`, and pending writes from
200
+ LangGraph's `__start__` task. The fourth matched it to an input checkpoint with identical
201
+ `channel_values`, `channel_versions` and `versions_seen`, and compared its run with the
202
+ original's. Each hid a genuine failure on a plain `Pregel` graph, so none shipped. A
203
+ `Pregel` input's first step can run a user node, and a no-op `update_state(...,
204
+ as_node="__input__")` can write an input identical to a loop checkpoint.
205
+
206
+ ## 0.5.0 - 2026-10-18
207
+
208
+ ### Added
209
+
210
+ - **Dropped OTLP span links are disclosed** (#49). A link the OTLP importer could not record as a
211
+ cause was skipped silently, while the header still read `causal_fidelity=declared_dag` and
212
+ `links_preserved=true`. Each kind now prints one stderr line with its count and becomes a
213
+ capture gap: `otlp.unresolved_span_link` (names this trace but no span in the export,
214
+ possibly a partial export), `otlp.unscoped_span_link` (names no trace, missing or empty `traceId`; it may
215
+ refer to a span of this trace, so it is no longer treated as foreign) and
216
+ `otlp.foreign_trace_link` (names another trace). `check` treats the first two as gaps that may
217
+ have left a step out (one-sided for the count rules, insufficient for topology) and the third
218
+ as having no effect. The schema stays v7. Only artifacts ingested from exports with such links
219
+ change, and with them the digests of reports made from them; every committed fixture resolves
220
+ all its links. An artifact holding none of these codes reads and checks as before.
221
+ - **`--strict` for `query` and `export-review-candidates`** (#50). A `*.json` file in a directory
222
+ that is not a valid artifact is skipped with a warning, so a truncated artifact holding the
223
+ failure let a "pattern must not appear" gate pass with exit 1 ("no matches"), and let
224
+ `export-review-candidates` write a report without it. With `--strict`, any skipped directory
225
+ file fails the command with exit 2 (`--strict: N discovered file(s) could not be loaded as
226
+ tracegraph artifacts`) after the usual skip warning, and no report is written. An explicitly named bad
227
+ file still fails first, as before; `--strict` is checked before "no valid artifacts" and
228
+ duplicate trace ids. Without the flag nothing changes, and with it and no strays the output
229
+ is byte-identical.
230
+ - **`tracegraph check`, an explicit regression verdict** (#59): one current run, one baseline the
231
+ caller names, and rules the caller names (`failures-not-increased`,
232
+ `pattern-not-increased:<preset>`, `topology-unchanged[:shape]`; no default set). Exits 0 PASS,
233
+ 1 FAIL, 3 INSUFFICIENT_EVIDENCE, 2 invalid input (no report written). Rules compare counts
234
+ per step `(kind, name)`; unknown evidence the selected rule depends on — an `unset` status, a
235
+ substituted value or missing name on a step it could count, a capture gap that may have
236
+ left a step out of the artifact — blocks PASS on the current side and
237
+ FAIL on the baseline side instead of counting as zero (a step-omitting gap on either side makes
238
+ the topology rule insufficient, since it compares trees, not counts). Pairs from different
239
+ sources, or with either artifact lacking capture provenance (written before artifact v7),
240
+ are insufficient for every rule, and pairs read with different causal settings for the
241
+ rules that read causal structure. New contract `tracegraph.check` v1
242
+ (`contracts/check-report.schema.json`), bound to both canonical artifact digests and each
243
+ rule's id, version and parameters. `diff`, `query` and `analyze` keep their exit codes.
244
+ - **Capture provenance, artifact schema version 7** (groundwork for #59): every ingest now
245
+ records in the artifact what the adapter could not observe, so a later comparison can tell
246
+ "recorded, nothing disclosed" from "not recorded". `trace.capture.gaps` keeps one fixed code and count
247
+ per ingest disclosure (the stderr text is unchanged and stays out of the artifact; the code
248
+ carries no checkpoint id, task id or producer value), and `capture` is present with an empty
249
+ list when the read raised no disclosure; values substituted on one step are recorded
250
+ separately. `steps[].unknown_fields` names values an adapter substituted:
251
+ `kind` and `name` (a span without a recognized OpenInference kind or a usable name, including
252
+ Phoenix names dropped as non-identifiers; an unattributed LangGraph task; and every LangGraph
253
+ checkpoint step except the input checkpoint and the loop step that applies it, named or not,
254
+ because a checkpoint does not record its writer and its name is inferred from the parent's
255
+ routing) and `error_msg` (OTLP's `"error"` fallback, withheld messages, LangGraph's
256
+ unserialized-exception label). Task, interrupt and scheduled steps named from LangGraph's own
257
+ task ids are not flagged. Two OTLP parent resolutions that were silent are now recorded too: a `graph.node.parent_id` resolved by start time among
258
+ several spans, and one not honored in favour of `parentSpanId`; each also prints a new stderr
259
+ warning. Every newly ingested artifact declares 7 and 0.4.x refuses it by name; an artifact
260
+ written by an earlier release re-serializes exactly as the previous release wrote it (v2–v6
261
+ byte for byte, v1 migrated to v2 as before) and never gains provenance. Newly ingested
262
+ artifacts therefore have new digests, as do analysis reports and incident memos built from
263
+ them; report and memo shapes, other CLI output and exit codes are unchanged.
264
+ - **`analyze` reads OTLP span exports directly** (#57): a JSON or Collector JSONL export
265
+ (`resourceSpans`) is diagnosed in memory with the same adapter settings as `ingest-otlp`, so
266
+ the report is byte-identical to analysing the ingested artifact; `--trace` selects one trace of
267
+ a multi-trace export and `--baseline` accepts an OTLP export too. Inputs are routed by shape
268
+ before any adapter runs (`schema_version` → artifact, `resourceSpans` → OTLP, `spans` or a
269
+ root `traces`/`data` list → Phoenix); record validity stays the adapter's call, so the direct
270
+ path accepts and rejects what the explicit ingest commands do. A payload carrying both OTLP
271
+ and Phoenix records exits 2: a single object names the ingest command that reads each format,
272
+ and a list, JSONL or Phoenix envelope says to extract one format's records first. An unknown
273
+ shape exits 2 listing the supported ones instead of failing as a Phoenix export.
274
+ `ingest-otlp --file -` reads stdin, and stdin now goes through the same
275
+ `TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling as files for `analyze`, `ingest-otlp` and
276
+ `ingest-phoenix` (the stdin half of #53; the `px` output half is the "Phoenix `px` output is bounded" entry below);
277
+ `ingest-phoenix` reads its input with the Phoenix adapter only (its accepted inputs are
278
+ unchanged). Next investigation steps after a direct OTLP
279
+ diagnosis print an `ingest-otlp` save template. No file is written without `--json-out`;
280
+ analysis JSON schemas, exit codes and privacy boundaries are unchanged.
281
+ - **Guided investigation after `analyze` and `phoenix diagnose`** (#58): displayed failure
282
+ candidates include full step IDs, with POSIX-quoted `explain` commands for existing artifacts
283
+ or explicitly labelled save/replay templates for exports, stdin, and unsaved Phoenix traces.
284
+ Evidence IDs, paths and commands preserve literal emoji-like tokens and are not forcibly
285
+ line-wrapped; consumed pipe inputs and descriptor aliases (`/dev/stdin`, `/dev/fd/N`,
286
+ `/proc/<pid>/fd/N`, or a symlink to one, including regular-file stdin redirects; a symlink
287
+ to a directory descriptor is not detected) require replay. Nonprinting arguments are shown as
288
+ escaped data instead of copyable commands. Hidden
289
+ candidates show the required `--limit`; report paths are acknowledged only after successful
290
+ writes, without duplicate report-path notices. Existing edge-origin/fidelity warnings and
291
+ missing-telemetry disclosures continue to distinguish evidence from causal claims; this
292
+ change adds navigation, not new causal inference. Analysis JSON schemas, exit codes, and
293
+ artifact privacy boundaries are unchanged.
294
+ - **`ingest --graph` reports LangGraph tasks that were scheduled but recorded nothing.**
295
+ Given the compiled graph (`path/to/file.py:attr` or `pkg.module:attr`), LangGraph's
296
+ `get_state` lists the tasks it schedules at the thread's newest root checkpoint, and each one
297
+ with no write there becomes an `unset` step with the new source value `scheduled`, linked by
298
+ `CAUSED_BY` to that checkpoint: the recursion limit, a killed process, a static breakpoint,
299
+ one step per `Send` packet. Finished, failed and paused tasks are still read from the
300
+ checkpoint's writes, so these steps never duplicate `task` or `interrupt` steps. The result
301
+ is LangGraph's answer for the supplied graph; the trace header records its structural
302
+ fingerprint as `schedule_graph`, also when nothing is scheduled. An unapplied
303
+ `Command(goto=...)` input and `Send` packets to a node the graph lacks are disclosed at
304
+ ingest. `--graph` imports the module and runs the graph's state callbacks while reading,
305
+ but no node. `inspect` marks such a step `◌ scheduled` and adds `· N scheduled` to the
306
+ summary.
307
+ - **Artifact schema version 6**, stamped by content: an artifact read with `--graph` (it has
308
+ `schedule_graph`, or a `scheduled` step) declares 6, and 0.4.x refuses it by name. The header
309
+ field is omitted, not written as `null`. (A fresh ingest by this release declares 7 for its
310
+ capture record, which outranks 6.)
311
+ - **A hands-on getting-started notebook in Korean**, `examples/getting_started_ko.ipynb`, for
312
+ first-time users working from a repository checkout. It runs the example agent and the
313
+ offline commands other than `check` and `export-review-candidates`, interprets each output, and works through two applied scenarios:
314
+ checking a change against a baseline, and aggregating failures across many runs from
315
+ `analyze --json-out` reports. Its outputs are committed, and `tests/test_notebooks.py`
316
+ executes it so its commands and exit codes cannot drift from the CLI. `nbclient` and
317
+ `ipykernel` join the development dependencies. Nothing ships in the wheel or sdist, and no
318
+ artifact or report contract changes.
319
+
320
+ ### Changed
321
+
322
+ - **`artifact.save()` and `artifact.save_atomic()` refuse a trace the readers would reject**
323
+ (#67). Pydantic does not revalidate on attribute assignment, so a `NormalizedTrace` changed in
324
+ place could be written in a form tracegraph's own readers refuse: `save()` wrote a mistyped
325
+ `status`, and both wrote a `CAUSED_BY` edge to a missing step (which `artifact.load` accepted
326
+ and only `validate` / `analyze` rejected), a trace status that disagrees with its steps, a
327
+ wrong `projection_lossy` flag, or a missing `TREE_PARENT`. Both writers now round-trip the
328
+ model and run `validate_normalized`, the check the stores and `validate` apply on load
329
+ (legacy projections included), before touching the destination, and raise `ValueError`
330
+ (pydantic's `ValidationError` is one), leaving an existing file untouched. A mistyped `seq` used to escape
331
+ every writer as a raw `TypeError`; it is now a `ValidationError` too. A valid trace is written
332
+ byte for byte as before, from the validated copy, so a value the model coerces (`"0"` for a
333
+ `seq`) is written as it would be read. Cost on a 6000-step chain: `save()` 0.16 s → 0.29 s,
334
+ `save_atomic()` 0.19 s → 0.30 s; `ingest` already validated before saving, so it now checks
335
+ twice. Because the model now checks first, a capture-provenance
336
+ violation reaches a writer's caller as pydantic's `ValidationError` rather than the
337
+ `check_provenance` message (`artifact.dumps()` still raises the latter). `artifact.dumps()`
338
+ stays a serializer without the model and structure checks (its `TypeError` on a mistyped
339
+ `seq` is #82).
340
+ - `inspect` marks a step paused by `interrupt()` as `⏸ interrupted`, in yellow rather than
341
+ the green used for finished steps, and the summary line gains `· N interrupted` when there
342
+ is one. The output for every trace with neither interrupted nor scheduled steps is unchanged.
343
+
344
+ ### Fixed
345
+
346
+ - **A LangGraph node writing the error channel while it is still set is disclosed** (#47). A
347
+ checkpoint is an error only where the error channel is set and every causal parent's is
348
+ empty, so a node failing again before the key was cleared stayed `ok`, silently, and the
349
+ retry presets could not match. The rule is unchanged: a checkpoint does not record which
350
+ node raised, and both a value-change rule and the issue's version rule were measured to
351
+ invent failures (a node returning the whole state, a reducer that grows the value, a
352
+ handler rewording the error). Instead, a node-written checkpoint whose error channel was
353
+ written again while set (its `channel_versions` entry moved, or without versions its value
354
+ changed) counts toward a stderr disclosure and the capture gap
355
+ `langgraph.rewritten_error_channel`. `check` reads it for the count rules (one-sided on that
356
+ side) and for success-dependent patterns such as `gate-failure-after-success` (insufficient
357
+ on either side), not for topology. Input, `__start__`, update and fork-copy checkpoints, and
358
+ parents in another namespace (a subgraph's entry or exit), are not compared, so propagation
359
+ and a caller's input disclose nothing; a propagated error recorded again at a subgraph's
360
+ `__start__` is pre-existing and filed as #85. Measured on LangGraph 1.2.2 and 1.2.15 with
361
+ InMemorySaver and SqliteSaver. Artifacts change only for histories with such writes; the
362
+ committed fixture has none. Schema stays v7.
363
+ - **`analyze --trace` and `--baseline-trace` no longer ignore the selector on an artifact**
364
+ (#64). An artifact holds one trace, and a selector naming a different one was accepted: the
365
+ command exited 0 with a report on the artifact's trace, not the one requested. It now exits 2
366
+ with `Invalid value for --trace: <input> holds trace '<id>', not '<selector>'` (or
367
+ `--baseline-trace`) and writes no report, as `check` already did and as a missing trace in an
368
+ OTLP or Phoenix export does. A selector naming the artifact's own trace is still accepted, and
369
+ the report is byte-identical to the one without it.
370
+ - **`ingest-phoenix --trace` no longer ignores the selector on a Phoenix-sourced artifact** (#69).
371
+ Re-ingesting such an artifact with a selector naming another trace exited 0 and re-saved the
372
+ artifact's own trace. It now exits 2 with the same message as `analyze` and writes nothing, to
373
+ `--out` or to the default path. A matching selector re-saves the artifact byte for byte, as
374
+ before. An artifact from any other source keeps its existing error, `--file must contain a
375
+ Phoenix CLI trace export`.
376
+ - **`phoenix doctor` and `phoenix diagnose` no longer crash when `px` cannot be run** (#56). A
377
+ `px` on PATH without the exec bit, a directory named `px`, or a file the OS cannot execute
378
+ raised `PermissionError` or `OSError` with a traceback. They now report that `px` failed with
379
+ an OS error, naming the operating system's text for the error number and never the exception
380
+ message: `doctor` prints `FAIL` and exits 1, `diagnose` exits 2, as for their other `px`
381
+ failures. When the OS refused to launch `px`, the message adds a hint to check that `px` on
382
+ PATH is an executable file.
383
+ - **Phoenix `px` output is bounded** (#53, the half #63 left open). `phoenix doctor` and
384
+ `phoenix diagnose` held all of `px`'s stdout and stderr in memory with only a time limit. Stdout
385
+ now goes to an unlinked temporary file held to the same `TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling
386
+ as file and stdin input (default 256 MiB, `0` disables it). `px` is killed and reaped once the
387
+ file passes the limit, and the command reports `... output exceeded the N-byte limit` without
388
+ echoing the output: doctor `FAIL` exit 1, diagnose exit 2. Stderr is discarded; it was never
389
+ shown. Two smaller changes come with it. Output that is not UTF-8 is now reported as such:
390
+ before, `doctor` printed the codec error and `diagnose` crashed with a traceback. And a
391
+ descendant of `px` that keeps stdout open no longer holds the call until the timeout, and
392
+ the capture stops at the size sampled once `px` has exited, read by position on POSIX so a
393
+ descendant's later writes cannot overwrite it. Such descendants are not
394
+ stopped: `px` is trusted like any installed executable. Timeout, nonzero-exit and
395
+ launch-error messages are unchanged.
396
+ - **`SECURITY.md` now states the `ingest --graph` trust boundary** (#55). It said tracegraph
397
+ "does not execute graphs" and called a checkpoint database the one input that can run code,
398
+ but `--graph` imports the module you name, running its top-level code, and then runs the
399
+ graph's state callbacks while reading. A new section says what runs (no node does), that
400
+ checkpoint payloads stay under the empty serializer allowlist while the named code itself is
401
+ unsandboxed, and that the graph is never inferred. A table at the top separates JSON parsing,
402
+ hardened checkpoint deserialization and explicitly supplied graph code. The same file no
403
+ longer says piped stdin is read without a size cap: since direct OTLP analysis (#63), `-` goes
404
+ through the same `TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling as a named file. Documentation only.
405
+ - **`query` no longer keeps every trace and every match in memory** (part of #54). It loaded
406
+ and validated all artifacts, matched them all, and only then sliced the result to `--limit`,
407
+ so `--limit 1` over 200 artifacts of 2000 steps (248 MB on disk) peaked at 1374 MiB. It now
408
+ loads, validates and matches one artifact after another, keeps only the matches it prints
409
+ (and, with `--explain`, the traces they belong to), and builds labels only for those. With
410
+ `--limit`, peak memory grows with the number of artifacts only by a small per-file record:
411
+ 70 MiB for the same query. Without `--limit` it holds the matches it prints: 105 MiB. Output, exit codes and error precedence are
412
+ unchanged: every input is still validated, a later file's load error or a duplicate
413
+ trace_id still wins over a matching failure, and the footer's total stays exact. Matches past
414
+ `--limit` are counted but no longer labelled, so a labelling fault there (an internal bug:
415
+ labels look up step ids the matcher just returned) would no longer surface. One trace
416
+ with very many matches still enumerates them all (3.1M matches: 971 MiB, down from 986 MiB
417
+ but 5.4 s instead of 17 s); bounding that needs a decision about the `--limit` total and is
418
+ tracked as #79. An interrupt or `MemoryError` now surfaces at the artifact being read,
419
+ after the skip warnings collected so far, rather than after every file has loaded.
420
+ - **`repeated-agent-failure` no longer takes quadratic time when no names can match** (#52).
421
+ The unbounded two-step matcher it shares with `tool-repeat-failure-heuristic`, and which
422
+ `analyze` runs on every trace, walked and sorted every failing endpoint's whole causal
423
+ history before checking the name back-reference: a 2000-step chain of failing agents with distinct names
424
+ took 1.3s for zero matches, 4x per doubling. It now skips an endpoint no earlier candidate
425
+ can pair with and walks back only as far as the earliest one (2000 steps: 0.001s; a chain
426
+ whose only repeats are disjoint adjacent pairs is linear too). The longest-path pass also stopped rescanning a
427
+ high-fan-out cause once per endpoint. Matches, their order and every report are unchanged.
428
+
429
+ ## 0.4.0 - 2026-10-14
430
+
431
+ ### Added
432
+
433
+ - **LangGraph tasks paused by `interrupt()` are reported.** A task whose only write on the
434
+ thread's newest root checkpoint is `__interrupt__` — it paused and the history holds no
435
+ resume and no result — becomes an `unset` step with the new source value `interrupt`,
436
+ linked by `CAUSED_BY` to that checkpoint and named by the same task-id reconstruction as
437
+ failed tasks. The step describes the recorded history, not whether LangGraph would resume
438
+ the task now. An interrupt recorded beside a resume or an error and no result is ambiguous
439
+ in the checkpoint (under LangGraph 1.2.2 a resumed task that wrote nothing looks exactly
440
+ like one paused twice; 1.2.15 marks it finished), so it is disclosed at ingest instead. Trace `status` is unchanged: a pause is not an error.
441
+ - **Artifact schema version 5**, stamped by content: only an artifact carrying an
442
+ `interrupt` step declares it, so every other artifact serializes byte for byte as before.
443
+ 0.3.1 and earlier refuse a version 5 artifact by name; this build reads versions 1 to 5,
444
+ and still rejects an envelope that declares less than its content needs.
445
+
446
+ ### Known limitations
447
+
448
+ - Tasks scheduled but never started — the recursion limit, a killed process, an
449
+ `interrupt_before`/`interrupt_after` breakpoint — leave no write and are still not
450
+ reported. Telling them apart from finished work needs the compiled graph.
451
+ - An interrupt inside a subgraph is reported as the subgraph's node at the root, not as the
452
+ node inside the subgraph that paused.
453
+
454
+ ## 0.3.1 - 2026-10-13
455
+
456
+ ### Fixed
457
+
458
+ - **An unreadable cost no longer understates the run's total.** The importers dropped a
459
+ `llm.cost.total` that was not a finite non-negative decimal (`"abc"`, `-1`, a boolean), so
460
+ `analyze` summed the remaining costs and printed a total that was knowably too low, with no
461
+ warning. The documented rule that a single unparseable cost withholds the whole total only
462
+ applied to artifacts built by hand. An unreadable total cost is now stored as the fixed
463
+ marker `"unreadable"`, never the producer's value, and the report withholds `total_cost`
464
+ and its delta with the existing note.
465
+ - Token counts, prompt and completion costs, cost currencies and Phoenix evaluation scores
466
+ that cannot be read were dropped without a trace. They are still reported as unavailable,
467
+ and `ingest-otlp`, `ingest-phoenix`, `analyze` and `phoenix diagnose` now say so on stderr,
468
+ naming the kind of value and how many spans (for scores, evaluations) carried it, never
469
+ the value itself. A boolean score still reads as `1.0` or `0.0`, as before.
470
+ - An OTLP `intValue` sent as a fractional JSON number was truncated before validation:
471
+ `1.5` became `1` and `-0.5` became `0`, so an invalid token count looked valid. It is now
472
+ validated as sent.
473
+ - **A checkpoint history LangGraph could not have written is a usage error, not a
474
+ traceback or a different trace.** About half of 64 one-field mutations of a real history
475
+ (a non-object `metadata` or `channel_values`, a numeric `checkpoint_ns`, a boolean `v`, an
476
+ unhashable parent checkpoint id, ...) crashed `ingest` with a traceback, and several were
477
+ read silently as something else: a malformed parent config or `metadata.parents` dropped
478
+ the declared parent and made the checkpoint a root, and `metadata.step` of `true` or `1.5`
479
+ was read as step 1. Each checkpoint is now checked against the types LangGraph declares
480
+ for the fields tracegraph reads, strictly, and rejected with a message naming the field.
481
+ A config `checkpoint_id` that disagrees with the checkpoint's own id is rejected too.
482
+ - A checkpoint row the saver cannot decode (an unknown serialization type, metadata nested
483
+ past the recursion limit, a non-string metadata column) crashed `ingest` and `discover`
484
+ with a traceback, except that `discover` reported an unknown serialization type as "does
485
+ not support listing all threads". Both now report the history as undecodable.
486
+ - Pending writes that are not a `(task id, channel, value)` triple were skipped without a
487
+ word, and any 3-character string was unpacked as one (`"abc"` became task `a`). They are
488
+ still ignored, but `ingest` now says how many on stderr, since one could have carried a
489
+ task failure.
490
+
491
+ ### Compatibility notes for imports
492
+
493
+ - An artifact imported from a span with an unreadable total cost now carries
494
+ `"total_cost": "unreadable"` instead of no cost, and its reports withhold the run's
495
+ total cost. No schema change: 0.3.0 reads such an artifact and withholds the same way.
496
+ - Because `intValue` is no longer truncated, `llm.cost.total = {"intValue": 1.5}` is stored as
497
+ `"1.5"` instead of `"1"`, and a `toolgraph.graph_generation` of `{"intValue": 1.5}` is
498
+ rejected like any other invalid generation instead of being read as `1`.
499
+ - The committed Phoenix fixture carries an unreadable `llm.cost.prompt`; reading it now prints
500
+ one stderr disclosure. Every committed fixture's artifact, report, memo, candidate file and
501
+ CLI stdout is unchanged.
502
+ - LangGraph histories that `ingest` used to accept with a malformed field listed above are
503
+ now refused. Histories written by LangGraph's own savers are unaffected: the checks cover
504
+ only fields tracegraph reads (`counters_since_delta_snapshot`, which savers return as
505
+ lists although it is declared as tuples, is not checked), a missing `updated_channels`
506
+ and a legacy `pending_sends` are accepted, and an unknown `metadata.source` string still
507
+ reads as a loop. The committed LangGraph fixture is byte-identical.
508
+
509
+ ## 0.3.0 - 2026-10-13
510
+
511
+ ### Fixed
512
+
513
+ - An artifact whose content was not canonical (for example a stored trace status that
514
+ disagreed with its steps) was stamped `schema_version` 4 even when it carried no edge
515
+ origins. Version 4 is now stamped only when the file's TREE_PARENT edges are the
516
+ origin-priority projection of its causal edges and that differs from the temporal one; the
517
+ rest of the trace does not affect the stamp. **Digest impact:** canonical artifacts are
518
+ unchanged, but re-serializing a non-canonical trace (one `validate` rejects) whose tree is
519
+ not the origin-priority one, such as a trace with its derived edges missing, now writes
520
+ version 2 or 3 instead of 4. Any digest computed from that re-serialization changes,
521
+ including the `artifact_digest` that `analyze` reports for such a trace.
522
+ - Loading now rejects an envelope that understates its content: a `schema_version` 2 file
523
+ holding an origin-priority projection or derived task steps. Every reader that trusts the
524
+ envelope would have read such a file under the wrong rules, and a re-save rewrote it as a
525
+ different file. An envelope may still overstate what its content needs.
526
+ - `schema_version` must be an integer. `true`, `1.0`, `2.0` and `4.0` were accepted because
527
+ they compare equal, and `true` was migrated as a version 1 file.
528
+ - Evidence floats (`duration_ms`, evaluation `score`) must be finite, and the writer refuses
529
+ to emit `NaN`/`Infinity`, which are not JSON.
530
+ - `--backend ladybug` crashed with a raw traceback on a step `seq` outside the INT64 range
531
+ the backend stores. It now exits with a usage error naming the limit.
532
+ - `InMemoryStore.upsert_edges` accepted a `CAUSED_BY`/`TREE_PARENT` edge whose endpoint was
533
+ not a stored step, or a `BELONGS_TO` edge pointing at another trace; the failure surfaced
534
+ later as a `KeyError` from `ancestors`. It now raises `ValueError` and inserts nothing from
535
+ that batch, matching the LadybugDB store. `WROTE`/`READ` channel edges are unchecked as
536
+ before, and node upsert still replaces by ID.
537
+ - OTLP and Phoenix imports crashed with a raw traceback on a timestamp of `Infinity`, `NaN`
538
+ or a number beyond the representable date range, on a `spanId`/`parentSpanId` given as a
539
+ list, and on Phoenix `annotations` that were not an array. A timestamp that cannot be read
540
+ — including garbage text, which was silently dropped before — is now reported as
541
+ unavailable and disclosed on stderr by `ingest-otlp`, `ingest-phoenix`, `analyze` and
542
+ `phoenix diagnose`. Malformed ids and annotations are rejected with a usage error.
543
+ - An OTLP `status.code` of `true` was read as OK because `true == 1`, and `false` as UNSET
544
+ because `false == 0`; both are now rejected, for OTLP and Phoenix imports alike.
545
+ - LangGraph: two derived task steps could receive the same id (checkpoint `A` with task
546
+ `x#task:y`, checkpoint `A#task:x` with task `y`); the second silently replaced the first
547
+ and gained a second cause it never had. The collision is now rejected like a collision
548
+ with a checkpoint.
549
+ - LangGraph: a failed task that no candidate node reproduces is still reported unnamed, and
550
+ the read now says so on stderr. An unnamed failure is also what a change in LangGraph's
551
+ task-id algorithm would look like, so it is no longer silent.
552
+
553
+ - `diff` and `validate` printed producer text through Rich markup: a step named
554
+ `tool[bold]` showed as `tool`, and an invalid edge naming `ghost[red]X[/red]` showed as
555
+ `ghostX`. Both now print the text literally, as `inspect`, `explain` and `query` already
556
+ did. Output paths are printed literally too: an `--out` path containing `[/]` used to
557
+ raise a markup error after the file had been written, so the command exited 1 for work
558
+ it had completed.
559
+ - `diff --structure` paired any two leftover subtrees because every label is empty under
560
+ that option, then reported them under an empty path (`only in B under > : subtree ·`).
561
+ An empty label is no longer treated as evidence of correspondence, and the reported
562
+ subtrees now show step names so they can be found, while the comparison still ignores
563
+ them. The identical/not-identical verdict and exit code are unchanged.
564
+ - `explain` and `query --explain` labeled a nameless step by its `source`, which for
565
+ OTLP and Phoenix spans is always `loop` and for LangGraph is checkpoint metadata
566
+ (`input`, `loop`), not what the step was. They now use the step kind, like every other
567
+ view.
568
+ - `export-incident-memo` wrote `links_preserved` as `true`/`false`/`null` in its front
569
+ matter but as `True`/`False`/`None` in its summary. Both now use the front-matter form.
570
+ - The Phoenix CLI version-floor message is derived from the floor constant instead of
571
+ repeating it.
572
+
573
+ - `analyze --baseline` published token deltas, and cost deltas, from totals that covered
574
+ only some steps on one side: with tokens on 3 of 3 steps in the baseline and 1 of 3 in
575
+ the current run it reported `-200`, although the silent steps could have used any amount.
576
+ Like the duration delta, these deltas are now withheld with a note unless every step on
577
+ both sides reports the metric. In traces where only model calls carry token counts this
578
+ withholds the token delta. A cost total or delta withheld because steps or runs report
579
+ different or unknown currencies is now disclosed too; before, `analyze` printed
580
+ `cost=unavailable` with no reason.
581
+
582
+ - Writing an artifact or an analysis report left a hidden temporary file behind when the
583
+ write or fsync failed, because the temporary name was recorded only after writing. All
584
+ four output writers now share one helper that removes it on every failure.
585
+
586
+ ### Compatibility notes for imports
587
+
588
+ These change what an input that imported successfully before now produces or whether it
589
+ imports at all. No committed fixture is affected.
590
+
591
+ - An OTLP token count given as a boolean (`true`, or `{"intValue": true}`) was stored as `1`;
592
+ it is now unavailable, matching how an unusable cost is already treated. A non-finite
593
+ token count is unavailable too instead of crashing the import. An `intValue` holding a
594
+ boolean is no longer converted to `0`/`1` for any attribute.
595
+ - Raw OTLP input could inject evaluation summaries through the private
596
+ `_tracegraph_evaluations` span key, bypassing the length limit the Phoenix importer
597
+ applies. Only the Phoenix importer reads that key now.
598
+ - An empty-string `traceId` or `spanId` is rejected. A `parentSpanId` of `0`, `false` or
599
+ `[]` used to be read as "no parent", which made a span with a malformed declared parent a
600
+ root; it is now rejected. A missing, `null` or empty-string `parentSpanId` still marks a
601
+ root span.
602
+ - A Phoenix evaluation score too large to represent crashed the import; it is now
603
+ unavailable, like a non-finite score.
604
+ - When several spans carry the `graph.node.parent_id` a span declares (a loop), the importer
605
+ picks the nearest execution that started earlier. A span with a missing or unreadable
606
+ start time sorted as time 0 and could win that choice, so a timestamp nobody recorded
607
+ decided a causal edge. Such spans no longer take part: the choice is treated as
608
+ ambiguous, and the span falls back to its declared `parentSpanId`, or the import is
609
+ rejected if it has none.
610
+ - The CLA workflow refused every pull request from the maintainer's own commit-author
611
+ account, which is not the account that owns the repository. Both are allowlisted now.
612
+ - The release documentation asked for a personal access token with *read* access to pull
613
+ requests. Posting the CLA comment failed with that configuration and succeeded after
614
+ raising it to write; the cause remains unconfirmed, since GitHub documents either Issues
615
+ write or Pull requests write as sufficient. The documentation now asks for write on
616
+ Contents, Issues and Pull requests alike and records the observation rather than
617
+ inferring a rule.
618
+ - It also did not say that a Trusted Publisher's "Environment name" is the GitHub
619
+ environment rather than the index, which failed the first 0.2.0 rehearsal.
620
+
621
+ ### Dependencies
622
+
623
+ - The optional `[cypher]` extra now accepts LadybugDB `>=0.18,<0.22` (was `<0.19`), and the
624
+ lockfile moves to 0.21.2. The full test suite and every fixture's output are unchanged
625
+ on 0.21.2.
626
+ - CI now tests against Phoenix CLI 1.18.8 (was 1.8.1). The runtime floor stays 1.0.4: this
627
+ pin is what CI proves, not what tracegraph requires.
628
+
629
+ ### Added
630
+
631
+ - `tracegraph export-incident-memo <artifact> -o memo.md` writes a Markdown post-mortem
632
+ intended for long-term memory stores. Its output is a strict allowlist: the run id appears
633
+ only as a truncated SHA-256 `run_digest`, steps appear as `step_<n>_<kind>` aliases, and
634
+ no step name, error message, evaluation label or payload is written. Edges that only show
635
+ containment or have no recorded origin are rendered as "preceded by", never "caused by".
636
+ The memo carries the SHA-256 of the exact artifact bytes it was built from.
637
+
638
+ ### Changed
639
+
640
+ - **Artifact schema version 4.** When a step has several causes, its single TREE_PARENT is
641
+ now chosen by edge origin first — a declared graph or checkpoint parent over span
642
+ containment, over a span link, over an unknown origin — and only then by time. An artifact
643
+ whose tree differs from the purely temporal choice is stamped version 4; every other
644
+ artifact serializes as version 2 or 3, byte for byte as before. **Readers from 0.2.0 and
645
+ older refuse version 4 files** with an explicit unsupported-version error. This build reads
646
+ both projections, and the LadybugDB store re-emits a legacy file unchanged. The derived
647
+ tree feeds `inspect`, `diff`, and `analyze --baseline`, where it decides both the topology
648
+ changes and how steps are matched between the two runs: a step whose tree parent moved
649
+ can now appear as one removed and one added behavior change, and its logical step key
650
+ changes. Raw causal queries (`explain`, `query`) are unaffected.
651
+ - Phoenix timestamps without a timezone are now read as UTC rather than in the local
652
+ timezone of the machine running the import. **Digest impact:** artifacts built from such
653
+ exports now hash the same on every machine, and differ from any artifact previously built
654
+ where the local timezone applied a nonzero offset to those timestamps. Timestamps that
655
+ carry a `Z` or an offset are unchanged.
656
+ - **Phoenix artifacts no longer carry free-text annotation names or labels.** The Phoenix
657
+ import is documented as body-free and already kept only identifier-shaped span names, but
658
+ annotation names and labels were kept as free text (whitespace-collapsed, cut to 200
659
+ characters), so an evaluator's note such as `user said: my password is hunter2` reached
660
+ the artifact. They now pass the same identifier filter as span names: an evaluation whose
661
+ name fails it is dropped, and a label that fails it becomes `null` while the score is
662
+ kept. **Artifact content change:** a re-import of a Phoenix export with such annotations
663
+ produces different bytes and digests. Identifier-shaped text, which can still be personal
664
+ data such as an e-mail address, is kept.
665
+
666
+ ## 0.2.0 - 2026-09-12
667
+
668
+ First published release. `0.1.0` was tagged but never uploaded.
669
+
670
+ ### Fixed
671
+
672
+ - **A LangGraph node that raised an exception was reported as a successful run**
673
+ (`status=ok`, `error_count=0`). LangGraph does not route such a failure through a state
674
+ channel: it records it in the checkpoint's pending writes and writes no further checkpoint,
675
+ so reading channel values alone could not see it and no `--error-channel` value recovered
676
+ it. Each failed task now becomes its own step, hanging off the checkpoint that scheduled it
677
+ rather than marking that checkpoint — which is named after the node that *produced* it, so
678
+ marking it reported the wrong node.
679
+ - A task cancelled because a *sibling* failed is no longer counted as a second failure.
680
+ LangGraph records sibling cancellation through the same `__error__` channel as a genuine
681
+ fault; those tasks are reported as `unset` with no message, so one fault stays one failure
682
+ and a healthy node is never a primary candidate.
683
+
684
+ ### Security
685
+
686
+ - **Reading a checkpoint database no longer executes what it contains.** LangGraph's
687
+ deserializer revives stored objects by importing a module and calling a name, both taken
688
+ from the payload, and by default an unrecognized target is logged and then called — so a
689
+ crafted checkpoint naming `os.system` or `subprocess.run` ran on the machine doing the
690
+ analysis (both verified against the pinned dependency). `tracegraph ingest` now passes a
691
+ serializer with an empty allowlist.
692
+
693
+ **Behaviour change:** objects outside the framework's registered types no longer revive
694
+ through `ingest`; they come back as their raw arguments. If you were relying on a custom
695
+ class being reconstructed from checkpoint state, it will now surface as plain data.
696
+ Constructing `LangGraphCheckpointAdapter` yourself is unaffected — the serializer is
697
+ whichever one your saver carries.
698
+
699
+ ### Added
700
+
701
+ - The failing node's name, recovered by recomputing LangGraph's task id from checkpoint data.
702
+ The match is self-verifying, so a task nothing identifies keeps no name rather than
703
+ borrowing a neighbour's.
704
+ - `StepSource.TASK`, marking a step derived from pending writes rather than from a checkpoint.
705
+ - `RawTrace.ingest_warnings`, printed by `ingest` to stderr — disclosures about the *read*
706
+ (for example `Send` packets that could not be decoded), which are not part of the run and so
707
+ never enter the artifact.
708
+ - First public release metadata: Apache-2.0 `LICENSE`, `CLA.md`, `CODE_OF_CONDUCT.md`,
709
+ `CONTRIBUTING.md`, `SECURITY.md`, and PyPI classifiers, keywords, authors and project URLs.
710
+ - `tracegraph --version` (`-V`), which prints the bare version and nothing else. There was no
711
+ way to ask an installed build what it was.
712
+ - An sdist allowlist. Previously the source distribution shipped everything git tracked —
713
+ tests, docs, CI workflows, the lockfile and the checkpoint fixtures.
714
+ - A tag-triggered release workflow publishing through PyPI Trusted Publishing: `test-v*`
715
+ rehearses against TestPyPI, `v*` publishes to PyPI. Only the publishing job holds an OIDC
716
+ token, and it checks out nothing, installs nothing, and uploads only files whose digests
717
+ match what the build job recorded. Before any of that, the build refuses a tag whose
718
+ commit is not on the default branch, has no green `tests` run of its own, disagrees with
719
+ the packaged version, or has no dated changelog entry.
720
+ - `scripts/verify_artifacts.py`, which checks that the built distributions are exactly the
721
+ two files that will be uploaded and that the source distribution carries only what the
722
+ packaging allowlist intends. It also refuses tests, docs, CI configuration and the
723
+ lockfile outright, so widening that allowlist cannot quietly start shipping them, and
724
+ refuses any archive member whose path does not go where it reads — a `..` segment is
725
+ what would let a member satisfy both lists and still land somewhere else, or outside
726
+ the archive entirely, when someone extracts it.
727
+ - `scripts/require_ci_success.py`, the release gate described above.
728
+ - A [release runbook](docs/releasing.md) and a
729
+ [public release checklist](docs/public-release-checklist.md).
730
+ - The installed-wheel CI jobs now pin the version they expect and run the artifact
731
+ verifier, so the release checks are exercised on every pull request rather than first at
732
+ tag time.
733
+
734
+ ### Changed
735
+
736
+ - **The distribution is now `agent-tracegraph`.** The plain `tracegraph` name on PyPI belongs
737
+ to an unrelated project. The import package, the console script, the schema `kind`
738
+ constants and the `TRACEGRAPH_*` environment variables are **unchanged** — only the name
739
+ you install differs. Installing from source or from a checkout is unaffected.
740
+ - The build backend version is pinned (`hatchling==1.32.0`), so a release rehearsal and the
741
+ production tag cannot run *different backend versions* against the same commit. This is
742
+ drift control, not reproducibility: hatchling's own dependencies still float. The uv
743
+ version used to build is pinned in the same way.
744
+ - `twine check` is a gate again, pinned to `twine@7.0.0`. It was disabled because twine 6
745
+ rejected the `Metadata-Version: 2.5` this backend emits; twine 7 accepts it.
746
+ - **Artifact schema version 3**, stamped *by content*: an artifact carrying derived task steps
747
+ declares 3 so an older reader refuses it by name instead of failing on an enum, while
748
+ artifacts without them still serialize as version 2 byte for byte. The envelope is inside
749
+ the digest `export-review-candidates` binds to, so a blanket bump would have invalidated
750
+ digests already published for unchanged artifacts.
751
+ - The LangGraph capture disclosure in every report now names both the error channel and task
752
+ pending writes. It remains unconditional: an artifact ingested by an older build, or one
753
+ using an unrecognized checkpoint layout, carries no derived step and is indistinguishable
754
+ from a genuinely clean run.
755
+ - `xxhash` is now a direct runtime dependency (LangGraph hashes task ids with it, but
756
+ `langgraph` itself is only a development dependency here).
757
+ - `langgraph-checkpoint>=4.1` is now declared directly rather than left transitive, because
758
+ ingestion calls its serializer API. `allowed_msgpack_modules` does not exist before 4.1
759
+ (4.0.0 raises `TypeError`), so the floor is what makes the hardening above guaranteed
760
+ rather than incidental.
761
+
762
+ ### Known limitations
763
+
764
+ - Tasks that were *scheduled but never committed* — a run paused by `interrupt()`, stopped by
765
+ the recursion limit, or killed — are not reported. Trace `status` still means "did a step
766
+ error", so such a run reports `ok`.
767
+ - Only the latest persisted error survives per task, so repeated attempts are not
768
+ reconstructed.
769
+ - `Send` packets can only be named where `langgraph.types` is importable; an ordinary install
770
+ has only the `langgraph.checkpoint` half of that namespace, and the gap is disclosed.
771
+
772
+ ## 0.1.0 - 2026-05-30
773
+
774
+ Tagged but never published. Initial read-path causal-graph analysis of LangGraph agent traces.