diff --git a/packages/code-runtime/code-runtime-python/README.i18n.yaml b/packages/code-runtime/code-runtime-python/README.i18n.yaml index 5ae8a26ac3..7605fa5ec6 100644 --- a/packages/code-runtime/code-runtime-python/README.i18n.yaml +++ b/packages/code-runtime/code-runtime-python/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md -README.md: 557ff7f58b70fcbc5cd9d7901c0c6b66d123c0eb -README.zh.md: d98fdcc52a05541c66e050445ebc04264686451b +README.md: c6ab48b5ccd4e386f9ad416dee64b090500594f6 +README.zh.md: 637de914212be9b7e6a3b63e443d7252c092acf2 diff --git a/packages/code-runtime/code-runtime-python/README.md b/packages/code-runtime/code-runtime-python/README.md index 557ff7f58b..c6ab48b5cc 100644 --- a/packages/code-runtime/code-runtime-python/README.md +++ b/packages/code-runtime/code-runtime-python/README.md @@ -113,7 +113,7 @@ These limits define what the package does and does not cover; they are current p - **The cross-language guard covers the executed surfaces and the frame field shapes, not the field types** — the mirror e2e compares required/optional field sets, not that `cpuSeconds` is an `int` on both sides; a type-level drift is caught by review plus the backend's real-subprocess suite. - **A descendant that escapes the child's process group with `setsid()` is not reaped by the group teardown** — `kill(-pid)` cannot reach it; the run still settles on the value the done frame decided, and the close-deadline backstop forces settlement if the orphan holds the pipes open, but the orphan itself outlives the fiber until it exits on its own. - **A `log` frame that arrives after settlement is dropped** — once the run has settled, host-side capture is closed; a late fd-3 `log` frame (from a thread that outlived the done frame) is discarded rather than appended to `logs`. -- **The host-side memory bound for a binding VALUE is the frame parse cap, not a seam budget** — a binding reply's value is rebuilt host-side and billed against `maxValueBytes`; an intermediate binding value has no seam-level byte cap and is bounded by the lossless-JSON serialization cost and process memory (see the binding-argument entry). +- **A binding REPLY value has no seam-level byte or depth cap** — `maxValueBytes` meters only the done frame's completion value; a wide binding reply is rebuilt host-side (`snapshotJsonValue` traversal) and encoded whole, bounded on both sides only by process memory (like a binding argument, which has no child-side budget either). - **`run()` is one-shot** — `logs` become available only after `CodeRunResult` resolves; there is no streaming-log or progress interface for output produced by a running program. - **No state persists across runs** — every request executes in a fresh subprocess; a persistent REPL-style kernel stays deferred until a backend brings its own logging scheme. - **An fd-3 frame whose raw length exceeds 64 MiB settles the run as a worker-exit** — `maxLogBytes`/`maxValueBytes` are load-bounded to the same parser cap so an honest child's frames always fit; a model-constructed binding ARGUMENT above 64 MiB (a value with no seam-level budget) trips the same cap — an accepted residual of the OOM guard. diff --git a/packages/code-runtime/code-runtime-python/README.zh.md b/packages/code-runtime/code-runtime-python/README.zh.md index d98fdcc52a..637de91421 100644 --- a/packages/code-runtime/code-runtime-python/README.zh.md +++ b/packages/code-runtime/code-runtime-python/README.zh.md @@ -113,7 +113,7 @@ kind: "package-reference" - **跨语言 guard 覆盖执行的表面与帧字段形状,而非字段类型**——mirror e2e 比较必填/可选字段集,而非 `cpuSeconds` 在两侧是否都是 `int`;类型级漂移由评审加后端的真实子进程套件捕获。 - **以 `setsid()` 逃出子进程组后代不被组拆卸回收**——`kill(-pid)` 够不到它;运行仍按 done 帧决定的值结算,若该孤儿持有管道,close 截止兜底会强制结算,但孤儿本身在自行退出前一直存活到 fiber 之外。 - **结算后到达的 `log` 帧被丢弃**——运行一旦结算,宿主侧捕获即关闭;迟到的 fd-3 `log` 帧(来自比 done 帧存活更久的线程)会被丢弃,而不是追加到 `logs`。 -- **binding 值的宿主侧内存界是帧解析上限,而非 seam 预算**——binding 回复的值在宿主侧重建并按 `maxValueBytes` 计费;中间 binding 值没有 seam 级字节上限,受无损 JSON 序列化成本与进程内存约束(见 binding 实参条目)。 +- **binding 回复值没有 seam 级字节或深度上限**——`maxValueBytes` 只计量 done 帧的完成值;宽 binding 回复在宿主侧重建(`snapshotJsonValue` 遍历)并整帧编码,两侧都只受进程内存约束(与没有子进程侧预算的 binding 实参一样)。 - **`run()` 是一次性的**——`logs` 只有在 `CodeRunResult` resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。 - **运行之间不保留状态**——每次请求都在全新子进程中执行;持久 REPL 风格内核在某个后端带来自己的日志方案之前保持延期。 - **原始长度超过 64 MiB 的 fd-3 帧会让本次运行以 worker-exit 结算**——`maxLogBytes`/`maxValueBytes` 在加载期被限制到同一解析器上限,因此诚实子进程的帧总能放得下;模型构造的超过 64 MiB 的 binding 实参(一个在 seam 层没有预算的值)会触发同一上限——这是该 OOM 防护的已接受残余。 diff --git a/packages/code-runtime/code-runtime-python/py/bootstrap.py b/packages/code-runtime/code-runtime-python/py/bootstrap.py index 8b8c8a473b..887ebc03c1 100644 --- a/packages/code-runtime/code-runtime-python/py/bootstrap.py +++ b/packages/code-runtime/code-runtime-python/py/bootstrap.py @@ -25,7 +25,15 @@ import signal import sys import threading import traceback -from decimal import Decimal +from decimal import Context, Decimal + +# The float encoder must NOT depend on the process-global decimal context: a +# legitimate program may set `getcontext().prec = 2` (silently rounding the +# completion value's digits) or `traps[Inexact] = True` (making the encode +# raise, misclassifying a successful run as an exception). A fixed context with +# prec=28 (more than the 17 significant digits a double needs) makes the +# normalize() spelling decision context-independent. +_FLOAT_CONTEXT = Context(prec=28) from pathlib import Path from typing import Any @@ -1727,7 +1735,7 @@ def _dump_float(value: float) -> str: if value.is_integer() and value > float(2**53 - 1): # The host's BigInt branch: exact digits, not shortest-round-trip. return str(int(value)) - parts = Decimal(repr(value)).normalize().as_tuple() + parts = Decimal(repr(value)).normalize(context=_FLOAT_CONTEXT).as_tuple() digits = "".join(str(digit) for digit in parts.digits) k = len(digits) n = parts.exponent + k diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index 8d9c345882..9f11fe2808 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -1843,6 +1843,27 @@ describe('PythonCodeRuntime — programs and bindings', () => { expect(result.error?.kind).not.toBe('worker-exit') }, 90_000) + it('keeps a float completion exact when the program mutates the decimal context', async () => { + // The float encoder's Decimal(repr(value)).normalize() used the process + // GLOBAL decimal context: a legitimate program setting + // `getcontext().prec = 2` silently rounded the completion value's digits, + // and `traps[Inexact] = True` made the encode raise, misclassifying a + // successful run as an exception. A fixed module-level Context(prec=28) + // makes the spelling decision context-independent. + const { runtime } = await setup() + const result = await runtime.run({ + program: [ + 'from decimal import getcontext', + 'getcontext().prec = 2', + 'getcontext().traps[__import__("decimal").Inexact] = True', + 'return 1.2345678901234567', + ].join('\n'), + bindings: [], + }) + expect(result.error).toBeUndefined() + expect(result.value).toBe(1.2345678901234567) + }, 15_000) + it('bounds an over-cap exception-group nesting on the copy', async () => { // Exception groups link through `exceptions`, not the cause/context // dunders, so the cap has to count that edge too — otherwise a deeply @@ -2952,7 +2973,6 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => { expect(still).toBe(true) }, 20_000) - it('dispose awaits reaping of a same-group survivor from a completed run', async () => { // The quiescence contract also holds for a run that ALREADY resolved: the run // stays tracked in `live` until its process group is reaped, so a `dispose()`