fix(mcp): bound chart decode, transcript walks, and repeat-delivery spend - #1
Closed
jmesparteroup wants to merge 2 commits into
Conversation
A compact PNG can decode to gigabytes: Pillow's decompression-bomb error only fires above ~179M pixels, so a 690 KB 13000x13000 chart cleared the byte cap and peaked over 2 GB RSS inside the embed thread. Decodes now refuse anything above 25M pixels before load(). Artifact keys ended in the agent's filename, so a turn that rewrote chart.png repointed an earlier turn's still-live presigned URL, and two same-named files in one turn collapsed into one object. The file id is part of the key now. Uploads run in a thread asyncio.timeout cannot cancel; one that started near the delivery deadline completed after the caller gave up and stranded an object with no reader. Uploads no longer start inside the final ten seconds of the budget. File discovery paged at the SDK default of 20 and truncated at 200 candidates before sorting on an ordering the API does not document; it requests full pages and logs when the cap drops candidates. Configured artifact settings with no constructed store also log instead of silently disabling URL delivery.
The transcript walks in ask and deliver_turn_charts were unbounded, and each page costs an ownership retrieve plus an events list, so a long session could outlast any hosted client's tool timeout before delivery even began. Both walks stop after 20 pages. deliver_turn_charts is ungated and unmetered, and every call re-downloaded each chart from the Files API and re-wrote the artifact store, so a client retry loop turned into unbounded operator spend. A delivered result is cached briefly per turn boundary, never longer than its presigned links live. ask raised on any status outside idle/rescheduling/running, but the status value set is upstream-controlled; an unmodeled transient status failed a turn admission had already billed. Only terminated is terminal; anything unknown keeps polling until the deadline. AskResult advertised image_blocks in its output schema while excluding it from every dump, and a chartless ask returned a JSON blob where a charted one returned prose. The field is out of the schema and every result carries the same prose-first content shape.
Author
|
Closing — restacking this onto a base branch in pymc-labs/daimon instead. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacks on pymc-labs#129 (
feat/deliver-turn-charts-upstream) and folds into it on merge; second-pass review follow-ups. No issue closes here — pymc-labs#129 carries theClosesfor pymc-labs#119 (and pymc-labs#124 still needs adding there, per the thread).load().chart.pngrepointed an earlier turn's still-live presigned URL, and two same-named files in one turn collapsed into one object. The file id is now part of the key.asyncio.timeoutcannot cancel; one started near the delivery deadline completed after the caller gave up, stranding an unreferenced object in the bucket. Uploads no longer start inside the final 10 s of the 60 s budget.scan_truncatedwhen the cap drops candidates. Configured artifact settings with no constructed store logstore_unconfiguredinstead of silently disabling URL delivery.askanddeliver_turn_chartswere unbounded, and each page costs an ownership retrieve plus an events list; both walks stop after 20 pages.deliver_turn_chartsis ungated and unmetered, and every call re-downloaded each chart from the Files API and re-wrote the artifact store. A delivered result is cached in-process per turn boundary (at most 5 minutes, never longer than its presigned links live), so client retry loops stop multiplying operator-side spend.askraised on any status outsideidle/rescheduling/running, but the status value set is upstream-controlled. Onlyterminatedis terminal; an unmodeled transient status keeps polling until the deadline instead of failing a turn admission already billed.AskResultadvertisedimage_blocksin its output schema while excluding it from every dump. The field is out of the schema, and chartless and charted replies share one content shape: prose text first, image blocks after, structured payload throughout.Behaviour changes
ask/deliver_turn_chartsreply is a prose-firstToolResult; a chartlessaskpreviously returned the serialized model as structured JSON only.deliver_turn_chartsfor the same completed turn within the cache window returns the earlier result without re-contacting the Files API or the bucket.no completed turn boundaryerror instead of walking the full transcript.Testing
uv run pytest: 4670 passed, 6 skipped; the two notebook-host self-chown failures are the known macOSPermissionErrors, byte-identical to the PR base.uv run pyright— 0 errors.uv run ruff check .andruff format --check .clean.uv run lint-imports— 6 contracts kept.AskResultoutput-schema change has direct assertions instead).Checklist
uv run pytestpasses locally (two pre-existing macOS notebook-host failures, identical on base)uv run pyrightpasses locally (strict mode)uv run ruff check .is cleanuv run lint-importspasses (package boundary contracts)