Commit Graph
14 Commits
Author SHA1 Message Date
Chinesezjc 4d49406bd5 fix(code-runtime-python): bind the wire-field mirror to TS required/optional keys
The previous mirror binding (FrameFields<keyof T>) only checked membership: it
could not see a TS-side optionality flip (truncated? -> truncated leaves keyof
unchanged) or a field added on one side, so the "depends on the TS
declaration" claim was overstated.

- Promote the inline frame shapes (Namespace, ErrorClass, DoneErrorField,
  RunMessage, and the two Reply variants) to named interfaces so every frame
  binds uniformly.
- Derive FrameFields from RequiredKeys<T>/OptionalKeys<T>, so `required` and
  `optional` each accept only that side's keys. An optionality flip or a rename
  now fails typecheck (verified: flipping LogMessage.truncated to required
  errors at the constant).
- Enumerate EVERY public TypedDict in py/protocol.py in the mirror e2e (not a
  name list taken from the TS side) and assert both the frame roster and each
  frame's required/optional sets by exact equality, so a frame or field present
  on only one side of the wire fails the test.
2026-08-07 13:27:54 +08:00
Chinesezjc be839a8e53 fix(code-runtime-python): count non-lossless bytes and bind the mirror gate to TS types
Two gaps from the previous round's fixes:

- checkDoneValue flagged a non-lossless number but skipped counting its encoded
  bytes, so a value over budget ONLY through that number classified as
  non-lossless instead of over-budget (e.g. [Infinity] at cap 3, whose encoding
  is 10 bytes). Count the scalar's bytes even when flagging, so the budget check
  wins as the JSDoc promises. Add cap-3 regression cases.

- The mirror e2e compared the Python TypedDict keys against a hand-written
  constant, so a field change on the TS side alone would not fail it, and the
  reply frames were not probed at all. Introduce WIRE_FRAME_FIELDS in
  protocol.ts, bound to each frame interface's key set via `satisfies` (a
  renamed/removed field breaks typecheck — verified), and drive the mirror test
  from it, now covering ReplyOk/ReplyErr too. The test therefore fails on
  one-sided drift from either language.
2026-08-07 13:27:54 +08:00
Chinesezjc 8a60b5f005 feat(code-runtime-python): make the TypedDict wire mirror an executable gate
Address the two standing review suggestions in this layer rather than deferring
them to PR #4:

- Extend tests/protocol-mirror.e2e.ts to read each py/protocol.py TypedDict's
  required/optional key set and assert it against the wire field names
  src/protocol.ts declares (global included, via functional TypedDict). The
  round-12 class of drift — a renamed/dropped field, or one side making a field
  optional the other requires — now fails a test instead of relying on review.
  Field types remain review-guarded (no mechanical TS/Python equivalent).
- Drop the forward references to PR #4's internal mechanisms from this layer's
  prose: the "256 MiB frame ceiling" figure and the "(index.ts)" fd-3 pinning
  citation become an abstract "host-side inbound frame-size cap" so the JSDoc,
  spec, README, and Agent Note describe only what this layer owns.

