Skip to content

fix(parser): recover mid-turn operator messages and stop self-inflicted parse noise - #47

Draft
m-szymanska wants to merge 4 commits into
mainfrom
fix/midturn-queue-user-turns
Draft

fix(parser): recover mid-turn operator messages and stop self-inflicted parse noise#47
m-szymanska wants to merge 4 commits into
mainfrom
fix/midturn-queue-user-turns

Conversation

@m-szymanska

Copy link
Copy Markdown
Contributor

What

Three same-class classification fixes in the Claude JSONL adapter, found by a full JSONL-vs-extract audit (208 utterances, live session):

  1. Mid-turn operator messages are recovered (2ae831b). Messages the operator sends while the agent is working exist in the JSONL only as queue-operation records (text in .content, older builds .prompt) and are delivered inside tool_result entries — they never become standalone user rows, so extract --conversation silently dropped them. The audit proved this is the extractor's only systematic loss class (assistant losses: 0, standalone-user losses: 0), and it skews toward the most valuable words: decisions and course corrections typed during dense tool activity. Enqueues are now projected as regular UserMsg turns (consumption stays metadata_record; frozen taxonomy untouched), with a two-layer dedupe: re-enqueued text keeps the first occurrence, and a message later delivered as a real user row wins over its queue synthetic.
  2. file-history-delta is recognized as metadata (5d888ab). The harness replaced file-history-snapshot with delta records (rewind/backup bookkeeping — zero conversational content). Unrecognized, each one counted as skipped(unknown_payload_type) + warning.
  3. Signature-only empty thinking blocks are consumed silently (a6dc10f). The harness stores thinking blocks with an empty body and only a signature (400/400 blocks in the probe session). Each one raised UnknownPayloadType and latched unsupported_visible_event, so every modern reasoning session degraded its own parse status — the flag was dead as a signal. Empty body is now consumed like empty text; a missing/non-string body still raises the flag (that narrowing is deliberate and tested).

9750b3b records the projection in PARSER_NORMATIVE_CONTRACT.md (§2.1 "Consumption kind is not a licence to drop the body").

Measured on a live 5 MB session JSONL

Metric Before After
Operator mid-turn messages in extract lost 20 recovered (21 candidates, 1 correctly deduped against its real delivery)
skipped(unknown_payload_type) records 48 0
UnknownPayloadType warnings 448 0
unsupported_visible_event true (permanently) false
Coverage counters / turn count (fixes 2-3) unchanged (classification-only)

Test plan

  • cargo test -p aicx-parser — 0 failed (22 tests in claude_adapter, incl. 7 new for the queue projection + dedupe, 3 for empty thinking, 1 for file-history-delta)
  • taxonomy oracle: raw_unit_taxonomy.toml match rules unchanged for queue-operation; file-history-delta added with a fixture unit so the new kind is actually exercised by the gate
  • e2e in src/extraction/tests.rs: fixture JSONL → to_conversation carries the mid-turn message
  • full make test, make fmt-check, make clippy (-D warnings), pre-push gate (clippy + test + semgrep) — all green
  • acceptance on the live session: all 4 audit-flagged permanently-lost messages present with correct timestamps

Notes for review

  • Branch is cherry-picked onto main; the same commits also exist stacked on feat/live-window-hot-path as feat/midturn-queue-user-turns (local) — provenance, not a competing line.
  • Engine validation intentionally required no changes: validate_evidence_reference checks evidence ids against coverage membership only, so a turn may reference a metadata_record-consumed unit as long as the evidence_event_id matches the consumption exactly.

🤖 Generated with Claude Code

vetcoders-agents added 4 commits July 31, 2026 07:39
…user turns

A message the operator submits while a turn is running exists in a Claude
session JSONL only as a `queue-operation` enqueue record. The harness writes
a `type:"user"` row only for what it delivers between turns, so a mid-turn
submission that is interrupted or superseded never becomes one. The adapter
consumed the record as `metadata_record` and discarded its body, making that
text unrecoverable for every downstream consumer.

The record stays consumed as `metadata_record` — the frozen taxonomy and the
oracle fixture do not move — and its `enqueue` text is now additionally
projected as an ordinary `TurnKind::UserMsg` turn, timestamped from the record
and referencing the record's own evidence. No schema change: markdown renders
the standard `**[HH:MM:SS] user:**` header.

