Telemetry was the only call site this PR could not cover without changing
the SDK transport underneath it, and both ways of doing that cost more
than the channel is worth.
Routing an `http.Agent` needs Node's `proxyEnv`, added in 22.21 and 24.5
— inside the engines range, so three supported runtimes stayed direct
anyway, and the proxy package had to keep a `createNodeHttpAgent` export
for a path that only sometimes worked. Replacing the transport with the
SDK's `fetch` delegate covered every runtime but has no compression,
while the shipped `base` bundle enables gzip and a realistic OTLP batch
is 6.4x smaller with it; keeping both meant gzipping at the serializer,
which put transport code inside a telemetry plugin.
Telemetry is the one outbound channel whose loss costs the user nothing:
no tool, model request, or session depends on it, and an export that
cannot connect is already dropped silently. A user behind a mandatory
proxy is left where they were rather than regressed.
`src/index.ts`, `otel.spec.ts`, and `tsconfig.json` return to their state
on master; the package keeps only a dev dependency on the proxy library.
`egress.spec.ts` inverts: it installs a policy and asserts the fake proxy
saw nothing, so an SDK upgrade that moved the exporter onto `fetch` would
surface as a failing test rather than silently routing telemetry.
The previous commit moved the OTLP exporter to the SDK's `fetch` delegate
and refused `exporter.compression` at load, on the belief that nothing
shipped enabled it. `packages/bundle/base/cordis.patch.yml` does, so the
refusal broke every test that boots the shipped bundle — the snapshot,
e2e, and Windows observational jobs all failed on that one load error.
Dropping gzip was the wrong trade anyway: a realistic OTLP batch measures
6.4x smaller with it, so trading it for proxy support would have charged
every deployment to fix one. The `fetch` transport has no compression
hook, but serialization is the seam before the body reaches it — the
plugin now gzips there and declares `Content-Encoding` itself.
`keepAlive` and `httpAgentOptions` have no such seam, since they
configure a connection pool `fetch` does not expose, so those two stay
refused at load rather than accepted and ignored. `compression` is typed
as the two values this package can actually apply rather than the SDK's
wider enum, and a third value fails loud at load.
The package exported six functions, four of them shaped by one SDK's
transport each: a dispatcher factory, a `node:http` agent factory, a
proxy-URL lookup, and a policy accessor. Review asked whether the call
sites could converge instead of the package growing an export per SDK.
They could, and each removal took a whole shape with it:
- The OTLP exporter moves to the SDK's `fetch` delegate, retiring
`createNodeHttpAgent`. Its Node-version floor goes too: `proxyEnv` on
an `http.Agent` needs 22.21 or 24.5, inside the engines range, so
telemetry was direct on 22.19, 22.20, and 24.0-24.4. The cost is
`compression`, a Node-transport option; the plugin now refuses it,
`keepAlive`, and `httpAgentOptions` at load instead of ignoring them.
- `web-fetch-http` builds its own address-pinning agent under an
annotated `proxy-exempt:` exemption, retiring `createDispatcher`.
Pinning is per-request state a process-wide dispatcher cannot hold.
- E2B reads `route.proxy`, retiring `proxyUrlFor`.
What remains is `installProxyFromEnvironment`, `proxyRouteFor`,
`proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller
can need the policy. Installation absorbs resolution and diagnostic
reporting, which no caller needed apart.
`proxyRouteFor` also closes a defect the old accessor made expressible:
`web-fetch-http` read the policy to decide whether to pin, then read it
again to build a transport, so an unmount between the two returned a
direct, unpinned agent for a URL the first read had cleared as proxied.
A route carries the answer and the transport that answer assumed.
Every egress spec now installs through `installProxyFromEnvironment`, so
no test asserts a policy object a real launch could not produce.
Re-declare the derived elevation tokens on body * so per-surface
--dsw-elevation-stroke-color rebinds reach the consuming shadow (custom
properties inherit with var() already substituted); pin that mechanism
and add synthetic rejection cases to the stylesheet scans; take the
ring-track basenames through node:path so the exemption matches on
Windows; update the ModelsSection row-card spec to the hairline recipe;
align the elevation note with the shipped l1 menu rebind, refresh the
feedback-popover note's surface recipe, and document the soft tier.
Apply global visual polish across the web client: corner-shape:
superellipse(1.5) with corner-shape: round pairing for full circles,
elevation tokens that draw 0.5px stroke outlines inside box-shadow for
floating surfaces, 0.5px hairline borders and divider lines for
neutral-token strokes, and tuned stroke contrast plus larger radii for
menus, settings panels, and cards. Stylesheet-scan specs in ui-theme
reject unpaired circles, border+shadow mixes, and 1px neutral hairlines
repo-wide.
Closes#3287
Master moved 212 commits. Conflicts were eight `package.json` and nine
`tsconfig.json` files, all dependency-and-reference unions: master dropped the
`runtime-diagnostics/invariants` reference across packages while this branch
added `util/http-proxy`. The lockfile, `tsconfig.base.json`, and the module
graph were regenerated rather than merged by hand.
Master also brought `verify-package-invariants`, which rejects an empty
invariant companion. This package's companion was empty by design, so it is
gone with its publication wiring — the export, the `files` entry, the project
reference, the `dsh-invariants` peer, and the test. Master did the same across
`util/`. The README carries the reason sentence that gate requires.
Carried in the same commit, because the review arrived while the merge was open:
`e2bApiUrl` moves out of the `dsh-e2b` entry into `src/api-url.ts`. Code Entropy
flagged it as a public name with no consumer outside its own package; the seam
keeps its URL-precedence test, which is worth having directly — getting that
order wrong hands a proxy the control-plane traffic and its API key.
Review asked why this is a plugin and why it exports so much. The design note
this branch shipped answered the second question itself — "a pure resolution
function plus an installation function" — and the code drifted to seventeen
exports and a Cordis plugin nobody approved or mounted.
The plugin is gone. Transport policy has one answer per process: nothing to
swap, and no scope narrower than the process to give one. Its `Config` was also
the only supplier of a configuration branch, so resolution now reads the
environment and nothing else — `mode`, the config-sourced fields, and the
`config` policy source were unreachable the moment the plugin left.
Four exports nothing outside the package used are internal again, and
`currentProxyPolicy` answers with the direct policy instead of `undefined`, so
`DIRECT_POLICY` no longer needs a public face. Nine functions remain, one per
way a caller can need the policy; two is not reachable with six consumer seams.
The package moves to `util/`. The note claimed a dependency on `undici`
disqualified it from that group; the charter governs harness dependencies, not
external ones, and the process note that says so predates this branch. The real
blocker was the harness dependency: resolution needed one method of
`LaunchEnvironmentSnapshot`, so it names a structural `EnvLookup` and the
launcher passes its snapshot unchanged. `net/` is dissolved.
Dropping the group's line from the repository layout also returns `AGENTS.md`
to its original ceiling, so the raise the merge needed is reverted.
Review follow-up: the per-step gate recomputed view(previous) on every
changed apply — a property read for identity-stable views, but a fresh
throwaway object per change for computing views. The registry now keeps a
WeakMap from state object to raw view: the previous state's view was
cached when that state was current, so each distinct state's view computes
exactly once (gate and snapshot share the memo) and the quiet path
allocates nothing. Unlike the earlier lastView record, an entry is keyed
by the state itself — the view of that exact state by the pure-view
contract — so no stamping discipline exists to get wrong. Primitive
states bypass the WeakMap and compute directly.
Review consensus: for the only declaring unit the token function was
literally the view function, so the extra wire member bought nothing —
default raw-view comparison produces the identical Object.is on
state.turns at the identical cost (one property read per side). The
per-step gate stays: it holds the no-stored-baseline property, and its
measured overhead (~ns per changed state) is four orders of magnitude
below the push path it guards.
Review follow-up (imccyu): comparing view(previous) recomputes the view on
every quiet change. The wire block now takes an optional viewKey(state)
declaring the cheap comparison token; the drive compares tokens across the
previous and next states and calls view only for an actual push. Default
stays the raw view output. turnOutline declares viewKey: state => state.turns,
making the identity-stable-turns convention an explicit contract; the
registration erasure forwards viewKey (dropping it silently reverted the
gate to the fallback, caught by the new view-call-counting test).
Review suggestion (imccyu): the drive holds both the previous and next
state, so the identity gate can compute view(previous) and view(next) in
the driving step and compare them directly. The stored lastView cell field
and its stamp-on-every-change rule are deleted; with no dedup memory,
nothing can go stale across listener generations by construction, and a
rebuilt cell no longer pushes an unchanged view on its first live event.
Costs one extra pure view() call per changed state.
The change feed stamped lastView only when a listener was subscribed, so a
value change during a listener-free window (HMR swap) froze the baseline
and a later transition back to the old value was silently deduplicated.
The baseline now advances on every changed state, heard or not; broadcast
still only happens with listeners. Docs, catalog, and the feature note
follow the corrected semantics.
The feed previously fired on every changed state reference of a
client-visible unit; it now also compares the raw view output against
the last delivered one and stays quiet when Object.is-identical, so a
unit can buffer working fields in state behind an identity-stable
projection. Units whose views build fresh objects per call are
unaffected.
New turnOutline projection unit serving every started turn's number,
turn/start seq, and bounded first-prompt preview through the
session-projection seam, mounted in the web-app bundle for the chat
turn rail. Entries stay strictly increasing; previews mirror the rail's
loaded-turn preview budget.
The capability-seams graph derives its implementation lists from
SERVICE_ROLES in scripts/gen-doc-graphs.ts, which still listed only the
worker-thread backend. Add experimental-code-runtime-python so the
generated graph and table match the registered ctx.codeRuntime
implementations; regenerate docs/capability-seams.md, sync the zh pair,
and re-record the i18n pairing.
Review findings on the CPython backend: an explicit pythonBin path bypassed
the load-time checks (missing/non-executable/directory paths surfaced only
as a run-time worker-exit); a throwing binding member accessor escaped the
fd-3 data callback and terminated the host; the reply drain waited on
'drain' alone, so a pipe destroyed under the wait hung forever; and two
staging-leak assertions diffed a global tmpdir that parallel workers can
perturb.
resolvePythonBin now applies the same accessSync(X_OK) + isFile check to
explicit paths (resolved against the host CWD), and the load error message
distinguishes 'is not an executable regular file' from 'does not resolve on
PATH'. validateBindings snapshots callables into a plain record during run()'s
synchronous validation, turning an accessor throw into the seam-misuse
rejection and fixing the key set the boot frame and dispatch share. The reply
drain waits on drain/close/error together and short-circuits on
proto.destroyed. The staging-leak assertions check the exact paths this test
file staged (recorded by the mocked mkdtempSync) instead of a tmpdir diff.
docs(code-runtime-python): add the alternatives section to the hardening note
docs(config-catalog): refresh the code-runtime-python Config source line
test(code-runtime-python): cover the async spawn-error worker-exit path
The review's carry-over: 'each has a published backend' in the CodeRuntime
JSDoc and its projections (tool-cordis api-catalog, subsystems page) plus
'both shipped'/'backends ship' in the code-runtime README all claimed the
Python backend is released; it is private and experimental, excluded from the
release family. The wording now states the TypeScript backend is released and
the Python backend is experimental and private (not published), in the JSDoc
(api-catalog regenerated to match), the READMEs (paired), and the subsystems
page (paired).
The CPython code runtime's complete public contract is experimental, so it
moves to packages/experimental per the experimental-packages rules: npm name
@deepseek-ai/dsh-experimental-code-runtime-python, private: true, no
publishConfig. All references updated (code-runtime READMEs, config-catalog
and module-graph regenerated with zh alignment, tsconfig paths, doc-standard
and workspace-constraints scripts, the fd-3 and settlement Agent Notes, and
the package README links); md-links and translation pairing pass, and the
suite still runs green.
The earlier merge had adopted master's protocol-only package.json (peer/dev
limited to invariants and cordis, no dependencies), but src/index.ts imports
@deepseek-ai/dsh-code-runtime, dsh-session, dsh-timeout, and schemastery at
runtime — a published lib/index.js could not resolve those bare specifiers.
The manifest now mirrors code-runtime-worker-thread (the five peers, the
schemastery dependency, and the matching dev set); the lockfile, module graph,
and third-party notices are regenerated, and the module-graph zh pair is
re-synced.
The master merge brought a stale generated config-catalog that omitted the
dsh-code-runtime-python config section and mislisted the package. Regenerate
docs/config-catalog.md (verify-config-catalog passes), translate the python
config section into zh, keep the ts config-catalog code blocks verbatim
(untranslated, per the pairing rule), and drop the stray zh Library-packages
line. Corpus-wide verify-translation-pairing passes 1029.
`sendReply` ignored `proto.write`'s `false` return, so a program resolving
several large values in one `asyncio.gather` round encoded every reply in the
same turn and queued all of them in fd 3's writable buffer. Binding resolution
carries no seam-level byte cap to bound that, and the failure kills the host
process rather than failing the run: measured on a 64 KiB-highWaterMark pipe,
eight 4 MiB replies buffered 32.0 MiB at once against 0.0 MiB once paced.
Replies now go through a queue that encodes and writes one frame at a time,
awaiting `drain` when the pipe is full. The encode happens inside the loop, so a
queued reply the run no longer needs is dropped by the `settled` check without
ever being serialized.
This was previously deferred on the grounds that serializing would narrow the
seam's concurrency contract. That reasoning was wrong: the child matches each
reply to its `call` by id from a pump that reads fd 3 continuously, so arrival
order was never observable, and the bindings still run concurrently. Only the
host's peak memory and the flush timing change. The README entry recording the
deferral is removed and the Agent Note records the mechanism instead.
Adding a published Python backend and reordering `flush_line` left several
owning documents stating things that are no longer true.
`src/invariant.ts` justified its empty installer with "ships only the fd-3
wire-protocol codec", which the subprocess execution path contradicts. The
reason now states the actual one: every relation this backend maintains lives
in the CPython child or on the fd-3 wire, so no same-process event sequence is
observable from a listener -- the same shape the sibling worker-thread backend
uses.
The seam's `PORTABLE_RESERVED_WORDS` and `language` JSDoc, the code-runtime
README pair, and docs/subsystems/code-runtime both said only TypeScript has a
published backend. Corrected in all four, with the generated cordis catalog
regenerated for the `language` change.
The note attributed the 12x multiple to the settlement flush holding three
copies. That stopped being true when `flush_line` was reordered to drop the
pending chunks before its push: the binding worst case is the newline path's
single near-budget write. Corrected in the note (both sides) and in the test
comment that repeated it.
The note's Testing section now registers the cases this stack added, and the
Chinese side receives the O(depth) entry it never got plus the new ones -- it
had drifted from the English.
`INTERPRETER_BASELINE_BYTES` argued 64 MiB from a RESIDENT set while RLIMIT_AS
bounds address space. It now cites the bootstrap's own measurement (30.23 MiB
of mappings for `python3 -I`), making 64 MiB roughly twice the measured
baseline.
Also: a hardcoded `(:232-235)` comment reference becomes a reference by name,
a "which now walks in O(depth) too" change narrative becomes a current-state
statement, and a stray double blank line is removed.
The load-time output-budget/addressSpaceMb gate used a worst-case multiple of 8,
assuming two simultaneous ~4x astral copies (the built string and its encode).
Three are live at the peak: on the newline path a single write holds the caller's
text argument, the line slice handed to push, and push's encode copy; the
settlement flush_line path held the pending chunks, their join, and that encode
copy. A budget admitted at 8x (e.g. maxLogBytes 48 MiB against addressSpaceMb 512)
could still OOM the child. The multiple is now 12, the strict `>` is `>=` so a
budget whose peak exactly equals the room left after the interpreter baseline is
rejected (that peak plus the baseline is the whole address space), and flush_line
drops the pending chunks before its push to match the newline path's
join-clear-push order. The child re-check mirror and both note sides move in step;
config-catalog is regenerated from the updated field JSDoc.