Update both README sides and the Agent Note (both languages) to state the
mirror is now executable, and re-record their i18n pairings.
2026-08-07 13:27:54 +08:00
Chinesezjc 146a9d9f61 fix(code-runtime-python): make checkDoneValue over-budget precedence order-independent
checkDoneValue returned non-lossless the instant it hit a non-finite/negative-
zero number, before finishing the budget metering. A value that is BOTH over-
budget and non-lossless then classified by member order: `["<huge>", 1e400]`
gave non-lossless while `[1e400, "<huge>"]` gave over-budget — the same value,
two verdicts — which would drive the consumer to emit invalid-output vs
output-limit non-deterministically, contradicting the JSDoc promise that an
over-budget value is rejected as over-budget regardless. Record the number
violation in a flag and let metering finish; return non-lossless only once the
whole value is confirmed within budget. Add a regression test asserting both
member orders classify as over-budget.
2026-08-07 13:27:54 +08:00
Chinesezjc 8cf253a470 docs(code-runtime-python): sync README metering claim with code and note
Both README sides still described checkDoneValue as "one bounded traversal /
一次有界遍历" — the same overclaim already retracted in the code JSDoc and the
Agent Note. Reword both to match: the walk bounds only the incremental
allocation it adds (escaped-string copy, enqueued children, per-key stringify);
the frame's own width is parsed upstream and capped by the host's fd-3 receive
buffer, not re-bounded here. Re-record README.i18n.yaml.
2026-08-07 13:27:54 +08:00
Chinesezjc 69796d214c fix(code-runtime-python): remove NUL bytes and sync the Agent Note metering claim
- Replace four raw U+0000 bytes in protocol.spec.ts string literals with the
  \0 escape so the source stays plain text (a bare NUL makes text tools treat
  the file as binary); the runtime value is unchanged, so the bytes:8 NUL-escape
  assertion still holds.
- Sync the Agent Note (both languages) with the corrected checkDoneValue
  contract: the walk bounds only the incremental allocation it would add, not
  the frame width, which is already parsed and capped upstream by the host's
  fd-3 receive buffer. Drop the "prevents a hundreds-of-MB allocation" overclaim
  that the code JSDoc already retracted. Re-record the note i18n pairing.
2026-08-07 13:27:54 +08:00
Chinesezjc b4487485c2 docs(code-runtime-python): correct ownValues allocation claim
ownValues' JSDoc claimed the generator avoids a "second full-breadth
allocation before a single value is examined", but for...in still materializes
the key-name enumeration when the loop starts — the same JS limitation the
checkDoneValue rewrite now acknowledges. What the generator genuinely saves is
the extra VALUE array Object.values/Object.entries would copy; state that
precisely rather than implying sublinear startup.
2026-08-07 13:27:54 +08:00
Chinesezjc ae8070d799 fix(code-runtime-python): stop overclaiming O(cap) object metering
checkDoneValue cannot bound object width sublinearly: JS has no lazy own-key
iterator (for...in materializes the key set), and done.value is already
JSON.parse'd before the check runs, so the frame's width is paid upstream. The
genuine width bound is the host's fixed 256 MiB fd-3 receive buffer (a later
stack layer). Reword the JSDoc and branch comments to claim only what holds —
the traversal caps the INCREMENTAL allocation the check would add (escaped
strings, enqueued children, per-key stringify) and refuses over-budget before
those secondary allocations — and drop the mid-count micro-check that JS cannot
honor. Replace the Proxy test (whose ownKeys allocated a 2M array, proving
nothing) with assertions that an over-budget string/array/object is refused
before its escaped copy or child enqueue.
2026-08-07 13:27:54 +08:00
Chinesezjc 104cd5f975 fix(code-runtime-python): bound checkDoneValue object metering in O(cap)
The object branch counted every own key before applying the size bound, so a
forged done.value with millions of keys and a small cap forced an O(frame)
walk — contradicting the O(cap) guarantee the comment promised and able to
block the host event loop. Bail mid-count the instant the running minimum
encoding (braces + 4 bytes/entry + commas) crosses maxBytes, and drop the now
-redundant post-count check the loop subsumes. Add a Proxy-based test proving
a 2M-key object enumerates fewer than 1000 keys under a 64-byte cap.

Also correct the checkDoneValue JSDoc: per-scalar byte length is measured via
scalarJson (exact BigInt digits for beyond-safe integers), not JSON.stringify.
2026-08-07 13:27:54 +08:00
Chinesezjc 31506dec2d fix(code-runtime-python): correct Chinese translation quality
- Translate README.zh.md's Model Experience body and KV Cache line, which
  were left verbatim in English.
- Convert half-width punctuation to full-width across the README.zh.md
  Known Limitations bullets and the entire Agent Note Chinese side, per
  docs/i18n translation-rules.md Typography (MUST use ,。:()in Chinese prose).
