Skip to content

fix(sleep): skip assistant.message events with non-dict data - #232

Merged
Yifan Yang (Yif-Yang) merged 1 commit into
microsoft:mainfrom
pravit-amp:fix/sleep-copilot-jsonl-nondict-data
Aug 20, 2026
Merged

fix(sleep): skip assistant.message events with non-dict data#232
Yifan Yang (Yif-Yang) merged 1 commit into
microsoft:mainfrom
pravit-amp:fix/sleep-copilot-jsonl-nondict-data

Conversation

@pravit-amp

Copy link
Copy Markdown
Contributor

Closes #231

Problem

CopilotCliBackend._parse_jsonl_response treated the data field of an assistant.message event as an object:

content = (obj.get("data") or {}).get("content")

A truthy non-dict value ("text", 5, a non-empty list) raises AttributeError on the field access. That happens outside the per-line try, which only wraps json.loads, so a single malformed line aborts the parse of the whole stream and the backend returns nothing for that call. data: [] slipped through only because [] or {} falls back to {}.

Fix

Port the guard that skillopt/model/copilot_backend.py::parse_copilot_jsonl already applies to the same event format:

data = obj.get("data")
if not isinstance(data, dict):
    continue
content = data.get("content")

That guard landed in 5497a31 ("harden and deduplicate JSONL parsing"), which removed this exact line from copilot_backend.py and codex_harness.py and folded all three copies into one helper. skillopt_sleep/backend.py predates it and was left behind, since the sleep package deliberately keeps zero dependency on the research package.

I kept the vendored copy rather than importing the shared helper, to preserve that decoupling. The docstring now records the relationship and asks for the two to be kept in sync, matching how skillopt_sleep/gate.py documents its own vendoring of the validation gate.

One intentional difference from the research-package copy: the wider except (ValueError, RecursionError, TypeError) stays. json.loads raises RecursionError, not JSONDecodeError, on a deeply nested payload, so narrowing it to match would reintroduce a crash the sleep copy already handles. The docstring calls this out so it does not read as accidental drift.

Tests

Added two focused cases to TestCopilotBackend:

  • test_parse_jsonl_skips_non_dict_data_without_losing_stream places a bad data value between two good messages and asserts "first\nsecond" still comes through, so a malformed event costs only its own line. Runs as subtests over "text", 5, [1,2] and true. This mirrors the existing coverage in tests/test_copilot_exec_backend.py for the research-package parser.
  • test_parse_jsonl_ignores_non_object_top_level covers non-object top-level lines.

This also fixes the pre-existing failure in test_parse_jsonl_ignores_excessively_nested_json. That test builds a 2000 deep nested array expecting RecursionError from json.loads, but on Python 3.14 it parses fine, reaches the field access and hits the AttributeError instead.

Verification

  • Focused: 154 passed, 1 skipped across tests/test_sleep_engine.py and tests/test_copilot_exec_backend.py.
  • Full suite: 1097 passed, 10 skipped, 2 failed, down from 3 failures on main.
  • ruff check on both changed files reports 17 errors before and after, none on the touched lines. They are pre-existing (import tempfile, shutil, stat, trailing whitespace in tests, an unused logging import) and left alone to keep the diff reviewable.

No documentation changes, so mkdocs build --strict was not run.

Environment

macOS 15, Python 3.14.6, branched from main at 9c776fc

CopilotCliBackend._parse_jsonl_response assumed the data field of an
assistant.message event was an object, so a truthy non-dict value raised
AttributeError from the field access. That escaped the per-line try, which
only wraps json.loads, and killed the parse of the entire stream.

Port the isinstance guard already used by parse_copilot_jsonl in
skillopt/model/copilot_backend.py, which was hardened in 5497a31 but did not
reach this vendored copy. The wider except clause is kept, since json.loads
raises RecursionError rather than JSONDecodeError on deeply nested payloads.

Fixes the pre-existing failure in
tests/test_sleep_engine.py::TestCopilotBackend::test_parse_jsonl_ignores_excessively_nested_json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pravit-amp

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@pravit-amp

Copy link
Copy Markdown
Contributor Author

pravit-amp Reviewed and verified this one — it's good to go, and the CLA is the only thing holding it up.

I reproduced the crash against current main: an assistant.message event whose data is a truthy non-dict (e.g. a bare string) raises AttributeError out of _parse_jsonl_response, and no caller catches it, so one malformed line takes down the whole response instead of costing just that line. Your fix is consistent with the defensive type-checking the sibling parser in skillopt_sleep/harvest_copilot.py already applies (:95, :249, :278), so it's the house style rather than a new pattern. Full suite on your branch: 1099 passed, 10 skipped.

Could you post the CLA line below so we can merge? It's a one-liner in a comment on this PR:

@microsoft-github-policy-service agree

(or agree [company="..."] if you're contributing on behalf of an employer.)

Just commented. Thanks for the review!

@Yif-Yang
Yifan Yang (Yif-Yang) merged commit a33e56d into microsoft:main Aug 20, 2026
1 check passed
@pravit-amp
pravit-amp deleted the fix/sleep-copilot-jsonl-nondict-data branch August 20, 2026 20:18
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.

Copilot backend in skillopt_sleep crashes when an assistant.message event has non-dict data

2 participants