Repository navigation
fix(sdk-python): fix 4 bugs: early loop exit leaks the CLI, backstop timeout reported as cancel, bare KeyError, unpicklable errors - #585
Merged
Conversation
…pickling bugs
- Leaving `async for` over a StreamedTurn early (break or an exception in the
loop body) now stops the CLI and releases the Thread. StreamedTurn.__aiter__
returned the inner async generator, which Python never closes on break
(PEP 525), so the CLI kept running and later Turns raised ThreadBusyError.
The next Turn waits for the abandoned one to shut down.
- When the timeout_ms backstop kills a hung CLI, raise MCodeRunError with
status="timeout" instead of MCodeAbortError ("cancelled").
- A result without durationMs raises MCodeProtocolError, and a failed result
whose error lacks message/category raises MCodeRunError, instead of KeyError.
- MCodeError subclasses survive pickling (MCodeRunError could not be
unpickled; ThreadBusyError repeated its message).
- repr(MCodeOptions) no longer prints env values.
- Docs: identical item.updated snapshots are skipped, and the blocking API
does not work where an event loop is already running (e.g. Jupyter).
Merged
3 of 4 tasks
1anZhang
approved these changes
Nov 4, 2026
3 of 4 tasks
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.
Change
Fixes four bugs in the Python SDK (
minimax-code-sdk, import nameminimax_code, packagepackages/code-sdk-python) that are present in the published0.1.0b1and onmain, plus three related documentation/safety issues. All changes are confined topackages/code-sdk-python. The package version is not bumped; the fixes are recorded under a new## UnreleasedCHANGELOG section (the release check still reads the first dated heading).BUG-1 (medium-high): leaving
async forearly did not stop the CLI and left the Thread busyStreamedTurn.__aiter__returned the inner async generator. Python never callsaclose()on an async generator when the loop is left withbreakor an exception in the loop body (PEP 525), andstreamed.eventskept it alive, so the CLI kept running and every later Turn on the same Thread raisedThreadBusyError. That contradicts the README, the CHANGELOG and theStreamedTurndocstring.Fix:
__aiter__now returns a small_TurnIterator. When it is dropped without being exhausted (break / exception in the body), it starts closing the Turn in the background (SIGINT, then SIGKILL after the grace period).StreamedTurn.aclose()now goes through one shared close task, so the background close and an explicitaclose()/async withexit never race on the generator.Thread._startwaits for an in-progress close of the previous Turn before deciding the Thread is busy, sobreakfollowed immediately byawait thread.run(...)works.thread.run(),async with streamed, explicitaclose()andSyncThread.run_streamedkeep their behavior.BUG-2 (medium): the
timeout_msbackstop was reported as a cancellationWhen the CLI ignored its own timeout and the SDK backstop (
timeout_ms + 30 s) killed it with SIGKILL, the negative exit code was mapped toMCodeAbortError("The MCode run was cancelled."), so callers could not tell a timeout from a cancel.Fix: the backstop now records that it fired (
_Exec.kill_for_timeout), anderror_for_exit(..., timed_out=True)raisesMCodeRunError(status="timeout")with the message "The MCode run exceeded timeout_ms and was force-stopped by the SDK.", matching the TypeScript SDK (exec-process.ts,backstopFired).BUG-3 (low): incomplete results raised a bare
KeyErrorAn
exec.completedresult withoutdurationMsraisedKeyError: 'durationMs'inThread.run, and a failed result whoseerrorlackedmessageraisedKeyError: 'message'inerror_for_result. Neither is caught byexcept MCodeError, although the README says all errors subclassMCodeError.Fix: a successful result without an integer
durationMsraisesMCodeProtocolError;error_for_resultreadsmessage/categorydefensively and still raisesMCodeRunError(falling back to "status ").BUG-4 (low): SDK errors did not survive pickling
MCodeRunErrorcould not be unpickled (TypeError: missing keyword-only argument 'status') and an unpickledThreadBusyErrorrepeated its message, becauseException.__reduce__rebuilds withcls(*self.args). This breaksmultiprocessing,ProcessPoolExecutorand Celery-style workers.Fix:
MCodeError.__reduce__rebuilds viacls.__new__(cls, *args)and restores the instance attributes, so every subclass round-trips with its type, message and fields (status,category,exit_code,stderr, ...).Related items
repr(MCodeOptions(...))printed the wholeenvdict, soenv=dict(os.environ)could put API keys into logs.MCodeOptions.__repr__now shows only the variable names with every value replaced by***. Masking all values instead of guessing which names look secret avoids missed keys.item.updatedsnapshot identical to the previous snapshot of the same item is skipped (intentional, same as the TypeScript SDK'sitems.ts). The README and CHANGELOG now describe this; behavior is unchanged.SyncMCodeworks in notebooks, but inside a running event loop (Jupyter) it raisesRuntimeError: Cannot run the event loop while another loop is running. The CHANGELOG, README andsync.pydocstring now say the blocking API cannot be used where a loop is already running and point to the async API. The SDK does not pretend to support it.The README "Cancellation" section also documents that the next Turn waits for an abandoned Turn to shut down, and that the backstop raises
MCodeRunError(status="timeout").Validation
mainat 6e062ae. Environment: macOS arm64, project-localuvvirtual environments.cd packages/code-sdk-python && uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest -W error: all checks pass, mypy strict clean, 44 passed, 3 skipped (the 3 skips are the opt-in e2e tests). Same result on Python 3.10.20 and 3.13.tests/test_sdk.py(with newhang,no_durationanderror_no_messagescenarios intests/fake_mcode.py):test_break_without_aclose_stops_cli_and_frees_thread,test_exception_in_loop_body_stops_cli_and_frees_thread,test_timeout_backstop_raises_timeout_error,test_incomplete_results_raise_sdk_errors(x2),test_errors_survive_pickling,test_options_repr_hides_env_values. Withsrc/reverted tomain, all 7 fail; with this change, all pass.mcode, no credentials), run withPYTHONPATH=packages/code-sdk-python/src:CLI child still alive 2 s after break: True, thenThreadBusyErroron the next run. After,alive: False; a run started immediately afterbreakstarts a new CLI instead of raisingThreadBusyError; the exception-in-body case behaves the same.MCodeAbortError | The MCode run was cancelled. | exit_code -9. After,MCodeRunError | The MCode run exceeded timeout_ms and was force-stopped by the SDK. | exit_code -9 | status: timeout.KeyError: 'durationMs'/KeyError: 'message'. After,MCodeProtocolError/MCodeRunError.TypeErrorforMCodeRunErrorand a doubledThreadBusyErrormessage. After, both round-trip with type, message and attributes intact.0.1.0b1it had 6 failures, all mapped to BUG-1..4).node scripts/source-inventory.mjs: public source check passed. No files were added, removed or renamed.uv run python scripts/release_check.py metadata: version metadata is consistent (0.1.0b1).mcodeCLI and model (-m e2e); Windows (the SDK usesterminate()instead of SIGINT there); Linux was not run locally, but the Python SDK CI matrix covers ubuntu and macOS. The full monorepopnpm verifywas not run because no TypeScript or release-contract files change. All results above are offline, fake-CLI results, not live-service acceptance.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable. (No files added or removed; Python tests are collected by pytest, not Vitest.)Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: pending. The Python SDK fixes should be ported back to the shared source. A follow-up
0.1.0b2release is needed for PyPI users to get these fixes.Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.