Skip to content

Refuse sessions with non-object event data instead of printing a traceback - #293

Merged
Siddhant-K-code merged 5 commits into
mainfrom
siddhant-k-code-non-object-event-data-tracebacks
Oct 25, 2026
Merged

Siddhant-K-code merged 5 commits into
mainfrom
siddhant-k-code-non-object-event-data-tracebacks

Conversation

@Siddhant-K-code

@Siddhant-K-code Siddhant-K-code commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Commands that read keys from event data printed a Python traceback when an event's data was not a JSON object. agent-strace server stores array, string, number and boolean data from POST /events, and a hand edit can store null, so these sessions exist in normal use. With this change, those commands refuse the session the same way share has since #287: they write one Error: line to stderr, exit with code 1, and print, write, send or open nothing. The one exception is optimize with an LLM configured, described in decision 1.

On 024814f, agent-strace explain on such a session ends with a full traceback and AttributeError: 'str' object has no attribute 'get'. With this branch:

$ agent-strace explain b2c3d4e5f6a70002
Error: session event data must be an object: event 6 of 25 (tool_call) has string data; 5 more events have non-object data
$ agent-strace team-report
Error: session c3d4e5f6a7b80003 event data must be an object: event 3 of 7 (tool_call) has array data

Both exit with code 1. There is no version bump and no release file change.

Changes after review at e3dc9a3

