agent-tracegraph 0.7.0__tar.gz → 0.9.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 (42) hide show
  1. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/.gitignore +4 -0
  2. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/CHANGELOG.md +258 -0
  3. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/PKG-INFO +113 -20
  4. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/README.md +110 -19
  5. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/SECURITY.md +2 -1
  6. agent_tracegraph-0.9.0/contracts/phoenix-runtime.json +8 -0
  7. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/pyproject.toml +4 -2
  8. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/langgraph_checkpoint.py +184 -25
  9. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/otlp_spans.py +79 -18
  10. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/phoenix_export.py +42 -1
  11. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/check.py +8 -1
  12. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/diagnose.py +48 -0
  13. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/explain.py +3 -3
  14. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/memo.py +27 -2
  15. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/patterns.py +52 -33
  16. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/artifact.py +14 -5
  17. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/cli.py +31 -12
  18. agent_tracegraph-0.9.0/src/tracegraph/input_validation.py +107 -0
  19. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/model.py +26 -7
  20. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/normalize.py +6 -2
  21. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/review_candidates.py +29 -12
  22. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/sqlite_snapshot.py +57 -0
  23. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/ladybug.py +44 -15
  24. agent_tracegraph-0.7.0/contracts/phoenix-runtime.json +0 -8
  25. agent_tracegraph-0.7.0/src/tracegraph/input_validation.py +0 -26
  26. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/LICENSE +0 -0
  27. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/analysis-report.schema.json +0 -0
  28. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/check-report.schema.json +0 -0
  29. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/review-candidates.schema.json +0 -0
  30. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/__init__.py +0 -0
  31. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/_atomic_write.py +0 -0
  32. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/_errors.py +0 -0
  33. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/__init__.py +0 -0
  34. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/base.py +0 -0
  35. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/__init__.py +0 -0
  36. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/ahu.py +0 -0
  37. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/motifs.py +0 -0
  38. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/graph_ref.py +0 -0
  39. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/py.typed +0 -0
  40. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/__init__.py +0 -0
  41. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/base.py +0 -0
  42. {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/in_memory.py +0 -0
@@ -12,3 +12,7 @@ wheels/
12
12
  # Local Codex/dev-trio review artifacts (logs, manifests, run metadata).
13
13
  .dev-trio/
14
14
  .DS_Store
15
+
16
+ # memtomem local artifacts (ADR-0011 project_local tier)
17
+ .memtomem/*.local/
18
+ .memtomem/.staging/
@@ -6,6 +6,264 @@ The **artifact** schema version and the **report**/**review-candidate** JSON sch
6
6
  separate contracts from this version number; changes to any of them are called out
7
7
  explicitly.
8
8
 
9
+ ## 0.9.0 - 2026-10-24
10
+
11
+ ### Changed
12
+
13
+ - **JSON nested more than 9000 levels deep is refused on every Python version** (#167). Whether
14
+ deep input was refused depended on the interpreter: 3.12 and 3.13's `json` refuse at about
15
+ 10,000 levels less the C frames already on the stack (9,984 through the CLI), while 3.14's
16
+ parses far deeper, so 3.14 accepted input that 3.12 refused. `input_validation.MAX_JSON_DEPTH`
17
+ (9000) is now checked on the text before it is parsed, wherever tracegraph parses JSON:
18
+ artifacts, OTLP JSON and JSONL, Phoenix exports and `px` output. It counts the text rather
19
+ than the parsed value, so a deep value that a later duplicate key overwrites still counts, and
20
+ brackets inside strings do not. `ingest --sqlite` checks each checkpoint's metadata, each
21
+ checkpoint body stored as JSON, and each JSON write the saver reads in the same way, before
22
+ decoding, and refuses with `checkpoint history cannot be decoded (JSON nesting too deep)`. The
23
+ scan counts against the snapshot `timeout`. The message for text input is unchanged,
24
+ `JSON nesting too deep`. On 3.12 and 3.13 this newly refuses nesting from 9001 levels up to
25
+ their own limit; on 3.14 it refuses everything above 9000. Objects already decoded by the
26
+ caller (`artifact.from_obj`, the adapter constructors, a caller-supplied checkpoint saver) are
27
+ not checked (#168). The scan adds about 2.4 s to `ingest-otlp` on a 120 MB input dense with
28
+ brackets and quotes (3.5 s before); the artifact is byte-identical.
29
+ - **The Phoenix + SyncMill E2E runs against Phoenix server 20.24.0** (#129), pinned by its index
30
+ digest in `contracts/phoenix-runtime.json`; it was 18.0.0. The body-free evidence (diagnosis
31
+ `tool-retry-failure@v2`, the diff, one review candidate and the board import) is the same under
32
+ both servers once run-specific ids and timings are masked. The Phoenix CLI pin stays 1.18.8.
33
+ - **CI runs the suite on Python 3.13 and 3.14** (#128), which `requires-python = ">=3.12"` already
34
+ allowed, and the package now lists both classifiers. A new `full suite (Python 3.13|3.14)` job
35
+ runs the `[cypher]` suite on each; the existing jobs and their names are unchanged and stay on
36
+ 3.12. Both versions failed before: a test docstring with a lone-surrogate escape does not compile
37
+ on 3.13 and later, and Python 3.14's `json` parses nesting that earlier versions refuse with
38
+ `RecursionError`; the explicit nesting cap above makes every version refuse the same input.
39
+ - **`analyze` and `export-incident-memo` show recorded task markers** (#127). A run paused by
40
+ `interrupt()` or stopped by the recursion limit has trace status `ok` unless a step failed,
41
+ because the status reports whether a recorded step failed; `inspect` marked the recorded ones
42
+ (a recursion stop has a marker only when ingest is given the graph), but `analyze` and the
43
+ memo said nothing. When a trace records any, `analyze` adds the warning `<label>: recorded
44
+ task markers: 1 interrupted · 1 scheduled · 1 undecided interrupt; trace status reports
45
+ whether a recorded step failed, not whether the run completed.` (non-zero parts only, for the
46
+ current trace and the baseline), and the memo adds a `Recorded Task Markers` line after Trace
47
+ Status. `undecided interrupt` counts the `langgraph.undecided_interrupt` capture gap. The
48
+ markers are records, not completion: see README. This change leaves the artifact, its schema
49
+ version, the trace status and the report schema as they were, and adds nothing to a report or
50
+ memo of a trace without markers.
51
+ - **`ReviewCandidateReport` and `ReviewCandidate` drop fields they do not know** (#126). Both
52
+ kept them, so reading a review-candidate document that carried, say, a `prompt` or `error_msg`
53
+ and writing it back with `dumps` passed the text through. They now discard unknown fields at
54
+ both levels, as the SyncMill and Toolgraph readers do, and a `ValidationError` from either model
55
+ no longer prints the rejected input; `errors()` still includes it unless called with
56
+ `include_input=False`. From the same artifacts, `export-review-candidates` writes the same
57
+ output; re-ingesting a source can change the artifact, and with it each candidate's
58
+ `artifact_digest`. The contract schema stays at version 1 and still allows additional fields.
59
+ When `build_report` builds a candidate that fails model validation, it still raises
60
+ `ValidationError`, now titled with that candidate's trace, since the rejected value is no
61
+ longer shown.
62
+ - **A LangGraph subgraph return is recorded on its edge** (#150). When the parent graph continues
63
+ after a subgraph, LangGraph declares the checkpoint that launched the subgraph as the
64
+ continuation's parent, and ingest links the continuation to a subgraph checkpoint instead. That
65
+ edge used to say `checkpoint_parent`, like a parent LangGraph declared. It now has origin
66
+ `checkpoint_subgraph_return`. `analyze` still counts it as explicit ancestry and the derived
67
+ tree ranks it as a declared checkpoint parent, so the new origin by itself changes neither
68
+ classification nor the tree (the return-edge entries under Fixed can);
69
+ the memo prints the new origin, and `summarize` groups it apart from a declared checkpoint
70
+ parent.
71
+ - **Artifact schema 9.** An artifact with a `checkpoint_subgraph_return` edge declares
72
+ `schema_version` 9, so 0.8.0 and earlier refuse it by version instead of failing on the new
73
+ origin value. An artifact written by 0.8.0 keeps its version and bytes when it is loaded and
74
+ saved again, and its return edges still say `checkpoint_parent`; a schema 1 artifact is still
75
+ migrated when it is loaded.
76
+ This entry changes nothing else in a fresh ingest; the OTLP and LangGraph entries in this release
77
+ can.
78
+ - **`explain` and `query --explain` describe fan-in as "more than one recorded cause" for every
79
+ lossy chain** (#143). A trace without a heuristic-parent disclosure used to print "multiple real
80
+ causes … trust this raw chain, not the tree", and `query --explain` said the lossy step was "in
81
+ this match" although it can be an ancestor outside the match. Both now print the wording that
82
+ traces with `otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback` have printed since
83
+ 0.8.0, because a recorded cause may have been reconstructed by the adapter without a mark. The
84
+ heuristic-parent caveat itself is unchanged and still appears only for those traces.
85
+ - **A loop parent chosen by start order is recorded on its edge, and `analyze` no longer treats
86
+ it as declared ancestry** (#142). When several spans carry the `graph.node.id` a span names as
87
+ its `graph.node.parent_id`, OTLP and Phoenix ingest take the nearest preceding one by start
88
+ time or start number. That edge used to say `graph_parent`, like a parent with one candidate,
89
+ so a failing loop run that happened to be picked made the child's failure "propagated" instead
90
+ of primary. The edge now has origin `graph_parent_heuristic`: `analyze` splits primary and
91
+ propagated failures only through declared origins, the edge still appears among a finding's
92
+ causal steps and edges with that origin, the memo writes it "preceded by (graph parent, chosen
93
+ heuristically)", and `summarize` groups it apart from a declared graph parent. The derived tree
94
+ is unchanged (the origin ranks as a graph parent does). When the span also links to the parent
95
+ that was picked, the link declares the edge: it stays `graph_parent` and is not counted under
96
+ `otlp.graph_parent_by_timing`, so a trace whose every timed choice is linked no longer carries
97
+ that code, and loses its heuristic-parent warnings unless it also has
98
+ `otlp.graph_parent_fallback`. The `otlp.graph_parent_by_timing` gap is otherwise recorded as
99
+ before.
100
+ - **Artifact schema 8.** An artifact with a `graph_parent_heuristic` edge declares
101
+ `schema_version` 8, or 9 if it also has a `checkpoint_subgraph_return` edge, so 0.8.0 and
102
+ earlier refuse it by version instead of failing on the new origin value. As far as this change
103
+ goes, every other artifact keeps the version it had, and its bytes too, except a fresh ingest
104
+ whose timed choices are all linked, which drops that gap as above. Artifacts written by
105
+ earlier releases are read as before: their timed edges say `graph_parent`, so `analyze`
106
+ classifies them as it did, and the memo keeps qualifying every graph parent in a trace with
107
+ `otlp.graph_parent_by_timing`.
108
+
109
+ ### Fixed
110
+
111
+ - **`--backend ladybug` no longer hangs on gap patterns or slows down on long ones** (#176,
112
+ #173). `LadybugStore` ran every pattern the Cypher compiler accepts as one native query. A
113
+ bounded gap compiles to a variable-length relationship, and LadybugDB enumerates every causal
114
+ path within it, exponentially many once steps have several causes: `query
115
+ tool-retry-failure-near --backend ladybug` (`gap=(2, 30)`) gave no result within 120 s on a
116
+ 200-step trace with two causes per step, where the default backend takes 0.02 s. A large
117
+ result could also end in `RuntimeError: Buffer manager exception`. Planning cost also grows
118
+ with the predicate count: 17.5 s for 32 predicates on a one-step trace, and a `RuntimeError`
119
+ at 1000. `LadybugStore` now runs a pattern as Cypher only if it has no gap after its first
120
+ predicate and at most six predicates (`MAX_NATIVE_PREDICATES`); every other pattern runs on
121
+ the pure-Python matcher, with the same matches, and `fell_back_to_python` is set.
122
+ `compile_to_cypher` is unchanged and still compiles bounded gaps.
123
+ - `query tool-retry-failure-near --backend ladybug` now prints the fallback note on stderr.
124
+ It used to run as Cypher without one. The other presets behave as before: the strict ones
125
+ run as Cypher, and the two with an unbounded gap already fell back.
126
+ - The note now reads `LadybugDB does not run patterns with a gap or more than 6 predicates;
127
+ ran the pure-Python matcher over the raw causal graph — the results are the same.` It
128
+ said `pattern not Cypher-compilable (unbounded gap)`.
129
+ - **A `PathPattern` of about 1000 or more predicates no longer fails with `RecursionError`**
130
+ (#170). The pure-Python matcher behind `find_matches`, `iter_matches` and `search` recursed
131
+ once per predicate; it now walks the predicates with an explicit stack. Matches and the order
132
+ `iter_matches` yields them in are unchanged. CLI `query` and `analyze` run short presets and
133
+ were not affected. `LadybugStore` runs long patterns on the same matcher (#173).
134
+ - **Deeply nested input no longer crashes ingestion with `RecursionError`** (#168). An entry point
135
+ that reads a document now reports a `RecursionError` raised while reading it as the
136
+ `ValueError` it promises, so the CLI exits with a usage error (2) instead of a traceback.
137
+ - The entry points are `artifact.from_obj`, `OTLPSpanAdapter.from_documents`, `.discover` and
138
+ `.ingest`, `PhoenixExportAdapter(document)` and `.ingest`, and
139
+ `LangGraphCheckpointAdapter.discover` and `.ingest`, which `ingest --sqlite` runs.
140
+ - Messages: `artifact cannot be read: recursion limit exceeded`, `trace document cannot be
141
+ read: recursion limit exceeded` (OTLP and Phoenix), and `checkpoint history cannot be read:
142
+ recursion limit exceeded`. They name the limit, not a cause, because a caller-supplied saver,
143
+ graph or object can recurse on its own.
144
+ - Reached from the CLI: `ingest --sqlite` on a crafted checkpoint body that decodes to a value
145
+ nested past the recursion limit, which then crashed while the error channel value was
146
+ formatted. The limit is about 10,000 levels on 3.12 and 3.13 and about 70,000 on 3.14,
147
+ where it is the C stack.
148
+ - Reached from the library: documents the caller decoded, which the #167 text cap does not see,
149
+ with a deep `exception.message`, a deep `schema_version` (formatted into the refusal),
150
+ a deep error channel or pending-write value, or a schedule graph whose `get_state` returns a
151
+ deep config.
152
+ - Decoded documents still have no explicit depth cap: within the recursion limit they are read
153
+ as before. The analysis functions are outside this guarantee.
154
+ - **A copy written after an edit inside a LangGraph subgraph no longer returns through the edit**
155
+ (#158). 0.8.0 returned a `__copy__` of a checkpoint that applied a subgraph's output through
156
+ the invocation's last subgraph checkpoint when that checkpoint preceded the copy, so an edit
157
+ made in between, which the copied checkpoint never saw, could become its cause, with any error
158
+ that edit wrote. A copy now returns through that checkpoint only when every earlier return
159
+ recorded from the same launcher into the same subgraph namespace went through it too, or none
160
+ was recorded. Otherwise nothing records which result it holds: it keeps its declared parent
161
+ without that return, and ingest records the new gap `langgraph.ambiguous_subgraph_return` (one
162
+ per return left out), which makes `check`'s multi-step path rules and `topology-unchanged`
163
+ insufficient evidence; the failure count rule is unaffected, since a copy records no new error.
164
+ The rule applies only to copies: loop checkpoints, which run the subgraph on from its latest
165
+ checkpoint, and `update_state` checkpoints, which apply the caller's values or the launcher's
166
+ stored output, are not withheld, though the other LangGraph entries here change how they
167
+ return. The gap needs no newer schema by itself, but a history that records it also has return
168
+ edges, so its artifact declares schema 9, which 0.8.0 and earlier refuse.
169
+ - **A later run of a LangGraph persistent subgraph is in its continuation's ancestry** (#154). A
170
+ subgraph compiled with `checkpointer=True` keeps one checkpoint namespace across the parent's
171
+ runs, so a later run's input checkpoint declares the previous run's last checkpoint as its
172
+ parent. Ingest treated that run as part of the first one: the parent graph's continuation was
173
+ linked straight to the checkpoint that launched the subgraph, and the later run was missing
174
+ from its ancestry. A subgraph checkpoint whose `metadata.parents` names a launcher the
175
+ namespace has not had yet now begins a new run, which gets that launcher as a second
176
+ `checkpoint_parent` (an error the launcher held is not counted as inherited), and the
177
+ continuation returns through the run's own checkpoints with origin
178
+ `checkpoint_subgraph_return`. This also covers a loop calling the subgraph again, a resume
179
+ after a root `update_state`, and time travel into a subgraph checkpoint. One case is still
180
+ missed: a run finished by editing a subgraph checkpoint after a root `update_state` records
181
+ the original launcher, so the continuation, which declares the root edit, is not linked
182
+ through the run. A persistent subgraph inside a plain subgraph has a namespace without the
183
+ enclosing call's task id (`mid|inner`); it is now linked to that call's checkpoint, which
184
+ launched it, instead of the root's, so the enclosing subgraph's continuation returns through
185
+ it, on the first run too. A replay or edit of a persistent subgraph's launcher is judged by
186
+ `versions_seen` as for other subgraphs (#155). Re-ingesting such a history adds these edges. A
187
+ caller's edit of an earlier run's checkpoint still joins the run in progress, since LangGraph
188
+ resumes from the namespace's latest checkpoint.
189
+ - **LangGraph time travel from a subgraph's launching checkpoint no longer gains the subgraph**
190
+ (#155). An `update_state` of the checkpoint that launched a subgraph as another node, or the
191
+ fork a replay from it writes, holds the state from before the subgraph ran, but ingest linked
192
+ it to the subgraph's last checkpoint. Such a checkpoint now returns through the subgraph only
193
+ if its `versions_seen` shows it applied the subgraph node's task, as an
194
+ `update_state(..., as_node=END)` applying the finished subgraph's output or a copy of the
195
+ continuation does; otherwise it keeps its declared parent. An `update_state` there that writes
196
+ the error channel is therefore recorded as introducing the error even when the subgraph had
197
+ failed. Such a checkpoint has no return edge, so it does not by itself make an artifact
198
+ declare schema 9. `Send`-launched invocations, and nodes whose task id the adapter cannot
199
+ recompute (a plain-channel trigger, some barrier configurations), skip this check; the
200
+ return-checkpoint choice in the #150 entry below still applies to them.
201
+ - **A later edit of a subgraph checkpoint no longer rewrites an earlier return** (#150). The
202
+ return used to go through the subgraph invocation's last checkpoint overall, so an
203
+ `update_state` of one of its checkpoints after the run made the original continuation lose
204
+ the subgraph from its ancestry on the next ingest. A continuation that returns through the
205
+ subgraph under the rules in the entries above now returns through the last subgraph checkpoint
206
+ written before it, which is also what LangGraph resumes from after an interrupted subgraph is
207
+ edited.
208
+
209
+ ## 0.8.0 - 2026-10-20
210
+
211
+ ### Changed
212
+
213
+ - **`analyze` and the incident memo say when a causal parent was chosen heuristically** (#138).
214
+ Ingest records `otlp.graph_parent_by_timing` (a loop parent taken by start time or, since #136,
215
+ by start number) or `otlp.graph_parent_fallback` (a declared graph parent not honored, so the
216
+ span parent is used) in the artifact, and `check` and `summarize` read those codes, but
217
+ `analyze` and `export-incident-memo` did not, so a report or memo built from the artifact did
218
+ not say that a parent may have been chosen heuristically, and the memo wrote a graph parent
219
+ taken by start order as "caused by". `analyze` now adds a warning for each side that
220
+ carries either code, and the memo adds a fidelity disclosure. The record does not say which
221
+ edge was chosen that way, so in a trace with `otlp.graph_parent_by_timing` the memo writes every
222
+ `graph_parent` edge in its causal ancestry graph as "preceded by (graph parent, possibly chosen heuristically)" instead of
223
+ "caused by"; checkpoint parents and span links keep "caused by". Reports and memos of traces
224
+ without these codes are unchanged.
225
+
226
+ - **`explain`, `query --explain` and `inspect` say when a causal parent was chosen heuristically**
227
+ (#140). They read the same two codes: `inspect` prints the caveat `analyze` uses under its
228
+ tree, and `explain` and `query --explain` under every chain with at least one cause (a root
229
+ step has no parent to qualify). In such a trace their fan-in note no longer says "multiple real
230
+ causes" or "trust this raw chain, not the tree"; it says the single-parent tree drops some
231
+ recorded causes and the raw chain lists every recorded ancestor, and the `query --explain` note
232
+ says "in this chain" rather than "in this match", since the step with several causes can be an
233
+ ancestor outside the match. Output for traces without these codes is unchanged. `analyze` still
234
+ classifies failures through a `graph_parent` edge as it did.
235
+
236
+ - **A Phoenix loop parent is chosen by start numbers whose unit is unknown again** (#136). Since
237
+ 0.7.0 a Phoenix time given as a number below 10^17 is reported as unavailable, and with it the
238
+ only evidence for choosing among several spans that carry the `graph.node.id` a span names as
239
+ its `graph.node.parent_id`. The span then fell back to its `parent_id`, or the export was
240
+ refused without one. Now, when that span and every candidate start at such a number, the
241
+ nearest preceding candidate by those numbers is taken. 0.6.1 also chose by them, read as
242
+ nanoseconds, but it dropped a float's fraction first, so with fractional seconds (`a` at
243
+ 1783987200.25, `b` at 1783987200.75, the span at 1783987200.5) it took `b` where this release
244
+ takes `a`. The times themselves stay unavailable. The choice is counted under the existing
245
+ `otlp.graph_parent_by_timing` gap with its own warning line, which says it assumes the export
246
+ gives every time in one unit; `check` treats it as a heuristic edge, as it does a choice by
247
+ readable time. Exports with ISO or nanosecond times are read as before, and so is a set of
248
+ candidates where any one has a readable time, no start, or no number below the floor. The
249
+ 0.7.0 example (spans `a` and `b` at 1783987200 and 1783987201, `c` at 1783987202 with no
250
+ `parent_id`) is accepted again with `b` as `c`'s parent. Where 0.7.0 fell back to the span's
251
+ `parent_id`, the parent taken now can differ, and so can what follows from it. Each case below
252
+ is a separate export built from that example, adding only what it names:
253
+ - The export can be refused if the new choice closes a causal cycle. Add a root span `r` at
254
+ 1783987199 and give `c` the `parent_id` `r` and `b` the `parent_id` `c`: 0.7.0 kept `r` as
255
+ `c`'s parent and exited 0; this release takes `b`, so `ingest-phoenix` and `analyze` exit 2
256
+ and `phoenix diagnose` exits 1 (0.6.1 refused that export too).
257
+ - `analyze` can classify failures differently, because the edge is now a `graph_parent`, which
258
+ it reads as explicit ancestry (#142). Give `c` the `parent_id` `b` and let `b` and `c` fail:
259
+ 0.7.0 reported both as primary failures; this release reports `b` as primary and `c` as
260
+ propagated.
261
+ - Step numbers can change. Add spans `x`, `y` (`parent_id` `x`) and `z` with no start time,
262
+ and give `b` the `parent_id` `y` and `c` the `parent_id` `z`: 0.7.0 numbered `c` 3 and `b` 5;
263
+ this release numbers `b` 4 and `c` 5.
264
+
265
+ Ties are still broken with these spans at time 0, so step order can also differ from 0.6.1.
266
+
9
267
  ## 0.7.0 - 2026-10-20
10
268
 
11
269
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tracegraph
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph.
5
5
  Project-URL: Homepage, https://github.com/memtomem/tracegraph
6
6
  Project-URL: Repository, https://github.com/memtomem/tracegraph
@@ -15,6 +15,8 @@ Classifier: Development Status :: 3 - Alpha
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
21
  Classifier: Topic :: Software Development :: Debuggers
20
22
  Classifier: Typing :: Typed
@@ -38,7 +40,7 @@ tracegraph ingests agent execution traces that already exist (LangGraph checkpoi
38
40
  Phoenix CLI exports, and OpenInference/OTLP spans) into a **normalized causal graph**,
39
41
  then runs analyses the mainstream observability tools don't:
40
42
 
41
- - **`explain`** — walk backward from a failure over the *raw* causal graph to every real cause.
43
+ - **`explain`** — walk backward from a failure over the *raw* causal graph to every recorded cause.
42
44
  - **`diff`** — exact structural regression diff between two runs (rooted-tree / AHU isomorphism).
43
45
  - **cross-trace pattern matching** — e.g. "tool X → retry → tool X → failure" across all runs.
44
46
 
@@ -68,15 +70,26 @@ uvx --from agent-tracegraph tracegraph --version
68
70
  `tracegraph`.** The plain name was already taken on PyPI by an unrelated project, so only the
69
71
  name you install differs — you still run `tracegraph` and still `import tracegraph`.
70
72
 
71
- Requires Python 3.12. CI exercises 3.12 on Linux; other versions and platforms are untested
72
- rather than unsupported.
73
+ Requires Python 3.12 or newer. CI exercises 3.12, 3.13 and 3.14 on Linux; other versions and
74
+ platforms are untested rather than unsupported.
73
75
 
74
76
  ## Design commitment: causality is never faked
75
77
 
76
78
  We persist the **raw multi-parent causal graph** as the system of record (`CAUSED_BY`,
77
- keeping *every* real predecessor) and treat the single-parent tree (`TREE_PARENT`) as an
78
- explicitly **derived, lossy projection**. Any step whose causality is collapsed for the
79
- tree view is flagged `projection_lossy`, so RCA never silently presents a fabricated cause.
79
+ keeping *every* recorded predecessor — every edge the ingest adapter emitted) and treat the
80
+ single-parent tree (`TREE_PARENT`) as an explicitly **derived, lossy projection**. Any step
81
+ whose causality is collapsed for the tree view is flagged `projection_lossy`, so RCA never
82
+ silently presents the tree's single parent as "the" cause.
83
+
84
+ Recorded is not the same as declared by the source. Adapters reconstruct some edges: OTLP and
85
+ Phoenix pick which run of a looped node is the parent by start time or start order, or fall back
86
+ to `parentSpanId`; LangGraph routes a subgraph's return through the last checkpoint that
87
+ subgraph invocation wrote before the parent graph continued. Some sources lose edges: OTLP span links that resolve to no span in the trace,
88
+ and every link in a Phoenix export, which is ingested parents only. `projection_lossy` measures
89
+ projection loss (more than one recorded parent), not capture fidelity. Some known limitations are
90
+ disclosed per trace (`trace.capture` gaps, `causal_fidelity`, `links_preserved`) or per edge (its
91
+ `origin`, such as `checkpoint_subgraph_return` for the LangGraph return edge), and an empty gap
92
+ list does not mean the capture is complete.
80
93
 
81
94
  | analysis | reads |
82
95
  |---|---|
@@ -221,6 +234,17 @@ Phoenix exports preserve span parents but currently do not expose the original O
221
234
  links. These artifacts are explicitly marked `causal_fidelity=parent_only`; tracegraph warns
222
235
  that additional fan-in causes may be missing instead of claiming a complete DAG.
223
236
 
237
+ Phoenix's own export gives times as ISO strings. A time given as a number is read as epoch
238
+ nanoseconds only from 10^17 up; a smaller one (seconds, milliseconds, a relative offset) is in
239
+ a unit tracegraph cannot tell, so it is reported as unavailable and disclosed as
240
+ `otlp.unreadable_timestamp`. Such numbers still order spans for one purpose: when several
241
+ spans carry the `graph.node.id` that a span names as its `graph.node.parent_id`, and that span
242
+ and every candidate start at such a number, the nearest preceding candidate by those numbers is
243
+ the parent. That assumes the export gives every time in one unit, and it is disclosed under
244
+ `otlp.graph_parent_by_timing` with its own warning line, so `check` treats the edge as heuristic.
245
+ Either way the chosen edge has origin `graph_parent_heuristic` rather than `graph_parent`, unless
246
+ the span also links to that parent, which declares it.
247
+
224
248
  The exact Phoenix CLI and server versions used by CI are centrally pinned. See the
225
249
  [Phoenix runtime pin maintenance runbook](docs/phoenix-cli-contract.md) for the controlled
226
250
  upgrade and contract-verification procedure, and the
@@ -304,8 +328,10 @@ The official `tool-retry-failure@v2` matches three consecutive causal steps:
304
328
  retry marker and the endpoint must name the same tool. This strict pattern is review-exportable.
305
329
  `tool-repeat-failure-heuristic` uses an unbounded gap and
306
330
  `tool-retry-failure-near` a bounded gap (up to 30); both are query-only heuristics.
307
- The generic matcher still supports gaps and back-references. Unbounded or oversized
308
- Cypher patterns fall back to the Python matcher without truncating results.
331
+ The generic matcher still supports gaps and back-references. With `--backend ladybug`, only
332
+ patterns without a gap and of at most six predicates run as Cypher; every other pattern runs
333
+ on the Python matcher, with the same results. A gap makes LadybugDB enumerate every causal
334
+ path within it, which grows exponentially once steps have several causes.
309
335
 
310
336
  Every shipped preset has a stable `pattern_id` and positive integer `pattern_version`, shown
311
337
  as `pattern-id@vN` by `presets` and `query`. Increment the version when match semantics or
@@ -317,8 +343,10 @@ A group is one observed shape. Two matches share a group when, for every matched
317
343
  status, source and safe-v1 name agree, and for every edge between them the `CAUSED_BY` origin
318
344
  agrees, under the same source kind, causal fidelity and link setting, and the same answer to
319
345
  whether the trace discloses causal edges chosen heuristically at ingest (`motif-signature/v1`).
320
- That last answer is per trace: an OTLP export records a parent picked by timing with the same
321
- origin as an unambiguous one, and says only that some edge was picked that way, not which. The
346
+ That last answer is per trace: the gap says only that some edge was picked heuristically, not
347
+ which. (A parent picked by start order now has its own origin, `graph_parent_heuristic`, so the
348
+ edge comparison already separates it; an artifact written before that records it as
349
+ `graph_parent`, like an unambiguous one, and a fallback edge is `span_parent_fallback`.) The
322
350
  answer is `disclosed` whenever a recognized heuristic-edge gap is present, else `not_disclosed`.
323
351
  It is `unknown` for a trace with no capture record, or with a gap code this build does not know
324
352
  and no recognized heuristic-edge gap. It is `not_applicable` for a single-step preset, which has
@@ -354,7 +382,13 @@ A `*.json` file in a directory that is not a valid artifact is skipped with a wa
354
382
  `query`; pass `--strict` to fail the batch instead.
355
383
  The public schema is a repository-vendored contract (not wheel package data) at
356
384
  [`contracts/review-candidates.schema.json`](contracts/review-candidates.schema.json); G3
357
- consumers should vendor that file explicitly.
385
+ consumers should vendor that file explicitly. The schema allows additional fields at both
386
+ levels so that version 1 can grow, which means a document that validates can still carry text
387
+ tracegraph never writes, such as a prompt or an error message. A consumer must ignore fields it
388
+ does not know and must not store, display or forward them, as SyncMill and Toolgraph do; a field
389
+ that carries trace body text would need a new major version. `ReviewCandidateReport` in the
390
+ `tracegraph.review_candidates` module reads that way too: it drops unknown fields and keeps rejected
391
+ input out of the `ValidationError` message.
358
392
 
359
393
  SyncMill can consume the report without importing this package:
360
394
 
@@ -394,16 +428,59 @@ Known limits:
394
428
  it such a run reads `ok` unless some other step recorded an error. Trace status stays `ok`
395
429
  either way. A task paused by `interrupt()` is reported without a graph. Both are reported
396
430
  only at the root graph: the subgraph node is named, not the node inside it.
431
+ - Trace status reports whether a recorded step failed, not whether the run completed: a
432
+ paused or stopped run reads `ok` unless a step failed. Next to it, `analyze` (as a warning,
433
+ also in `--json-out`) and `export-incident-memo` show the recorded task markers:
434
+ `interrupted`, `scheduled`, and `undecided interrupt` (an interrupt recorded with a resume or
435
+ an error and no result; it gets no interrupt or scheduled step and is counted from its
436
+ capture gap).
437
+ `inspect` shows the first two. They count what the newest root checkpoint records, per root
438
+ task: an interrupt record outlives a revised graph that finished without writing a
439
+ checkpoint, a resumed subgraph can leave its parent's record, several paused inner tasks
440
+ count as one subgraph task, and `scheduled` is the answer for the graph you passed. A
441
+ malformed pending write is ignored and disclosed, so it can hide a marker; with `--graph`,
442
+ unapplied `Command(goto=...)` input and `Send` packets to a node the graph lacks are
443
+ disclosed rather than counted as scheduled. `check` judges each rule's evidence, not
444
+ completion.
397
445
  - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
398
446
  causes may be missing.
399
447
  - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
400
448
  producer's names, and its error messages unless ingested with `--redact-errors`.
401
- - CI covers Python 3.12 on Linux.
449
+ - CI covers Python 3.12, 3.13 and 3.14 on Linux.
402
450
 
403
451
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
404
452
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
405
453
  span-level. Subgraph checkpoints are namespaced, but cross-namespace causality remains
406
- limited to parent links declared by LangGraph checkpoint metadata.
454
+ limited to parent links declared by LangGraph checkpoint metadata, plus the return edge the
455
+ adapter reconstructs when the parent namespace continues after a subgraph invocation. That edge
456
+ runs through the last checkpoint the invocation wrote before the continuation (a resumed
457
+ subgraph continues from, and returns, its namespace's latest checkpoint) and has origin
458
+ `checkpoint_subgraph_return`, which `analyze` counts as explicit ancestry and the derived tree
459
+ ranks as a declared checkpoint parent. A checkpoint a caller wrote from the checkpoint that
460
+ launched the subgraph (`update_state`, a replay's fork) is linked this way only if its
461
+ `versions_seen` shows it applied the subgraph node's task, so an edit as another node or a
462
+ replay keeps its declared parent, while `update_state(..., as_node=END)` applying the finished
463
+ subgraph's output, or a copy of the continuation, returns through the subgraph. Checkpoints do
464
+ not show everything, so the edge can still be wrong where they cannot: an `update_state` as the
465
+ subgraph node itself (metadata does not record `as_node`), clearing tasks with
466
+ `as_node=END` while the subgraph itself is interrupted, fresh input on that launcher, time
467
+ travel around a `Send` or a node whose task id the adapter cannot recompute (one triggered by a
468
+ plain channel, some barrier configurations), and a subgraph checkpoint written
469
+ after the subgraph finished but before the parent graph continued (a node editing its own
470
+ subgraph while it is still running, or an outside `update_state` of a finished subgraph while a
471
+ sibling task is interrupted). A persistent subgraph (`compile(checkpointer=True)`) keeps one
472
+ namespace across the parent graph's runs; each run is told apart by the launcher its
473
+ checkpoints record in `metadata.parents`. A caller's edit of an earlier run's checkpoint joins
474
+ the run in progress, because the subgraph resumes from its namespace's latest checkpoint. A
475
+ `__copy__` of a continuation holds output an earlier checkpoint applied, and nothing records
476
+ which one: when earlier checkpoints from that launcher returned through a different subgraph
477
+ checkpoint than the last one written before the copy (a subgraph checkpoint was edited in
478
+ between, or the launcher was resumed twice), the copy keeps its declared parent without that
479
+ return, and the gap `langgraph.ambiguous_subgraph_return` is recorded, which makes `check`'s
480
+ multi-step path rules and topology rules insufficient evidence. An `update_state` from the launcher still returns
481
+ through the last subgraph checkpoint written before it, edited or not. When an interrupted run is
482
+ finished by an `update_state` of the subgraph after the launcher itself was edited, the
483
+ continuation keeps the edited launcher as its parent and does not return through the subgraph.
407
484
 
408
485
  ## Develop
409
486
 
@@ -456,6 +533,18 @@ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logic
456
533
  comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
457
534
  Containment and unknown edges preserve all error investigation candidates, deeper first.
458
535
  Explicit causal ancestry supplies context, not proof that an exception propagated.
536
+ When the artifact records that a causal parent was chosen heuristically at ingest
537
+ (`otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback`), `analyze` adds a warning for
538
+ that side and the memo a fidelity disclosure. A loop parent picked by start order has origin
539
+ `graph_parent_heuristic`: `analyze` does not count it as explicit ancestry when it splits primary
540
+ from propagated failures, and the memo writes it "preceded by (graph parent, chosen
541
+ heuristically)". An artifact written before that origin existed does not say which edge, so a
542
+ memo of such a trace with `otlp.graph_parent_by_timing` writes every `graph_parent` edge as
543
+ "preceded by (graph parent, possibly chosen heuristically)" instead of "caused by", and
544
+ `analyze` classifies it as before. `inspect` prints the
545
+ same caveat under its tree, and `explain` and `query --explain` under every chain that has at
546
+ least one cause; there their fan-in note no longer calls the causes real or says to trust the
547
+ chain.
459
548
  Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
460
549
  artifacts are not reinterpreted.
461
550
  A span whose status is UNSET and that carries no exception event (one with a message or
@@ -525,7 +614,9 @@ managed values, cache-key functions — though no node runs. It is never inferre
525
614
  Every ingest records what the adapter could not observe: `trace.capture` is an object whose
526
615
  `gaps` list holds a fixed code and count per ingest disclosure (empty when there was none),
527
616
  and a step's `unknown_fields` names a kind, name or error message the adapter substituted. That
528
- record makes a fresh artifact declare `schema_version` 7. The envelope otherwise follows
617
+ record makes a fresh artifact declare `schema_version` 7, 8 when a causal edge has origin
618
+ `graph_parent_heuristic`, or 9 when one has origin `checkpoint_subgraph_return`. The envelope
619
+ otherwise follows
529
620
  content, which is what an artifact written by an earlier release keeps when it is loaded and
530
621
  saved again: a graph read (`--graph`) needs 6, interrupt steps 5, a derived tree that relies on
531
622
  EdgeOrigin precedence 4, derived task steps 3, and anything else 2, byte for byte (a v1 file is
@@ -546,7 +637,7 @@ withheld, and a single unparseable cost withholds the whole total instead of und
546
637
  An OTLP or Phoenix import records an unreadable total cost as `unreadable` for that reason, and
547
638
  reports token counts, other costs, currencies and evaluation scores it cannot read as
548
639
  unavailable, disclosing each on stderr; the producer's value is never kept. File input, stdin
549
- and Phoenix `px` output share one size limit (`TRACEGRAPH_MAX_ARTIFACT_BYTES`). `query` reads
640
+ and Phoenix `px` output share one size limit (`TRACEGRAPH_MAX_ARTIFACT_BYTES`). JSON nested more than 9000 levels deep is refused as `JSON nesting too deep` on every Python version, counted on the text before it is parsed; `ingest --sqlite` checks the JSON in the thread's checkpoint rows the same way. A document the caller decoded and passes to the library is not depth-capped, but reading one too deep to process is refused as `ValueError` (`... cannot be read: recursion limit exceeded`), not a crash. `query` reads
550
641
  artifacts one after another and keeps only the matches it prints (plus the traces `--explain`
551
642
  renders). With `--limit`, its peak memory therefore grows with the number of artifacts only
552
643
  by a small per-file record (path, trace id); without it, by the matches it prints. Inside one
@@ -554,16 +645,18 @@ trace, too, it keeps only the matches that still fit under `--limit`, but it sti
554
645
  and counts every match to report the exact total, so time grows with the match count: one
555
646
  2500-step trace with 3.1M matches takes about 3 s at `--limit 1`, in 70 MiB. The search's own
556
647
  working state (steps reached at each distance of a gap) does not depend on `--limit`. With
557
- `--backend ladybug` on a pattern that compiles to Cypher, LadybugDB itself buffers the
648
+ `--backend ladybug` on a pattern that runs as Cypher, LadybugDB itself buffers the
558
649
  query's full result set: a 3001-step trace with 2.25M `tool-retry-failure` matches peaked
559
650
  at 757 MiB, where the store alone takes about 300 MiB. A plain read, `count(*)` and
560
- `ORDER BY … LIMIT` all buffered it in our measurements.
651
+ `ORDER BY … LIMIT` all buffered it in our measurements. The six-predicate limit on Cypher
652
+ bounds the query's planning, not its result: a short strict pattern over a graph where steps
653
+ have many effects can still have a large result, and LadybugDB buffers all of it.
561
654
 
562
655
  Both stores isolate returned mutable objects. InMemory node upsert replaces by ID;
563
656
  Ladybug's existing insertion semantics are unchanged. Use `is_isomorphic` or `diff` for
564
657
  deep-tree equality; `canonical()` preserves nested tuples, whose external Python equality
565
- can still reach the interpreter recursion limit. CI currently covers Python 3.12/Linux;
566
- other supported Python versions and platforms require separate validation.
658
+ can still reach the interpreter recursion limit. CI currently covers Python 3.12–3.14/Linux;
659
+ other Python versions and platforms require separate validation.
567
660
 
568
661
  ## Contributing, security, and license
569
662