- Re-record both README and Agent Note i18n.yaml pairing hashes.
- Reword the workspace-constraints extra-files comment: this layer's py/
  ships only the wire-protocol mirror; the spawned bootstrap arrives later.
2026-08-07 13:27:54 +08:00
Chinesezjc 98ebe1315d fix(code-runtime-python): reject -0 call ids and document done value/error
- Drop a CALL frame whose id is negative zero: it passes Number.isFinite but
  the reply re-serializes it as `0`, colliding with a real call id `0`. The
  honest child never issues `-0`.
- Document that validateChildFrame preserves a forged done frame's value and
  error together on purpose, so consumers must check error before value.
2026-08-07 13:27:54 +08:00
Chinesezjc f0d669883f fix(code-runtime-python): close coverage gap and tighten the wire mirror
- Cover the log-frame `truncated` rebuild branch: assert a literal-true flag
  rides along and any other value (1, string, false) is dropped, closing the
  protocol.ts branch the coverage gate flagged.
- Correct encodeJsonPlain's JSDoc: it matches compact JSON.stringify EXCEPT on
  a beyond-safe-range integral double, where it emits the exact BigInt digits
  (`...846976`) rather than the rounded `...847000` — the divergence the
  "emits exact digits" test pins.
- Declare py/protocol.py's `global`-bearing frames (Namespace, CallMessage)
  with functional TypedDict syntax so they carry the real wire key instead of
  a `global_` attribute the wire never sends, and split optional-field messages
  (Namespace/LogMessage/DoneMessage) into a required base plus a total=False
  subclass so `type` and other required fields cannot be dropped. Widen
  HostToChild to include the boot and run frames the host sends before replies.
- Reword the mirror e2e's py/ directory assertion to describe the source-tree
  layout it actually checks.
2026-08-07 13:27:54 +08:00
Chinesezjc b3e29e7af5 fix(code-runtime-python): satisfy static gates for the protocol-only layer
- Drop the unused @deepseek-ai/dsh-code-runtime dependency: this layer
  imports nothing from the seam (protocol.ts has no imports; the invariant
  companion uses only cordis and dsh-invariants). The backend-core PR
  re-adds it when PythonCodeRuntime consumes the seam. Fixes knip.
- Point the Agent Note's cross-reference to the seam note at the English
  target on both language sides, per the bilingual-pairing contract (only
  the language switcher flips to .zh.md). Re-record the sidecar.
- Add the Known Limitations section both READMEs require, covering the
  cross-language guard's scope and the deferred runtime implementation.
- Regenerate the module graph for the dropped dependency edge.
2026-08-07 13:27:54 +08:00
Chinesezjc e0f22aeaad feat(code-runtime-python): add the fd-3 frame protocol
Introduce @deepseek-ai/dsh-code-runtime-python with the versionless
JSON-lines protocol between the Node host and the CPython subprocess:
the host-side hostile-frame codec (validateChildFrame, encodeJsonPlain,
checkDoneValue, hasUnsafeIntegerToken, hasNonLosslessNumber,
logTruncationMarker) and the Python-side wire-vocabulary mirror
(py/protocol.py).

This is the protocol layer of the code-runtime-python stack, split from
#436 and based on the multi-language seam extension. The PythonCodeRuntime
implementation and its Python JSON codec land in the backend-core PR on
top of this branch.

Ship the minimal buildable package skeleton (package.json, tsconfig,
tsdown, barrel index, invariant companion, bilingual README) because the
workspace-constraint, coverage, and invariant-topology gates require the
package to exist and build the moment its directory does; the backend-core
PR extends those files rather than creating them.

Align py/protocol.py with src/protocol.ts (the round-12 review of #436
found LogMessage.truncated, DoneMessage.error.kind, and Namespace.errorClass
stale) and guard the two runtime-executed surfaces (PROTOCOL_FD and the log
truncation marker) with a real-python3 cross-language mirror e2e test.
2026-08-07 13:27:54 +08:00