Problem / 问题
When the managed TypeScript effect runtime rejects LOOPX_EFFECT_RUNTIME_IDLE_MS, it publishes exactly one JSON envelope on stderr carrying the typed code invalid_idle_timeout plus actionable guidance, and exits with status 2. loopx/control_plane/effect_runtime.py::_startup_diagnostic is the reader that turns that envelope back into a Python-side diagnostic, and its own docstring states the contract: "writes one JSON envelope to stderr".
The reader frames that stream with str.splitlines(), which treats U+0085 (NEL), U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) as line breaks. The rejection message quotes the offending value back to the user, and the server builds it with JSON.stringify, which escapes every code point below U+0020 but leaves those three characters raw. So a rejected value that contains any of them tears the single documented record into fragments that no longer parse, and the caller falls back to the generic runtime_exited_before_ready (exit_code=2) — the same misleading diagnostic that #4106 was opened to remove, still reachable for these three inputs.
This is about which diagnostic reaches the operator, not about accept/reject: all of these values are already rejected, and no runtime-info file is written.
Measured against main at 3eac23563 / 实测
Driving the real managed boundary (effect_runtime.effect_runtime_result("runtime.ping", {}) with the environment variable set, Node 22.23.2):
| Rejected value |
diagnostic_code observed |
Expected |
U+0085 + 250 + U+0085 |
runtime_exited_before_ready |
invalid_idle_timeout |
U+2028 + 250 + U+2028 |
runtime_exited_before_ready |
invalid_idle_timeout |
U+2029 + 250 + U+2029 |
runtime_exited_before_ready |
invalid_idle_timeout |
U+001C + 250 + U+001C |
invalid_idle_timeout |
ok |
space + 250 + space |
invalid_idle_timeout |
ok |
The split is exactly the set difference the mechanism predicts: U+001C is a control character, so JSON.stringify escapes it to \u001c and the record survives; the three above are not escaped and are honoured as newlines by splitlines().
Desired outcome / 期望行为
Frame the diagnostic stream on \n, the only separator the server writes, which is also what the response reader in the same module already does (raw.split(b"\n", 1)[0]). Any other stderr content, such as a Node.js stack trace, must keep reporting no typed diagnostic rather than being guessed at.
Do not change which values are accepted or rejected, the envelope schema, the exit status, or the guidance text.
Acceptance / 验收
- A rejected value padded with each of U+0085, U+2028 and U+2029 surfaces
invalid_idle_timeout through the real Python-to-TypeScript startup boundary and leaves no runtime-info file behind.
- The framing contract is pinned at the reader itself, so a future change of separator cannot silently re-tear the record.
- The already-correct
U+001C and ASCII-whitespace paddings keep reporting invalid_idle_timeout, so the accepted set does not move in either direction.
Related
Problem / 问题
When the managed TypeScript effect runtime rejects
LOOPX_EFFECT_RUNTIME_IDLE_MS, it publishes exactly one JSON envelope on stderr carrying the typed codeinvalid_idle_timeoutplus actionable guidance, and exits with status 2.loopx/control_plane/effect_runtime.py::_startup_diagnosticis the reader that turns that envelope back into a Python-side diagnostic, and its own docstring states the contract: "writes one JSON envelope to stderr".The reader frames that stream with
str.splitlines(), which treats U+0085 (NEL), U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) as line breaks. The rejection message quotes the offending value back to the user, and the server builds it withJSON.stringify, which escapes every code point below U+0020 but leaves those three characters raw. So a rejected value that contains any of them tears the single documented record into fragments that no longer parse, and the caller falls back to the genericruntime_exited_before_ready (exit_code=2)— the same misleading diagnostic that #4106 was opened to remove, still reachable for these three inputs.This is about which diagnostic reaches the operator, not about accept/reject: all of these values are already rejected, and no runtime-info file is written.
Measured against
mainat3eac23563/ 实测Driving the real managed boundary (
effect_runtime.effect_runtime_result("runtime.ping", {})with the environment variable set, Node 22.23.2):diagnostic_codeobservedU+0085+250+U+0085runtime_exited_before_readyinvalid_idle_timeoutU+2028+250+U+2028runtime_exited_before_readyinvalid_idle_timeoutU+2029+250+U+2029runtime_exited_before_readyinvalid_idle_timeoutU+001C+250+U+001Cinvalid_idle_timeout250+ spaceinvalid_idle_timeoutThe split is exactly the set difference the mechanism predicts:
U+001Cis a control character, soJSON.stringifyescapes it to\u001cand the record survives; the three above are not escaped and are honoured as newlines bysplitlines().Desired outcome / 期望行为
Frame the diagnostic stream on
\n, the only separator the server writes, which is also what the response reader in the same module already does (raw.split(b"\n", 1)[0]). Any other stderr content, such as a Node.js stack trace, must keep reporting no typed diagnostic rather than being guessed at.Do not change which values are accepted or rejected, the envelope schema, the exit status, or the guidance text.
Acceptance / 验收
invalid_idle_timeoutthrough the real Python-to-TypeScript startup boundary and leaves no runtime-info file behind.U+001Cand ASCII-whitespace paddings keep reportinginvalid_idle_timeout, so the accepted set does not move in either direction.Related
runtime_exited_before_readyfrom a contributor branch; its validation scope has since landed onmain, and it is being closed with an attribution note rather than revived.