docs(code-runtime-python): split the deadlock into its own bullet and qualify the done_value claim

Addresses the review's two registration-text accuracy findings:
- The cross-thread t.join() deadlock is a process-isolation-backend property (the
  pump runs on the child's main event loop), so it is split out of the wide-binding
  REPLY bullet into its own Known Limitations entry with the correct attribution
  (fix belongs in this backend, not packages/core/session); the zh half-width
  space is removed.
- The settlement note's _done_with_value def-time default-arg sentence is
  qualified: it guards a rebind of _check_done_value/_encode_json_plain, while a
  transitive encoder dep (_dump_scalar/io) rebind can still downgrade, which is
  registered as an accepted residual in the package README.
Pairing re-recorded; corpus-wide verify-translation-pairing passes 1004.
This commit is contained in:
Chinesezjc
2026-08-31 14:39:07 +08:00
committed by Tianyi Cui
parent c8bc96007b
commit e6b23e829b
6 changed files with 10 additions and 8 deletions
@@ -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: 180b0eec9bd5644bfb37086b9eb35cbe93d51672
README.zh.md: 8a1468f56c62cc1b8544a05c4420270aa26dba31
README.md: f976b050992863cc8586b5415d9c36cfe193229e
README.zh.md: 608ac28ecab35f90d9a9d6582e15a24fa4e78c10
@@ -41,4 +41,5 @@ No direct invalidation; the named consumer owns any request-prefix changes.
- **A 1-second dual-limit `ulimit -t 1` CPU overrun is reported as `worker-exit`, not a timeout.** When the host starts under a hard CPU limit equal to the soft (`ulimit -t N` sets both) and that limit is 1, `_clamped` cannot lower the soft to 0, so the kernel SIGKILLs the busy loop in the same tick and SIGXCPU is never delivered. The host classifies a CPU overrun only on `signal === 'SIGXCPU'`, so the overrun is reported as `worker-exit`. For a dual limit of 2 or more the soft is lowered by one unit, SIGXCPU fires, and the run is a timeout. Containment holds in both cases; only the classification is degraded.
- **A program that traps SIGXCPU can exceed the soft CPU limit during settlement encoding and still report success.** The settlement CPU recheck runs before the completion value is flushed and encoded; a program that traps SIGXCPU (soft limit) and keeps burning past it through the build-and-encode window returns a result before `die_if_cpu_exhausted` re-checks, so the run reports success. Containment holds — the hard limit (soft + 1s) and the wall clock still bound it — and only the classification is degraded. The CPU recheck does not run mid-encode because doing so would have to meter the encode itself, and the encode is the path the budget already bounds.
- **The encoder's direct dependencies resolve at call time.** `_encode_json_plain` reaches `_dump_scalar`/`_dump_string`/`json` via module-global lookup, so a program running as `__main__` that rebinds one of those names (e.g. `__main__._dump_scalar = boom`) after returning a legitimate value can make the encode throw and downgrade a success to `exception`. The value path's top-level `_check_done_value`/`_encode_json_plain` are bound as def-time defaults, but their transitive deps are not; this is an accepted residual for the same reason the analogous `_dump_*` helpers are not rebound in practice.
- **A wide binding REPLY expands host-side state per member.** Resolutions cross through `snapshotJsonValue` in [`@deepseek-ai/dsh-session`](../../core/session/README.md), whose `walkJsonValue` pushes one task frame per member, and binding resolution carries no seam-level byte cap. A legitimate reply of several million elements can therefore exhaust the host heap. A cross-thread binding that the program joins with a synchronous `t.join()` can also deadlock: the joining thread blocks the main coroutine while the binding's reply still needs the pump to deliver it, so `await` never resumes until the wall clock. The property belongs to that shared walk, not to this backend -- the worker-thread backend consumes the same function -- so the fix belongs in `packages/core/session` where every consumer benefits.
- **A wide binding REPLY expands host-side state per member.** Resolutions cross through `snapshotJsonValue` in [`@deepseek-ai/dsh-session`](../../core/session/README.md), whose `walkJsonValue` pushes one task frame per member, and binding resolution carries no seam-level byte cap. A legitimate reply of several million elements can therefore exhaust the host heap. The property belongs to that shared walk, not to this backend -- the worker-thread backend consumes the same function -- so the fix belongs in `packages/core/session` where every consumer benefits.
- **A cross-thread binding that the program joins with a synchronous `t.join()` can deadlock.** This is specific to the `process` isolation backend: the reply pump runs on the child's main event loop, so a worker thread that joins the main coroutine with `t.join()` blocks the loop the pump needs to deliver the binding's reply, and `await` never resumes until the wall clock. The worker-thread backend does not share this structure, so the fix belongs here, not in `packages/core/session`.
@@ -41,4 +41,5 @@ host 与 CPython 子进程在子进程的 fd 3 上交换一个无版本号的 JS
- **1 秒双限 `ulimit -t 1` 下的 CPU 超限会被报告为 `worker-exit`,而非超时。** 当宿主在一个硬 CPU 限制等于软限制(`ulimit -t N` 同时设置两者)且该限制为 1 的环境下启动时,`_clamped` 无法把软限制降到 0,因此内核在同一 tick 直接 SIGKILL 忙循环,SIGXCPU 永不送达。宿主只在 `signal === 'SIGXCPU'` 时把 CPU 超限分类为超时,因此该超限被报告为 `worker-exit`。当双限为 2 或更大时,软限制会被降低一个单位,SIGXCPU 触发,该次运行成为超时。两种情况 containment 都成立;只是分类被降级。
- **一个 trap SIGXCPU 的程序可以在结算编码期间超过软 CPU 限制并仍报告成功。** 结算时的 CPU 复查在完成值被 flush 与编码之前运行;一个 trap SIGXCPU(软限制)并在构建与编码窗口内继续燃烧 CPU 的程序会在 `die_if_cpu_exhausted` 复查之前返回结果,因此该次运行报告成功。containment 成立——硬限制(软限制 + 1s)与墙钟仍会约束它——只是分类被降级。CPU 复查不在编码中途运行,因为那必须计量编码本身,而编码正是预算已经约束的路径。
- **编码器的直接依赖在调用时解析。** `_encode_json_plain` 通过模块全局查找到达 `_dump_scalar`/`_dump_string`/`json`,因此以 `__main__` 运行的程序在返回合法值后重绑这些名字之一(例如 `__main__._dump_scalar = boom`)可以让编码抛出、把成功降级为 `exception`。值路径顶层的 `_check_done_value`/`_encode_json_plain` 被绑定为 def 期默认值,但其传递依赖没有;这是已接受的残余,原因与对应的 `_dump_*` 辅助函数在实践中不被重绑相同。
- **宽 binding 回复会按成员展开宿主侧状态。** 回复经由 [`@deepseek-ai/dsh-session`](../../core/session/README.zh.md) 的 `snapshotJsonValue` 穿越,其 `walkJsonValue` 为每个成员压入一个任务帧,而 binding 回复在 seam 层没有字节上限。因此一个数百万元素的合法回复可以耗尽宿主堆。程序用同步的 `t.join()` 连接一个跨线程 binding 也会死锁:该线程阻塞主协程,而 binding 的回复仍需泵来投递,因此 `await` 直到墙钟才会恢复。该性质属于那个共享遍历,而不属于本后端——worker-thread 后端消费同一个函数——所以修复应落在 `packages/core/session`,让所有消费方一并受益。
- **宽 binding 回复会按成员展开宿主侧状态。** 回复经由 [`@deepseek-ai/dsh-session`](../../core/session/README.zh.md) 的 `snapshotJsonValue` 穿越,其 `walkJsonValue` 为每个成员压入一个任务帧,而 binding 回复在 seam 层没有字节上限。因此一个数百万元素的合法回复可以耗尽宿主堆。该性质属于那个共享遍历,而不属于本后端——worker-thread 后端消费同一个函数——所以修复应落在 `packages/core/session`,让所有消费方一并受益。
- **程序用同步的 `t.join()` 连接一个跨线程 binding 会死锁。** 这是 `process` 隔离后端特有的:回复泵运行在子进程的主事件循环上,因此一个用 `t.join()` 阻塞主协程的 worker 线程会卡住泵投递该 binding 回复所需的循环,`await` 直到墙钟才会恢复。worker-thread 后端不共享此结构,所以修复应落在这里,而非 `packages/core/session`