Two dedupe layers, both observed on live sessions, keep the projection from
doubling a message: re-submitting the same text keeps the first enqueue, and a
queued message the harness later delivers as a real `user` row loses to that
row. Dropping a projected turn renumbers turns, tool events, skill invocations
and segment bounds so the model stays internally consistent.

Empty bodies and `<task-notification` heads are harness chatter, not operator
input; `remove`/`dequeue`/`popAll` repeat text `enqueue` already carried.

Authored-By: claude <agents@vetcoders.io>
session_id: 77601750-3f2c-4edb-8e99-07c3a4011a56
timestamp: 2026_0731_0024_CEST
runtime: claude-code
…ion contract

Contract §2.1 states the rule the adapter fix implements: a unit's consumption
kind decides its accounting, never whether its body may be discarded. Where a
recognized metadata record is the only carrier of literal operator input, the
adapter must project that text — spelled out for Claude `queue-operation`,
with the loss class (mid-turn submissions), the dedupe layers, and the reason
the loss is invisible to coverage.

Adds the end-to-end guard for the same path: a fixture session whose only
carrier of a mid-turn message is an enqueue record must surface that message
through `to_conversation`, at the moment it was submitted.

Authored-By: claude <agents@vetcoders.io>
session_id: 77601750-3f2c-4edb-8e99-07c3a4011a56
timestamp: 2026_0731_0026_CEST
runtime: claude-code
…adata_record

The harness moved Claude file-history tracking from whole snapshots to
per-message `file-history-delta` records, which the frozen taxonomy never
declared. Each one terminated as `skipped(unknown_payload_type)` with a typed
warning and raised `unsupported_visible_event`, so a perfectly healthy modern
session was flagged as carrying unsupported visible events purely from its own
bookkeeping.

The record was verified against live sources to be rewind/backup bookkeeping
only — a backup descriptor (`backupFileName`, `version`, `backupTime`,
`realParentDir`), the message ids it belongs to, and a tracking path. No
conversation content, so it carries no visible event and belongs with its
older sibling `file-history-snapshot` as `metadata_record`.

This is a deliberate edit of the frozen taxonomy: the declared list, its
description, the contract's Claude physical-unit row, and a fixture unit all
move together, and the contract gate stays green (42 kinds).

Measured on a live 3995-unit session: 48 records moved from
skipped(unknown_payload_type) to consumed(metadata_record), skipped went 48 ->
0, and unknown-payload warnings dropped 448 -> 400. The residual 400 come from
an unrelated pre-existing path (assistant `thinking` blocks the harness emits
with an empty body) and are untouched here.

Authored-By: claude <agents@vetcoders.io>
session_id: 77601750-3f2c-4edb-8e99-07c3a4011a56
timestamp: 2026_0731_0104_CEST
runtime: claude-code
…ng blocks silently

Since 2026-07 the Claude harness emits thinking blocks signature-only: the
block carries `thinking: ""` plus a signature, and the reasoning text never
reaches the JSONL at all. Live evidence on a 3995-unit session: 400 of 400
thinking blocks were empty.

The adapter read an empty body through `string_field`, which cannot tell an
empty string from a missing key, so every such block fell to the unrecognized
shape path — a typed `unknown_payload_type` warning plus
`unsupported_visible_event`. The result was that any session which reasoned at
all reported itself as carrying unsupported visible events, draining the flag
of its meaning exactly where it was supposed to be a signal.

A known block with no body is now consumed silently, the same semantics an
empty text block already had in `emit_text_turn`. This is a silent-consume of
a known-empty block, not a new kind and not a second redacted_thinking path:
accounting is untouched (still `consumed(thinking_block)`), only the turn
projection is skipped. A block with a body is unchanged, and a block missing
the field entirely stays an unsupported shape — the fix distinguishes the two
cases `string_field` had conflated.

Measured on the same live session: unknown-payload warnings 400 -> 0 and
`unsupported_visible_event` true -> false, with consumed/skipped/turn counts
unchanged (3995/0/1180).

Authored-By: claude <agents@vetcoders.io>
session_id: 77601750-3f2c-4edb-8e99-07c3a4011a56
timestamp: 2026_0731_0148_CEST
runtime: claude-code
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

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