fix(queue-state): parseable non-dict JSON no longer wedges scans and drains - #114241
beardthelion wants to merge 1 commit into
Conversation
|
Independent review — this fully addresses #114240, and the coverage is complete. I mapped every source site in the issue's audit to a guard in the diff:
All 13 files carry a matching test (the One thing worth a maintainer's eye rather than a change request: the guards use two deliberately different shapes — skip-and-preserve for scans vs. raise LGTM — resolves the P1 drain-wedge and the sibling class in one pass. |
Request changes conclusionGitHub does not permit this account to submit a formal change-request review, so this comment records the same blocking verdict. MotivationThe reported P1 bug is valid. A parseable JSON scalar can reach object-only queue and state readers, then stop a whole scan before healthy sibling records are processed. The primary deferred Bot Chat reproduction is fixed at exact head ApproachThe patch applies the correct general distinction in most places. Bulk scans skip malformed record shapes. Exact-ID reads fail closed. The batch and JSONL paths reject or pass through non-object entries according to their existing contracts. Changes reviewedI reviewed all 24 changed files. The cron queue, scheduler delivery, live-owner mailbox, Bot DM, relay outbox, local runtime manifest, A2A log, batch runner, trajectory compressor, write-approval store, Lightpanda state, shutdown recovery, and spawn-tree snapshot changes are individually small. The exact-ID guards preserve the no-overwrite rule. The relay claim keeps malformed input out of delivery after the atomic rename. The batch combine counter remains accurate. Risk to mainTwo blocking defects remain.
The branch also conflicts with current The patch adds 18 tests. Repository guidance limits one fix to one or two invariant tests. Please consolidate the cases around the scan-skip and exact-ID fail-closed contracts after the two correctness gaps are fixed. Overall assessmentRequest changes. The core fix is valid and most changed paths follow the intended policy, but the indexed spawn-tree path remains vulnerable and the transcript path can delete unrelated recovery evidence. Local validation at the exact head: 304 focused tests passed. Ruff and English verdict: REQUEST_CHANGES at |
|
Correcting my earlier "fully-fixes" verdict. @ehz0ah's two blocking points are real — I re-checked both against 1. Indexed spawn-tree path is unguarded. The PR guards 2. if not isinstance(payload, dict):
logger.warning("Removing structurally invalid transcript spool file %s", path)
path.unlink(missing_ok=True)
continue
if (payload.get("reason") != TRANSCRIPT_CAP_DROP_REASON
or payload.get("session_key") != session_id):
continueSo it unlinks any non-dict Also noting the branch now conflicts with The core scan-skip / exact-id-fail-closed design is right and most of the 24 sites are correct; these two gaps are the blockers. Apologies for the premature LGTM — @ehz0ah's read is the accurate one. |
… crashes the remaining file scans Same class as the Bot Chat drain wedge already on this branch: every JSON-file scan guarded "did it parse?" and then assumed the value was a dict. A file holding `42`, `"oops"` or `[1,2,3]` (corruption, truncated write, foreign tool) passed the guard and raised AttributeError/TypeError at the first `.get()`, usually before a single healthy sibling was processed. Each site now treats a non-object payload like a corrupt file under that subsystem's existing policy: - tools/bot_relay.py::_expire_if_stale / claim_pending_envelopes — the envelope is skipped by the sweep and not claimed (same as unparseable). - tools/browser_lightpanda.py::reap_orphaned_lightpanda — record unlinked, scan continues. - tools/write_approval.py::list_pending / get_pending — record skipped with the existing "unreadable pending record" warning / None. - tui_gateway/methods_session.py::_legacy_spawn_tree_entry / spawn_tree.load — scalar snapshot reads as empty / returns the existing 5000 error instead of violating the SpawnTreeLoadResult contract. - hermes_cli/local_runtime/binaries.py::manifest_verified — False. - plugins/platforms/a2a/protocol.py::load_conversation — non-dict lines are dropped, keeping the declared list[dict] return. - batch_runner.py::_load_dataset / _scan_completed_prompts_by_content / _combine_batch_files — line skipped and counted as filtered. - trajectory_compressor.py::process_entry_async — scalar entry passed through unchanged. Ported from the source hunks of PR #114241; its gateway/shutdown_flush.py drain_transcript_spool hunk is left to open PR #84785, and its recover_pending_to_db / cron / bot_live_delivery / bot_mode_dm hunks are already on this branch or on main. (cherry picked from commit d4b5456)
…s skipped by the sibling scans Per-subsystem invariants for the eight ported sites (relay outbox claim, lightpanda reaper, write-approval pending store, spawn_tree list/load, local-runtime manifest, A2A conversation replay, batch runner scans, trajectory compressor entry), trimmed from PR #114241's test hunks to at most two per subsystem (batch_runner's dataset-load and resume-scan cases collapsed into one). Each was red on the previous head of this branch. (cherry picked from commit d4b5456)
… deferred drain A receipt file that parses as JSON but is not an object (`42`, `"oops"`, `[1]` — corruption, a truncated write, a foreign writer) slipped past the "bad JSON" guard in `cron/bot_chat_delivery.py::_records` and raised TypeError in the `sorted(..., key=item[1]["sequence"])` of `_drain` and in `defer`'s sequence allocation, before a single healthy sibling was delivered — on every scheduler tick, until someone deleted the file. `_records` now rejects a non-dict payload on the same warn-once-and-preserve path as unparseable JSON (the file's own rule: one bad file must not wedge the dir), and the `_drain` re-read under the lock skips a record that turned non-dict between the scan and the claim. Ported from PR #114241 (hunks for cron/bot_chat_delivery.py::_records and ::_drain; the exact-id read hunks are superseded by the follow-up commit that rejects non-dict payloads once at the readers).
… crashes the remaining file scans Same class as the Bot Chat drain wedge already on this branch: every JSON-file scan guarded "did it parse?" and then assumed the value was a dict. A file holding `42`, `"oops"` or `[1,2,3]` (corruption, truncated write, foreign tool) passed the guard and raised AttributeError/TypeError at the first `.get()`, usually before a single healthy sibling was processed. Each site now treats a non-object payload like a corrupt file under that subsystem's existing policy: - tools/bot_relay.py::_expire_if_stale / claim_pending_envelopes — the envelope is skipped by the sweep and not claimed (same as unparseable). - tools/browser_lightpanda.py::reap_orphaned_lightpanda — record unlinked, scan continues. - tools/write_approval.py::list_pending / get_pending — record skipped with the existing "unreadable pending record" warning / None. - tui_gateway/methods_session.py::_legacy_spawn_tree_entry / spawn_tree.load — scalar snapshot reads as empty / returns the existing 5000 error instead of violating the SpawnTreeLoadResult contract. - hermes_cli/local_runtime/binaries.py::manifest_verified — False. - plugins/platforms/a2a/protocol.py::load_conversation — non-dict lines are dropped, keeping the declared list[dict] return. - batch_runner.py::_load_dataset / _scan_completed_prompts_by_content / _combine_batch_files — line skipped and counted as filtered. - trajectory_compressor.py::process_entry_async — scalar entry passed through unchanged. Ported from the source hunks of PR #114241; its gateway/shutdown_flush.py drain_transcript_spool hunk is left to open PR #84785, and its recover_pending_to_db / cron / bot_live_delivery / bot_mode_dm hunks are already on this branch or on main. (cherry picked from commit d4b5456)
…s skipped by the sibling scans Per-subsystem invariants for the eight ported sites (relay outbox claim, lightpanda reaper, write-approval pending store, spawn_tree list/load, local-runtime manifest, A2A conversation replay, batch runner scans, trajectory compressor entry), trimmed from PR #114241's test hunks to at most two per subsystem (batch_runner's dataset-load and resume-scan cases collapsed into one). Each was red on the previous head of this branch. (cherry picked from commit d4b5456)
…drains A JSON file that parses but is not an object (a scalar, string, or list from corruption or a foreign writer) slipped past every scan's "bad JSON" guard and crashed the sweep at the first subscript: - cron/bot_chat_pending: one such receipt wedged the deferred Bot Chat drain on EVERY tick — sorted() raised TypeError on record["sequence"] before any sibling was delivered, and defer() could not allocate a sequence either; violates the file's own "one bad file must not wedge the dir" rule. - Exact-id reads in bot_chat_delivery, scheduler_delivery, bot_live_delivery, and bot_mode_dm crashed with TypeError instead of the established "different payload" ValueError; they now fail closed and never overwrite a malformed receipt. - The same shape wedged shutdown-flush recovery (no per-file guard at all — even unparseable JSON aborted the pass), the transcript spool drain, bot_relay outbox claim + stale sweep, the lightpanda reaper, write-approval listing, spawn_tree.list/load (a scalar snapshot would violate the declared RPC result contract), manifest_verified, a2a load_conversation, batch_runner dataset/resume/combine scans, and trajectory_compressor pass-through. Each site now rejects non-dict payloads under its own existing contract: warn-and-preserve evidence for receipt dirs, quarantine on claim for the relay outbox, delete under the existing cleanup policy for reaper state, fail closed for exact-id reads. Malformed lines in JSONL scans are skipped with honest filtered_entries bookkeeping. Regression tests cover every site, including an end-to-end drain_in_background run that delivers the healthy sibling with a non-dict receipt in the dir. Related: NousResearch#87661 covers the recovery-processing boundary of one of these sites (recover_pending_to_db) with a broader except; NousResearch#84785 carries an equivalent check inside its spool-ordering rewrite. The other sites are uncovered.
d4b5456 to
27e21bb
Compare
|
Thanks @beardthelion. Your change was salvaged into #114759 with your authorship preserved (cherry-picked); #114759 — fix(cron): a parseable non-dict receipt no longer wedges the deferred Bot Chat drain or live-delivery mailbox (#114240, salvage #114241) — is now merged on |
Summary
Fixes #114240.
Every JSON-file scan guards
except (OSError, ValueError)— "did it parse?" — but treats parses as is a record. A parseable non-object value (42,"oops",[1,2,3]— corruption, truncated write, foreign tool) passes the guard and crashes the sweep at the first.get()/[...], usually before any healthy sibling runs.Worst site:
cron/bot_chat_pending— one such receipt wedged the deferred Bot Chat drain on every tick (sorted()→TypeErroronrecord["sequence"]), anddefer()couldn't allocate a sequence either. Violates the file's documented invariant "one bad file must not wedge the dir" (and itsbot_live_deliveryprecedent, #109820).Reproduced live; 15 sites audited — all confirmed against their own contracts:
_records,_scan_read,recover_pending_to_db,drain_transcript_spool,claim_pending_envelopes,_expire_if_stale,reap_orphaned_lightpanda,list_pending,_legacy_spawn_tree_entry,manifest_verified,load_conversation,batch_runnerdataset/resume/combine,process_entry_async): non-dict rejected inside the existing parse guard → identical skip path as unreadable files.defer,scheduler_deliverypending/receipt checks,deliver_to_live_owner,complete_delivery,_admit_live_dm,_wait_live_dm,get_pending,spawn_tree.load): fail closed with the establisheddifferent payloadValueError / RPC error /None— never overwrite or reinterpret a malformed file.claimed/, delete under existing cleanup policy for reaper state, honestfiltered_entriesbookkeeping inbatch_runner.Overlap / related work
recover_pending_to_db's processing boundary (broadexceptaround_recover_one_payload); mine covers the parse boundary. Complementary scopes; happy to rebase whichever lands second.isinstanceinside its spool-ordering rewrite; small overlap on one site.Test plan
tests/tui_gateway/test_spawn_tree_records.py— every site asserts non-dict no longer crashes and healthy siblings still processdrain_in_background(real scheduler entry →drain→_drain→_records) delivers the healthy deferred message with a[1,2,3]receipt in the dir; bad file preservedopenai/psutil/firenot installed; a pre-existingtest_batch_runner_checkpoint→ a2a cross-file pollution reproduced on clean HEAD)