The 60k oneOf-object test had only one object node (the innermost), so the
pre-fix code called allocateClassName once — linear, never tripping the
timeout, so it did not cover the class-name Θ(depth²) it named. Give every
level an object branch (both oneOf arms are objects) so each level propagates
a one-segment-longer class name; the pre-fix rope slice is then Θ(depth²)
(~9.5s, past the 5s default) while the capped path stays linear. Also extract
the shared cap expression into capClassNameBase (used by allocateClassName and
childClassName). py-types.ts stays at 100% per-file coverage.
The oneOf perf fix left a second Θ(depth²): a deep oneOf chain whose branches
are named objects propagated an ever-growing ConsString as the class-name
base, which allocateClassName then re-materialized (.length/.slice) at every
level. A childClassName helper now caps the base AT PROPAGATION, so each level
is O(1) and the walk is linear; the collision counter still makes truncated
bases unique. Also reword the oneOf comment (it said `+` but the code uses a
template literal — both are ConsString) and strengthen the tests: the deep
oneOf test now runs 100k levels (a quadratic regression trips the 5s timeout),
plus a 60k oneOf-object chain and a >120-char tool-name cap case. py-types.ts
stays at 100% per-file coverage.
A deep oneOf chain joined the accumulated union string at every level
(Array.join forces materialization), making it Theta(depth^2) — a
50,000-level chain took ~7.6s. Concatenate with `+` instead: V8 builds a
lazy ConsString that materializes once at the root, matching the array
arm's template-literal laziness and ts-types' composable-document approach.
The whole walk is now linear in depth. Adds a 20,000-level oneOf test
alongside the existing deep-array one; py-types.ts stays at 100% coverage.
The SDK_RENDERERS JSDoc kept the circular "a renderer here … plus the
renderer itself" phrasing the note already fixed, and its
{@link RUN_CODE_FLAVORS} pointed at a non-exported const in another module
(unresolvable). Reword to "an entry here and a RUN_CODE_FLAVORS entry in
code-mode.ts … plus the renderer function this table points at".
Follow-ups from the bot's review of the trusted-after-validation revert:
- renderType's Frame now carries JsonSchemaNode (the root schema is asserted
before any frame is built), dropping the `as Record<string, unknown>` casts,
the `node.oneOf as unknown[]` cast, and the runtime `required` filter — the
same typed-frame shape as the sibling ts-types renderer, so the "symmetric
with ts-types" claim holds structurally, not just behaviorally.
- The language-dispatch note broadens the trusted-input argument to cover all
real sources (first-party defineTool/raw registration and wire-derived plain
JSON), and the zh side uses full-width punctuation per translation-rules.md.
py-types.ts stays at 100% per-file coverage.
Rounds 6-9 of the bot review kept finding adjacent hostile-getter variants
(post-validation cycles, TOCTOU on const/enum/oneOf, self-referential
functions) because the renderer had grown per-shape runtime defenses the
sibling ts-types renderer does not have. Those inputs are unreachable: the
schema is a first-party defineTool object literal that already passed
assertSupportedJsonSchema, and per AGENTS.md "Trust TypeScript at typed
same-process seams" a typed same-process seam does not add hostile-input
handling for values the static interface forbids.
renderType now validates the whole tree once and trusts it, wrapping the walk
in one try/catch that degrades to Any — byte-for-byte the stance of the
ts-types sibling. This removes the cycle-tracking (activeSchemas/hasIdentity),
the const/enum/oneOf read snapshots, the isPyScalar re-check, the typing
rollback, and the pyScalar null->None re-read handling; the corresponding
hostile-getter tests are removed. Behavior fixes that hold for legitimate
input are kept: RESERVED soft-keyword exclusion, closed-empty-object TypedDict,
class-name cap + per-base collision counter, BigInt digits for beyond-safe
integers. py-types.ts stays at 100% per-file coverage.
The language-dispatch Agent Note documents the stance and its symmetry with
ts-types so the boundary is not re-litigated.
Address ds-review-bot v5/v6 review round 8. The prior guards re-read a
stateful getter's value between the check and the spelling, so a getter
returning different values across reads could still emit invalid Python:
- renderConstrainedScalar reads node.const ONCE into a local, then checks and
spells that snapshot; a third-read switch can no longer produce
Literal[[object Object]].
- The enum path snapshots via [...raw] (reading each element exactly once,
covering accessor-property elements) and requires the snapshot be a non-empty
all-scalar array; an emptied re-read no longer spells Literal[], and a
non-array re-read degrades.
- The oneOf branch build guards a non-array or empty re-read to Any instead of
joining to '' (a missing type).
- pyScalar spells null as None; its JSDoc no longer claims null cannot reach it.
Tests cover each re-read shape; py-types.ts stays at 100% coverage.
Address ds-review-bot v5/v6 review round 7:
- The render-walk cycle guard tracked only plain objects; a function has
typeof 'function' yet carries own properties and can reference itself, so a
post-validation getter returning a self-referential function bypassed the
guard and looped forever. A hasIdentity() helper now covers objects AND
functions, applied symmetrically at the three sites (root add, finish remove,
child check).
- renderConstrainedScalar re-reads const/enum at render time; a stateful getter
that validated as a scalar could return an object, spelling the invalid
Literal[[object Object]]. It now degrades to the broad type when the re-read
value is not a scalar (or the enum not an all-scalar array).
- The activeSchemas comment notes the out-of-scope boundary: a getter
fabricating a fresh node per read never repeats an ancestor and is
indistinguishable from a legitimately unbounded-depth schema.
Tests cover the function cycle and non-scalar const/enum re-reads; py-types.ts
stays at 100% per-file coverage.
Address ds-review-bot v5/v6 review round 6:
- renderType tracks the active ancestor schemas by object identity (the frame
stack is the DFS path). A stateful getter can mutate the graph after
validation so a child returns an ancestor at render time; without this the
walk pushed frames forever instead of degrading. A repeated ancestor now
degrades to Any, honoring the never-throw contract; distinct nodes in a
legitimately deep chain are different objects, so it stays O(1) per push and
O(depth) memory.
- The multiline allocateClassName JSDoc was still attached to the
MAX_CLASS_NAME_BASE constant (a self-referential @link, and the function had
no doc). Move the doc onto the function and give the constant its own
one-liner.
- Tests cover the post-validation cycle and a non-object render-time child;
py-types.ts stays at 100% per-file coverage.
Address ds-review-bot v5/v6 review round 5:
- allocateClassName: keep a per-base collision counter (state.nextClassCounter)
so a deep single-field chain sharing one capped base allocates in amortized
O(1) instead of rescanning from 2 each time (Theta(depth^2) time); remove the
stale one-line JSDoc left above the multiline one and attach the doc to the
function, not the constant.
- renderType's catch rolls back the typing symbols the discarded subtree added
(not just the classes) so the import line still lists exactly the symbols the
surviving output uses; the comment now names that the same path also degrades
this module's internal-invariant throws to Any, the trade for never throwing.
- README (both languages) no longer describes an installable
dsh-code-runtime-python package: the Python renderer is built in and drives
any runtime reporting language: 'python'; the first-party backend ships
separately.
- Tests: assert the render-phase degrade on the first call, assert the import
line after rollback, and cover the collision-skip loop; py-types.ts stays at
100% per-file coverage.
Address ds-review-bot v5/v6 review round 4:
- renderType now holds the no-throw contract across the whole walk, not
just root validation: a stateful getter that passes validation and then
throws in the render phase degrades the node to Any, rolling back any
classes the call had begun emitting, instead of escaping.
- allocateClassName caps the accumulated base name. Child class names
derive from their parent's, so an unbounded single-field object chain
grew the sum of names to Theta(depth^2) (a 5000-deep schema produced a
~25MB SDK); the cap keeps total emitted text linear, the collision
counter still makes truncated bases unique.
- The language-dispatch note's Consequences first sentence and the zh
guard paragraph are corrected: two table entries (not one), and
full-width Chinese punctuation per translation-rules.md.
The live fast-path fence and the raced-collision catch duplicated the same
subagent-ownership classification, tripping the duplication gate. Extract
`fencedLiveAgent` so both paths resolve one live identity through the
fence identically.
The client plugin now consumes `ctx.locale` (dictionary registration plus
the slot `t` seat), but the package graph did not know it: no
`dshClient.inject` entry, no peer/devDependency, no tsconfig project
reference. Mirror the ui-conversation convention so the dependency graph,
HMR/preflight metadata, and standalone packaging all recognize the
`@deepseek-ai/dsh-client-locale` seam.
The raced-collision catch mirrored only the subagent-owned half of
ensureSession's `.catch`: a concurrent plain-agent publish winning the
identity still fell through to `internal`, where ensureSession returns
the winner. Mirror in full — classify a subagent-owned winner as
`agent-busy`, return a clean plain-agent winner directly.
The README claimed "acceptance is governed by registry presence" as a
universal statement, but `sendReport` translates a registered parent's
send rejection into the same PARENT_UNAVAILABLE code — registry presence
governs parent *resolution*, while acceptance additionally depends on the
parent's log still admitting appends. Soften both languages to the
precise contract and re-record the pair.
The `host/session-removed` invalidation flipped the owned catalog and
addressed children to `parentAvailable:false`, but a `subagent.list` pull
already in flight was requested before the removal and its ok-response
carries the pre-removal `parentAvailable:true` — the response then
overwrote both the catalog and every addressed child, resurrecting the
writable-editor-against-a-dead-continuation-owner bug the invalidation
closes, with no refresh scheduled to converge afterwards.
Mark the owner stale when a pull is in flight at removal time, so one
trailing refresh runs after the in-flight response settles and the
post-removal host truth lands. Adds a regression test: removal mid-pull,
stale ok response, trailing pull, final state stays unavailable on the
catalog and the addressed child.
The setup validation and commit moved into the callback, so the outer
definite-assignment slot and its type import are no longer needed; declare
the transaction as a callback-local const.
Two pairs needed their confirmed-consistent state refreshed: the
intent-named note's supersession clause (zh link normalized to the shared
`.md` target, since the pairing contract requires identical link targets)
and the report README's acceptance-semantics rewrite (both sides edited).
Re-record both pairs so the translation-pairing gate passes.
Config() applies the schemastery default at runtime, but its return type
keeps the input's optional field, so assert the resolved shape at the
seam — keeping the dead fallback branch gone.