Files
deepseek-harness/packages/experimental/webworker-runtime/README.md
T
imccyu 22ddee3792 Merge pull request #7075 from deepseek-harness/worktree-chatgap
feat(chat): tighten the process-row gap to 4px
2026-10-23 19:48:59 +08:00

18 KiB
Raw Blame History

description, kind
description kind
Browser-worker harness hosting for maintainers building or debugging the experimental Web preview runtime. package-library

@deepseek-ai/dsh-experimental-webworker-runtime

English | 中文

Summary

The browser worker host: the whole harness plugin tree runs inside one dedicated Web Worker, for preview deployments and packaging regressions (experimental group). The worker inflates a packed VFS image off its download and mounts it in memory, loads its modules through a CommonJS wrapper loader, and serves the page over a postMessage tunnel that speaks plain HTTP. Use it when a preview must run the packaged harness without a Node host.

Table of Contents


Use this package

The public npm package exposes the runtime library at its root, the self-contained browser Worker at ./worker, and the page connector at ./client. Deployments supply and serve the VFS image separately through the packer; installing this library does not add a profile layer. The root also exports the module-proxy and replacement tables used by the packer.

Three build artifacts and one source-owned process layer:

  • lib/index.js (assembly library) — createWorkerHost/startWorkerHost mount the base image and any ordered data overlays (storage/), register the image's lazy archives, install the module loader (module-system/) and the process shim, boot the tree through the image's own dsh-app-boot, and hand the tunnel its serving seams. Overlays may replace files only under home/ and workspace/; they cannot replace the base manifest or modules. The image layout contract (image-layout.ts: virtual root, manifest path, lazy archive directory, empty directories) and the manifest (image-manifest.ts: profile, installation, the lowered wrapper-contract gate, lazy archives) are shared with the packer. The Worker boots the manifest's shipped profile as the launcher does: the image's app-boot initializes home/profiles/<profile>/ from the shipped template, resolves bundles from the installation manifest the image carries alone, and composes the bundle layers, the profile patch, and the home patch. The profile context, the launch environment snapshot, and a PluginPackages service without a runtime resolution are provided as the launcher provides them; the Worker loader owns module resolution. The profile context carries no command-line overlay; the image's home patch (home/cordis.patch.yml), which the packer writes from PLATFORM_ENTRY_CONFIG, keeps JSONL Session logs plaintext. ConfigEditor persists overrides in the profile patch, reconciles each write, and Loader commits live values. The Worker commits appReady once the tunnel serves, so HMR loads and applies profile changes live; the Worker's ES module cache stays empty and the process shim reports no entry script, so HMR compares and replaces no loaded modules. Plugin management lists and switches the installation's optional bundles, and its package operations fail with the execa refusal. Configuration edits last for the current Worker lifetime.
  • Lazy archives — the manifest registers each page chunk the packer moved out of the base image by path and size, with its archive name relative to the image URL. Existence, stats, and listings answer at once; the first asynchronous read (fs.promises.readFile, fs.promises.open, fs.createReadStream, fs.promises.cp) fetches and inflates that archive once for every waiting reader, checks it against the registration, and mounts its files without publishing a mutation. A failed fetch rejects the read and leaves the files registered, so the next read fetches again. Until then, synchronous reads, writes, appends, truncation, chmod, rename, and link of a registered file fail with ERR_DSH_VFS_DEFERRED naming the archive; unlink and recursive removal drop the registration, and seeding the path replaces it. The archive name derives from its content digest, and the fetch checks only the registered paths and sizes, not the digest. An image that registers lazy archives must be passed as a URL.
  • lib/worker.js (worker bundle) — the assembly plus this package's Node-compatibility layer as one self-contained ES module. The module proxy table (module-proxies.ts) is the only platform fork: node:* builtins over VFS/tunnel/browser primitives, structural stubs that fail loud on the console for what a browser cannot do, and native/binary package replacements. node:module supplies createRequire().resolve and .resolve.paths() over the image package root, so unchanged packages can discover manifests without evaluating their modules. node-addon-require-builtin answers the Node v24.18.0 internal modules that the vendored Loader, app-boot, and HMR read without --expose-internals: the cascaded ESM loader's resolveSync and import over the Worker loader, with Node's ERR_MODULE_NOT_FOUND and ERR_PACKAGE_PATH_NOT_EXPORTED codes, and an empty loadCache; and the CommonJS loader, package reader, modules binding, and resolve-cache members that HMR replaces, which refuse when called. process.versions.node reports 24.18.0, so ModuleLoader.fromInternal() and the Loader's internal reach that cascaded loader. node:path.win32.isAbsolute and node:util.inherits follow Node; other win32 members throw. The node:util JSON-data comparator compares own enumerable properties, preserving Node admission decisions for recorded fields such as __proto__. The global process shim carries Node detection fields including title, preventing Worker execution from entering DOM-only branches; Node’s global alias references the same globalThis object for image-loaded packages. The pack-time parser reports statically named module requests, including module-scope direct calls of the form createRequire(import.meta.url)('pkg') through a named node:module or module import, to the packer's reachability walk. Stored, CommonJS-obtained, and rebased createRequire calls require image entry seeds. VFS mutations drive node:fs callback, polling, and promise watchers, whose listeners run in the async context where the watcher was created rather than that of the writing code; open descriptors retain file identity and access mode across rename, replacement, and unlink, FileHandle.stat({ bigint: true }) reports the same device and inode identity as a path stat while the name still refers to that file, and FileHandle.chmod() updates the opened file identity; readable-stream supplies node:stream/promises completion and pipeline helpers and the stream state machine used by file streams and unchanged image packages such as Chokidar and readdirp. The maintained string_decoder package supplies node:string_decoder, retaining incomplete multibyte characters between chunks. AsyncLocalStorage carries sync-stack causality across await through the snapshot/restore faces the pack-time lowering injects. The worker holds no compiler: an image the packer did not lower is refused at mount. The callback fs.realpath.native alias resolves within the mounted VFS and supports util.promisify.
  • src/shell/ (the worker's own process layer) — a browser worker cannot fork, so node:child_process is not a stub but an implementation: spawn starts the command in its own Web Worker — this same bundle, told by its first frame to be a shell process — and reports it through the ChildProcess surface the subprocess service consumes. The command runs off the host's thread, SIGKILL terminates it whatever it is doing, and it reaches the VFS only by message (the host serves those frames). Worker platform executables preserve native-package protocols such as Landlock without replacing their JavaScript packages or coupling their implementations to node:child_process; xdg-open sends one VFS text file to the page's read-only viewer, so desktop-opener actions such as Open configuration file show the file in the page; ordinary commands use the package's evaluator and coreutils command table. The grammar is @yarnpkg/parsers' parseShell, while execSync/fork still refuse because they need a real process.
  • lib/client.js (page half) — startup has two independent stages. chooseWorkerHostSource({ image?, fixtureManifest? }) optionally owns the boot barrier and fixture manifest: without preview-fixture it waits at the source chooser, while a valid query selects directly; either path returns ordered overlays. connectWorkerHost(worker, { image?, overlays? }) remains the public base-runtime connector; callers that skip the chooser get an empty overlay list. apps/web invokes both and supplies its statically bundled Worker. The opening init frame carries the base and ordered overlay URLs, the boot payload delivers the structured index-injection table, and applyIndexInjections executes it before the shell entry runs. Script preload rows are advisory and skipped because /plugins resources resolve only through the tunnel; loadBundle fetches each combo on first demand, embeds its tunnel-only source map as a Base64 data URL, and executes the script as a Blob. Before the shell entry runs, the page reads GET /__assets__, which the tunnel answers with an uncompressed tar of every package asset in the image, and supplies resolveAsset, which maps each plugins/<package>/assets/<path> reference a bundle passes to require.asset to a Blob URL of those bytes. The worker's unsolicited view-text frame opens the page's modal text viewer. The tunnel also exposes fetch-shaped transport, the independent file-upload carrier, and the API client. Request frames preserve Blob bodies through structured clone and transfer ReadableStream<Uint8Array> ownership. The Host Worker streams both forms into the route, so neither browser thread creates a complete byte array for a generic-file upload. A Remote stream's uplink crosses as stream-uplink-item / stream-uplink-end frames into an unbounded TunnelUplink that drops items arriving after end: the page and its worker are one trusted same-origin boundary, so the Host inbox cap does not apply.

Callbacks and timers registered under an explicit root context retain its empty stores, so they cannot inherit an unrelated transaction.

Acceptance lives in apps/web/tests/preview-boot.e2e.ts, which serves the real built pages and drives the pre-boot chooser plus Worker activation in headless Chromium. The empty selection exercises first-run startup. The vfs-example overlay supplies ordinary workspace files and plaintext persistence artifacts for cold Workspace/Session discovery, tool presentation, subagent navigation, and history paging without a model request. The showcase mixes empty, whitespace-only, and non-empty reasoning with a long multiline command and a parent-path file link; these are authored fixtures, not live model output. A second root Session, preview-turn-spacing, carries three multi-step Turns whose bodies compare the tightened process-row spacing with the reply clearance. The packer prepares current-generation successors from committed historical logs in Node; those source logs remain byte-identical in the overlay. The fixture generator owns the acceptance test's current projection cache. The chooser reserves WebFS as a separate user-authorized source; that provider does not read the built-in fixture.

The built-bundle import sweep checks bare Node imports after the library build. Its Dockkit exception accepts Node’s unknown-.css-extension refusal for any stylesheet rather than one pinned path; another extension, error code, or message, and an unexpectedly successful exempt import, remain errors.


Model Experience

None, as this package only hosts the tree in a browser worker and answers its node:* calls; every model-facing registration belongs to the plugins it boots.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • The worker composition writes plaintext session logs (compression: 'none' in the image's home patch; ConfigEditor rejects edits to that row's config): it carries no Zstandard codec, so exported logs are .jsonl, never .jsonl.zstd.
  • node:dns/promises, node:vm, node:net, node:sqlite, node:worker_threads are structural stubs: every call reports its refusal on the console and throws with Node's ERR_FEATURE_UNAVAILABLE_ON_PLATFORM code. Rows needing native DNS, a real process, or realm isolation cannot run here.
  • node:assert/strict supports truthiness assertions only: for falsy values, the callable default and ok throw ERR_ASSERTION with the supplied message or generic text, or rethrow a supplied Error unchanged. Other assertion APIs are absent.
  • Native TLS listeners require a Node Host: the worker's node:https.createServer rejects activation rather than substituting plaintext. The preview's internal HTTP tunnel does not bind sockets; browser-facing HTTPS belongs to its deployment server.
  • Desktop product telemetry is unavailable: got.post reports an explicit worker-host refusal. Got and its Node HTTP dependencies are excluded from the browser image; Desktop reporting remains disabled in the preview composition.
  • Host package commands are unavailable: execa reports an explicit worker-host refusal; the preview cannot run pnpm, install plugins, or install native dependencies.
  • Pi agent execution is unavailable: the @earendil-works/pi-coding-agent replacement permits metadata imports and rejects SDK calls with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM. The image excludes its native CLI and terminal UI dependencies. Native DSH retains the full OAR runtime registry.
  • pi-ai model requests are unavailable: the replacement exposes a fixed subset of installed provider identifiers with no models, allowing the adapter to register configuration metadata. Its identifiers must stay valid when pi-ai is upgraded; inference entry points report an explicit worker-host refusal.
  • PTC Node programs are unavailable: the process shim exposes /dsh/bin/node as its executable identity so the provider can activate, but the Worker has neither a Node executable nor stripTypeScriptTypes. Program execution fails before launching a child.
  • Office conversion requires a Node Host: the LibreOffice kit replacement rejects converter creation with unavailable. The browser image excludes the kit and its engine dependencies; the Office preview reports that conversion is unavailable.
  • HMR replaces no modules: the Worker's ES module cache stays empty, so a module file changed in the VFS keeps its loaded code while HMR applies the profile change. The preview image changes no module files: package operations fail with the execa refusal.
  • The Node internal modules answer only their HMR-replaced members: the CommonJS loader, package reader, modules binding, and getOrCreateModuleJob refuse when called, and the Worker's own require does not route through the CommonJS loader, so replacing Module._resolveFilename or Module._load does not change Worker resolution.
  • CommonJS require('stream') returns the module namespace, not Node's Stream constructor: code that passes it to util.inherits fails at load.
  • A lazily supplied file has no synchronous read before its archive arrives: a consumer must read it asynchronously first; the Host's on-demand Client chunk route does.
  • Filesystem watchers observe only the mounted VFS: image seeding is silent and the VFS has no symlinks or external writers. persistent, ref(), and unref() preserve the Node API but cannot control a dedicated Worker's lifetime because browsers expose no ref-counted event loop.
  • Worker confinement is a VFS boundary, not kernel Landlock: read-only and workspace-write run the unchanged @deepseek-ai/node-addon-system/landlock-run JavaScript and launcher argv, but the process layer implements the logical landlock-run executable and enforces its grants on every shell filesystem request. full therefore covers the Worker command table and mounted VFS only; it does not claim arbitrary native-process execution or Linux kernel isolation.
  • The worker bundle pins a path inside @yarnpkg/parsers — the build resolves the package's own lib/shell.js instead of its root, whose barrel also re-exports the Syml parser and so drags js-yaml into a bundle that never parses that format (around 175 kB, plus its module body at worker start). The path is derived from the package manifest, so a layout change fails the build rather than reinstating the barrel; upgrading the dependency means re-checking that the shell parser still lives there.
  • The shell is not bash: no loops, functions, case, job control, or process substitution — the grammar stops at pipelines, &&/||, subshells, groups, redirections, and expansion. & runs its command to completion in place, sed accepts only substitution scripts, patterns are JavaScript regular expressions, and the command table holds coreutils only (no git, no network tools).
  • A shell process has no synchronous filesystem: it reads and writes the host's VFS by message, because blocking on a reply would need SharedArrayBuffer, which requires a cross-origin isolation GitHub Pages cannot grant. Directory-walking commands therefore cost one round trip per entry, and two concurrent commands can interleave their writes.
  • Transport, worker-host, and page-half coverage needs a browser-grade harness — the per-file coverage gate is unmet for those modules; unit specs cover storage, ALS, the transform, and the stub contracts.

Dev Note

Working context for maintainers — click to expand

None.