Both new commits change only tests/test_non_object_event_data_mirrors.py. src/ is the same as at e3dc9a3, so the case table, the sweep and the other results below still describe this branch.

  • 96fc036 covers the mutant that the review found. In otlp_data_indexes, changing event.event_type in (EventType.TOOL_RESULT, EventType.ERROR) to (EventType.TOOL_RESULT,) passed the full suite, while export --format otlp printed a traceback.
    • Take a session with a tool_call with data 42, an error that pairs with it, a tool_call that reuses its ID and a tool_result that pairs with that. export --format otlp -o out.json on main and on the mutant prints AttributeError: 'int' object has no attribute 'get'. This branch prints Error: session event data must be an object: event 2 of 6 (tool_call) has number data.
    • The export fixture now has a tool_call at event 34 that reuses the ID of the tool_call at event 15, after the error at event 16 pairs with event 15. Plain otlp reads event 15 when the error pairs with it. Under the mutant the error does not pair, event 34 replaces event 15 in the mirror's pending map, and the mirror never returns event 15.
    • The mirror test now fails on that mutant, for export otlp -o and export otlp --endpoint, with tool_call e1e2e3e4e5e60011 #15 = 42: raised AttributeError: 'int' object has no attribute 'get' (otlp.py:316). The full suite fails with that one failure.
    • Event 34 alone would hide another mutant that the test caught at e3dc9a3: the otlp-genai error branch not removing the tool_call from its pending map. Event 34 replaces event 15 in that map either way. Events 32 and 33 add a tool_call that an error pairs with and whose ID nothing reuses, so that mutant still fails the test.
  • a140125 covers three more single-line mutants that also passed the full suite at e3dc9a3. It is test-only, and you can drop it on its own.
    • otlp_data_indexes or otlp_genai_data_indexes not pairing a tool_result with its tool_call. Event 35 reuses the ID of the tool_call at event 13, after the tool_result at event 14 pairs with it. Take a session with a tool_call with data 42, a tool_result that pairs with it, and a tool_call that reuses its ID. With export --format otlp -o and --format otlp-genai -o, main prints a traceback, each mutant prints one for its format, and this branch prints Error: session event data must be an object: event 2 of 5 (tool_call) has number data.
    • otlp_genai_data_indexes pairing an llm_response that names its request with the oldest open request instead. Events 36 to 38 pair an llm_response with its request by parent while an older request is open, and then replace the older request. On a session shaped like that, with data 42 on the older request, main and this branch export it with --format otlp-genai -o and exit 0. The mutant refuses it with event 2 of 6 (llm_request) has number data.
  • I did not rebase. Main moved to 7143ae0 (Honor AGENT_TRACE_DIR in CLI read commands #296, fix(hooks): remove pending-calls state when a session ends #297 and release: prepare v0.102.1 #298). The PR still merges cleanly, and the merged tree passes the full suite on Python 3.14 and 3.10.

Changes after review at 8929bcc

  • 1b9c828 fixes the review items:
    • export --format otlp-genai no longer refuses a session because of a tool_call that an error event pairs with. session_to_otlp_genai never reads that tool_call, and main exports the session with exit code 0. It also no longer refuses because of an llm_request or tool_call that a later event with the same ID replaced before anything paired with it. otlp_genai_data_indexes replaces OTLP_GENAI_DATA_EVENT_TYPES. See decision 4.
    • export --format temporal no longer prints a traceback, with -o or --endpoint, on a session that has a Temporal trace context and non-object data. See decision 11.
    • New differential test, tests/test_non_object_event_data_mirrors.py, described under Tests. Removing any of the five checks that the review found untested now fails it.
    • The postmortem_data_indexes docstring now names the test that checks it, and docs/commands.md describes the temporal and optimize behavior.
  • e3dc9a3 is a fix the review did not ask for. Plain export --format otlp no longer refuses a session because of a tool_call that a later tool_call with the same ID replaced before anything paired with it. See decision 12. At that point you could drop it on its own. 96fc036 and a140125 now build on its test rows.
  • I did not rebase. Main moved to 9beb313 (Register Codex SessionEnd, SubagentStart and SubagentStop hooks #294 and release: prepare v0.102.0 #295). The PR still merges cleanly, and the merged tree passes the full suite on Python 3.14 and 3.10.

The same cases on main, at 8929bcc and on this branch, each run in a temporary cwd and HOME:

Case main 024814f 8929bcc e3dc9a3
export --format temporal, -o or --endpoint, Temporal context, tool_result with array data traceback traceback Error: session event data must be an object: event 4 of 5 (tool_result) has array data, exit 1
export --format otlp-genai -o, tool_call with data 42 that an error pairs with exit 0, file written refuses exit 0, file written
export --format otlp-genai --endpoint to a closed port, same session tries to send, OTLP GenAI export failed, exit 1 refuses same as main
export --format otlp -o, tool_call with data 42 replaced by a tool_call with the same ID exit 0, file written refuses exit 0, file written

What changed

  • New src/agent_trace/event_data.py. It holds:
    • NonObjectEventDataError, a ValueError.
    • require_object_event_data(events, *, event_types=None, indexes=None, session_id=None). The message gives the 1-based position, the event type and the JSON type, and counts every other non-object event in the session. It never includes data values or event IDs.
    • skipped_event_warning, used by the live watchers.
  • disclosure.py re-exports both names, so from agent_trace.disclosure import NonObjectEventDataError, require_object_event_data still works. share output does not change.
  • cli.py:
    • One catch point, _run_handler, called by _run_with_product_telemetry, writes Error: <message> to stderr and returns 1.
    • cli.py also checks the data for stats, export --format otlp and export --format otlp-genai. For the two export formats it checks the events that otlp_data_indexes and otlp_genai_data_indexes in otlp.py return. It re-raises NonObjectEventDataError before its except ValueError, so a temporal refusal prints the standard line.
  • Command modules check before they print or write anything. These 30 call require_object_event_data: a2a, audit, budget_report, cognitive_debt, compaction, compare, compliance, context_score, cost, dashboard, diff, explain, inflation, langfuse_export, mcp_scan, oncall, optimize, policy, postmortem, pr_comment, replay, sample, standup, subagent, team_report, temporal, tenancy, timeline, token_budget and why. otlp holds the index helpers for the export checks, and watch uses skipped_event_warning.
    • temporal.py runs the otlp-genai check in session_to_temporal_otlp, right after _validate_ids. export_temporal_otlp re-raises NonObjectEventDataError before its except ValueError.
  • docs/commands.md has a new section, "Sessions with non-object event data". It lists the refusing commands, the message format, the commands that keep working, and the watch and mcp-scan behavior. The share section links to it. It also says which export formats pair events by ID, that temporal checks the Temporal trace context first, and when optimize with an LLM can refuse after it sent the request.
  • Tests: tests/test_event_data.py, tests/test_non_object_event_data.py and tests/test_non_object_event_data_mirrors.py, described below.

Decisions

  1. Refuse up front, as fix(share): reject sessions whose event data is not an object #287 does.
    • Each command checks before its first output, so a refused command writes nothing to stdout, creates no file, sends nothing and opens no browser.
    • A partial report built from a session the command cannot read would be misleading, and this keeps every command consistent with share.
    • optimize with --base-url and --api-key is the one exception. It checks the events in its prompt before it sends the LLM request, so a refusal there sends nothing. When the LLM returns no patterns, it falls back to its heuristics, which read more events, so it can refuse after it sent the request. Main sends the same request and then crashes.
  2. One helper and one catch point.
    • Commands raise NonObjectEventDataError from the shared helper, and _run_handler prints it. No command formats its own error line.
    • cost --breakdown, cost --tenant and tenant report already catch ValueError and print Error: {exc}. That is the same line, so I left those handlers alone.
  3. Move the error class to event_data.py and keep the old import path. About 30 commands with nothing to do with share disclosure now use the check, so it lives in its own module next to the watch warning helper. disclosure.py re-exports both names.
  4. Check only the data each command reads.
    • Each check is limited to the event types or positions whose data that command reads, so it fires only where the old code would crash. A session keeps working with every command that never reads the bad event, which keeps verify, list and export (json, ndjson, csv) unchanged.
    • For example, explain still works when only a file_read event is bad, and replay --limit 5 still works when the bad event comes after the fifth event. The tests compare such cases against a clean store.
    • The OTLP exports pair events by ID, so a check by event type refused sessions they export. otlp_genai_data_indexes and otlp_data_indexes replay each converter's pairing and return the events whose data it reads:
      • otlp-genai and temporal read an llm_request when an llm_response pairs with it or when it is still unpaired at the end, and a tool_call when a tool_result pairs with it or when it is still unpaired at the end. They do not read a tool_call that an error pairs with, or an llm_request or tool_call that a later event with the same ID replaced while it was unpaired.
      • Plain otlp reads every llm_request. It reads a tool_call when a tool_result or an error pairs with it or when it is still unpaired at the end, but not when a later tool_call with the same ID replaced it while it was unpaired.
  5. replay refuses up front instead of printing bad events raw.
    • It used to print the events before the bad one and then crash. Refusing before the first line matches fix(share): reject sessions whose event data is not an object #287 and the other commands.
    • replay --format html and replay --diff write files, which must not happen for a session they cannot read. agent-strace export --format json still shows the raw data.
    • replay --format html copies data with dict(), so it refuses only data that dict() rejects. An empty array, an empty string or an array of key-value pairs still renders as before.
  6. watch keeps running and skips the checks for the bad event.
    • A live event with non-object data is still counted and still sent to --stream-to. When a check would read its data, watch skips the checks for that event and writes one line to stderr, for example [watch] Skipped checks for one tool_call event because its data is an array, not an object.
    • Stopping a long-running monitor because of one event would end monitoring of every later event. Events that no check reads behave as before.
  7. watch --compaction-checkpoint refuses at startup when the stored session already has non-object data, because the checkpoint watcher reads every stored event when it starts. Live events with non-object data are left out of checkpoints.
  8. mcp-scan --watch refuses at startup when the session history has non-object data, because scan_behavioural_anomalies reads every history event. Live events with non-object data are skipped with one [mcp-scan] warning line, and a non-object session_end still ends the watch.
  9. Commands that can read more than one session name the session in the message, so the user knows which one to fix.
    • pr-comment, policy generate and mcp-scan --session name it even for one session, because they share that code with their multi-session modes.
    • Commands that only ever read one session use the same wording as share.
  10. Telemetry records no error_type. The catch point returns 1 instead of raising, so the completion event has success: false and exit_code: 1 with no error_type, the same as a session refused by share.
  11. export --format temporal checks the Temporal trace context first.
    • session_to_temporal_otlp runs the otlp-genai check right after _validate_ids, so a session without a usable context keeps its Temporal export failed message and exit code 1.
    • NonObjectEventDataError is a ValueError, so cmd_export and export_temporal_otlp re-raise it before their except ValueError handlers. Without that, the refusal would print as Temporal export failed: session event data must be an object instead of the standard Error: line.
  12. The plain otlp fix for replaced tool_calls is a separate commit, e3dc9a3.
    • The review did not ask for it. I found it while writing the otlp-genai mirror. Main exports such a session with exit code 0 and 8929bcc refused it, which broke the requirement that working commands keep working. A server client that posts its own event IDs can store such a session.
    • To keep only the review fixes, drop e3dc9a3. 1b9c828 does not depend on it and passes the full suite on its own.
    • 96fc036 and a140125 add export fixture rows after the rows e3dc9a3 adds, so dropping e3dc9a3 now conflicts in the test file, and their rows move three positions earlier. Tell me if you want that version and I will push it.

Command sweep, before and after

I built a fixture store with three sessions:

  • a clean session;
  • a 25-event session with a subagent;
  • the subagent's own 7-event session.

I ran 184 command variants, covering every command and mode that reads session events, on base 024814f and on this branch. Each run used a temporary cwd and HOME, refused all network connections and stubbed the browser.

  • Main sweep (81 scenarios). A clean store, plus non-object data on each of the 14 event types, on every event, and on the subagent session. Each case used array, string, number, boolean and null data.
  • Edge-value sweep (112 scenarios). The same targets with [], an array of key-value pairs, an array of key names, 0, false, "" and a string of key names. These values slip past data or {}, dict(data) and in checks.
Sweep Runs Traceback on base Refuses now Identical Identical after normalizing Traceback on both
Main 14904 2585 2510 10391 1928 75
Edge values 20608 3529 3424 14545 2534 105
  • Refusals: every refusal is one line in the message format, with exit code 1, empty stdout, no file written in the cwd or HOME, no network connection and no browser opened.
  • Normalizing: these runs have the same exit code, stdout and stderr once timestamps, dates, UUIDs, generated IDs, base64 values and relative ages are replaced, and they write files with the same names. Their output includes the current time or generated IDs.
  • Traceback on both: every one of these runs is verify --from-export exp.json.
    • The sweep creates exp.json with export --format eu-ai-act --output exp.json. That export crashed on base and refuses now, so the file never exists and verify raises FileNotFoundError on both.
    • verify --from-export missing.json raises the same error on base with an empty store. It never reads event data, so this PR leaves it alone.
  • watch: 8 variants over 81 scenarios, 648 runs.
    • 155 runs that crashed on base now skip with a warning, and 80 now refuse at startup (--compaction-checkpoint).
    • 413 are unchanged, and no run crashes.
  • agent-strace mcp and server: the stdio tools (81 scenarios) and the server GET routes give the same responses as on base.
  • After the review fixes: 1b9c828 and e3dc9a3 change only cmd_export, otlp.py, temporal.py and a postmortem docstring. I re-ran the 16 export variants on fresh fixtures, 3088 runs each on base, 8929bcc and e3dc9a3. Their counts are the same as in the sweep at 8929bcc. Every e3dc9a3 run matches 8929bcc byte for byte, except 26 eu-ai-act runs that print the current time and match after normalizing.

Commands that printed a traceback on base and now refuse (84 sweep variants, plus export --format temporal):

  • a2a-tree
  • audit (also with --verify-chain and --policy)
  • budget-report
  • cognitive-debt
  • compaction (also with --diff and --behavior-diff)
  • compare (decision events, --last, --rerun)
  • compliance export
  • context-score --file
  • cost (session, --since, --csv, --breakdown, --tenant)
  • dashboard --trend
  • diff (also with --compare and --semantic)
  • explain
  • export --format otlp, otlp-genai, temporal and eu-ai-act. temporal crashes only on a session with a Temporal trace context. The sweep fixture had none, so the review at 8929bcc found it, and the sweep counts above leave it out.
  • export --backend otlp and langfuse
  • inflation
  • mcp-scan --session
  • oncall
  • optimize --dry-run
  • policy generate
  • postmortem (session and --list)
  • pr-comment --dry-run
  • replay (text, --filter, --limit, --expand-subagents, --diff, --format html)
  • sample (every strategy)
  • standup
  • stats
  • team-report
  • tenant report
  • timeline
  • token-budget
  • why

The earlier sweep listed cognitive-debt, context-score, inflation and token-budget as safe. They crash only when the bad event has a type they read:

  • cognitive-debt on tool_call or file_write;
  • context-score --file on tool_call;
  • inflation on user_prompt, llm_request, llm_response or assistant_response;
  • token-budget on llm_request or llm_response.

These commands exit 0 in every scenario before and after (68 variants):

  • annotation and audit: annotate, audit-readiness, audit-tools
  • store and setup: backfill-meta, baseline, config-watch affected, retention, tenant delete and tenant export
  • reports: context-score, curve, dashboard, drift --save-baseline, fingerprint, freshness, health
  • evals: eval compare, eval dataset
  • export and identity: export --format json, ndjson and csv, identity sign and identity verify
  • reading sessions: inspect, lint, list, replay --tree, stats --include-subagents, tree, verify, and why on the first event
  • integrations: mcp, mcp-scan without --session
  • policy: policy backtest, policy coverage, policy diff
  • skills: skills, skills-eval

freeze, regression, compare on non-decision events, compliance-report, org-report and share --assignment already exited 1 with their own message, and they still do.

Tests

  • tests/test_event_data.py (11 tests) covers:
    • the message for each JSON type;
    • the event_types and indexes filters;
    • the count of other bad events;
    • session naming;
    • the warning text;
    • the old disclosure import path.
  • tests/test_non_object_event_data.py (21 tests) ports the sweep fixture. It runs main() in-process with network connections refused, the browser stubbed and telemetry captured.
    • Refusals:
      • 80 rows put the bad event in the main session and 22 rows put it in the subagent session, across 29 commands and all five JSON types.
      • Each refusal asserts the exact stderr line, empty stdout, exit code 1, and a telemetry record with success: false, exit_code: 1 and no error_type. It also asserts no network, no browser and no file changes.
      • explain, timeline, cost, stats and replay refuse each of the five JSON types.
      • A subprocess test runs explain, timeline and cost and asserts that stderr has no traceback.
    • Unchanged behavior:
      • 16 command and event-type pairs that do not read the bad event give the same output as a clean store.
      • verify still checks the chain, export json and csv keep the raw data, and list shows every session.
      • replay --format html still renders data that dict() accepts.
    • watch:
      • Events checked on arrival are skipped with a warning.
      • llm_request is skipped only when the token budget reads it.
      • Events that no check reads are not skipped.
      • A skipped event is still streamed.
      • The checkpoint watcher receives only object data.
      • --compaction-checkpoint refuses at startup.
    • mcp-scan --watch: it refuses at startup, skips a live non-object event with a warning, and still ends on a non-object session_end.
  • tests/test_non_object_event_data_mirrors.py (8 tests) checks the commands whose check mirrors what they read: the OTLP and Langfuse exports, temporal, replay, why, token-budget, compare --rerun, postmortem, sample, mcp-scan --session, replay --expand-subagents and optimize.
    • It stores a non-object value as the data of one event at a time and runs each command twice: as shipped, and with require_object_event_data patched to a no-op in every agent_trace module that imports it.
    • The shipped run must refuse exactly when the unchecked run raises. A refusal must print only the standard line, exit 1, change no file, and use no network or browser.
    • The fixtures include:
      • tool_calls that an error pairs with, with and without a later tool_call that reuses the ID;
      • a tool_call whose ID a later tool_call reuses after a tool_result pairs with it;
      • llm_requests and tool_calls that one with the same ID replaces before anything pairs with them;
      • an llm_response that names its request while an older request is open;
      • a Temporal trace context;
      • postmortem sessions that succeed, fail and crash;
      • two sessions with the same tool sequence for sample --deduplicate;
      • an earlier session with session_start data for mcp-scan --session.
    • optimize runs with --base-url, --api-key and a patched _call_llm. When the prompt's events refuse, the LLM is not called. When the LLM returns no patterns, the one heuristics refusal comes after one call.
Run Tests Result
main 024814f, Python 3.14 2374 OK (skipped=2)
main 7143ae0, Python 3.14 2412 OK (skipped=2)
8929bcc, Python 3.14 2406 OK (skipped=2)
8929bcc, Python 3.10 2406 OK (skipped=3)
this branch a140125, Python 3.14 2414 OK (skipped=2)
this branch a140125, Python 3.10 2414 OK (skipped=3)
a140125 merged with main 7143ae0, Python 3.14 2452 OK (skipped=2)
a140125 merged with main 7143ae0, Python 3.10 2452 OK (skipped=3)
96fc036 without a140125, Python 3.14 2414 OK (skipped=2)
1b9c828 without e3dc9a3, Python 3.14 2414 OK (skipped=2)

The extra skip on Python 3.10 is the existing tomllib test that needs Python 3.11.

The new tests fail without the fix. Against git archive 024814f src:

  • tests/test_non_object_event_data.py reports FAILED (failures=4, errors=151), with subtests counted one by one. 13 of its 21 tests fail. The 8 that pass check behavior this PR keeps: the 2 fixture tests, the 5 unchanged-behavior tests, and the watch test for events that no check reads.
  • tests/test_event_data.py and tests/test_non_object_event_data_mirrors.py cannot import agent_trace.event_data.
mkdir -p /tmp/base && git archive 024814f src | tar -x -C /tmp/base
HOME=$(mktemp -d) PYTHONPATH=/tmp/base/src python3 -m unittest tests.test_non_object_event_data

tests/test_non_object_event_data_mirrors.py also fails on this branch when any one of these is undone, one at a time:

  • each of the five checks the review found untested: the mcp-scan --session check of an earlier session's session_start, the optimize LLM-path check, the sample --deduplicate check, and both postmortem_data_indexes scans;
  • the otlp-genai pairing for an error, the temporal check, and each of the two re-raises;
  • three changes to the llm_request pairing;
  • the plain otlp pairing for a replaced tool_call (e3dc9a3);
  • the error pairing in otlp_data_indexes, the mutant from the review at e3dc9a3 (caught since 96fc036);
  • the tool_result pairing in otlp_data_indexes and in otlp_genai_data_indexes, and the parent match for an llm_response in otlp_genai_data_indexes (caught since a140125).

I ran 46 mutants against each committed version of this test file. 45 are small changes to the checks, the re-raises and the pairing helpers otlp_data_indexes, otlp_genai_data_indexes and langfuse_data_indexes. The last one puts back 8929bcc's otlp.py, cli.py and temporal.py.

Test file at Mutants caught
e3dc9a3 39 of 46
96fc036 40 of 46
a140125 43 of 46

The 3 that survive read a tool_call or request when it pairs but leave it in the pending map, one in each pairing helper. They return the same indexes as the shipped code, because a tool_call or request they read again was already read when it first paired, so no test can tell them apart.

With 8929bcc's otlp.py, cli.py and temporal.py, the same test reports otlp-genai refusing at events 7, 9, 12, 15, 20, 29 and 32, which it never reads, plain otlp refusing at event 29, and temporal raising at 27 events.

Not changed

These items are out of scope:

  • Wrong types one level down, for example a tool_call whose arguments is a list or whose tool_name is not a string. explain and plain share still print a traceback for that, and share --dry-run still exits 0.
  • The server still accepts non-object data on POST /events. Rejecting it at ingestion is a separate change.

Existing behavior this PR leaves as it is:

  • verify --from-export on a missing file still raises FileNotFoundError. It never reads event data.
  • eval dataset auto without --dataset still raises its existing IsADirectoryError. It does the same on base with a clean store.
  • freeze, regression, compare on non-decision events, compliance-report, org-report and share --assignment keep their existing message and exit code 1.
  • lint and eval gate still print one [lint] Rule '<name>' failed: <error> warning for each affected rule and keep their exit code.
  • The agent-strace mcp tools keep their JSON-RPC -32603 error text.
  • The server routes are not affected.
  • mcp-scan without --session still skips sessions it cannot read without a message.
  • Commands that read several sessions check each session as they reach it, not the whole store first. In the sweep no refused command wrote, sent or opened anything. A command that acts on each session in turn could still act on earlier sessions before it refuses.
  • watch --stream-to with --stream-otlp still drops a batch that holds non-object data without a message, as on base.
  • Compaction checkpoints leave out non-object events of types the checkpoint watcher does not read. Base crashed when it wrote such a checkpoint.
  • The watch MCP poisoning rule still finds nothing while a non-object event is in its 80-event history, as on base, because it catches Exception.

Commands that read keys from event data crashed with a Python traceback when
`agent-strace server` stored array, string, number, or boolean data, or a hand
edit stored null. They now refuse the session the way `share` does since #287:
one `Error:` line on stderr, exit code 1, and nothing printed, written, sent,
or opened.

- Move NonObjectEventDataError and require_object_event_data to event_data.py
  and keep the disclosure import path. The helper can limit the check to the
  event types or positions a command reads, and can name the session.
- Catch NonObjectEventDataError once in the CLI handler wrapper. Telemetry
  records exit code 1 with no error_type, as it does for share.
- Each command checks only the events whose data it reads, so a session keeps
  working with commands that never read the bad event.
- watch skips its checks for a live event with non-object data and prints one
  warning. watch --compaction-checkpoint and mcp-scan --watch refuse at
  startup, and mcp-scan --watch skips live events with a warning.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Siddhant-K-code and others added 4 commits October 25, 2026 15:03
export --format otlp-genai refused a session when a tool_call with
non-object data was paired with an error event, although
session_to_otlp_genai never reads that tool_call's data. Replace
OTLP_GENAI_DATA_EVENT_TYPES with otlp_genai_data_indexes, which pairs
llm_request and tool_call events by ID the way the converter does.

export --format temporal printed a traceback, with -o and with
--endpoint, when the session had a Temporal trace context and
non-object data. session_to_temporal_otlp now runs the same check right
after it validates the context, so a session without one keeps its
"Temporal export failed" message. cmd_export and export_temporal_otlp
re-raise NonObjectEventDataError before their ValueError handlers so
the standard Error line prints.

Add tests/test_non_object_event_data_mirrors.py. It stores non-object
data in one event at a time and runs each command whose check mirrors
its reads twice: as shipped, and with require_object_event_data patched
to a no-op. The shipped run must refuse exactly when the unchecked run
raises. Removing any of five checks that no earlier test covered now
fails it: the mcp-scan --session check on an earlier session_start, the
optimize LLM-path check, the sample --deduplicate tool_call check, and
both postmortem_data_indexes scans. Fix the postmortem docstring that
pointed at tests that did not exist, and document the temporal and
optimize behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
session_to_otlp keeps one pending tool_call per event ID. When a second
tool_call reuses the ID of a tool_call that is still pending, the
converter drops the first one and never reads its data. The check read
every tool_call, so export --format otlp refused such a session, which
main exports with exit code 0. A server client that posts its own
event IDs can store such a session. otlp_data_indexes now pairs
tool_call events by ID the way the converter does.

This is separate from the review fixes so it can be dropped on its own.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
In the export fixture, a tool_call now reuses the ID of the tool_call
that an error paired with earlier. Plain otlp reads that earlier
tool_call when the error pairs with it, so otlp_data_indexes must pair
errors as well as tool_results. Before this, changing
(EventType.TOOL_RESULT, EventType.ERROR) to (EventType.TOOL_RESULT,)
passed the full suite while export --format otlp printed a traceback.

The reused ID alone would hide a missing pop in the otlp-genai error
branch, so the fixture also gets a tool_call that an error pairs with
and whose ID nothing reuses.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Three more single-line changes to the pairing helpers passed the full
suite while changing what the exports do:
- otlp_data_indexes and otlp_genai_data_indexes not pairing a
  tool_result with its tool_call. A later tool_call with the same ID
  then hides the earlier one, which the converter reads, so export
  printed a traceback.
- otlp_genai_data_indexes pairing an llm_response that names its
  request with the oldest open request instead. That refuses sessions
  that otlp-genai exports.

The export fixture now reuses the ID of a tool_call after a tool_result
pairs with it, and pairs an llm_response by parent while an older
request is open and later replaced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Siddhant-K-code
Siddhant-K-code merged commit 4319bd8 into main Oct 25, 2026
5 checks passed
@Siddhant-K-code
Siddhant-K-code deleted the siddhant-k-code-non-object-event-data-tracebacks branch October 25, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant