DSH plugin: prepare:dsh fails on a removed fs-ext assert
Building the desktop app from source on 0.1.5-rc.2, prepare:dsh always aborts at the bundled-runtime payload smoke: Cannot find module 'fs-ext', after which electron-builder finishes with ENOENT …resources\dsh\desktop-runtime.json — and fs-ext is no longer a dependency of any package, so this assertion cannot pass on any platform or any machine (#6372). The root cause is that checkFsExt() in the fixture became a check that can never succeed, and the reason it could sit there indefinitely — producing the same error on all three consecutive runs — is that the summary line it prints has no consumer at all: deleting any check* and having it pass are indistinguishable in a green run. This article goes "triage first → the two mechanisms (stale assertion + nobody reads the summary) → the three fixes (delete / upgrade to a contract / add the missing cover) → related cleanup", each step with a paste-ready patch and measured results; the easiest trap sits in the title: do not delete koffi along the way.
Triage first: this is a stale assertion, not a missing dependency
Seeing Cannot find module 'fs-ext', the reflex is "a package did not get installed", but this error is the exact opposite: the check is looking for a package that has already been deleted. The two require opposite responses, so pin the scope down with three commands first.
# 1. Does any manifest in the whole tree still declare fs-ext?
git grep -n '"fs-ext"' -- '*package.json' || echo 'NO manifest declares fs-ext'
# 2. Is there an entry for it in the lockfile?
grep -n 'fs-ext' pnpm-lock.yaml || echo 'NO lockfile entry'
# 3. Which files still mention the name at all?
git grep -n 'fs-ext' -- ':!pnpm-lock.yaml'
The measured output is: the first command has zero matches, the second only matches fs-extra, and the third hits just four leftovers (see the table below). That leaves exactly one possibility standing.
| Criterion | "Stale assertion" (this case) | "Missing dependency" (the common misreading) |
|---|---|---|
Declared in package.json | Zero declarations in the whole tree | Declared but not installed |
Entry in pnpm-lock.yaml | None | Present, or missing and failing frozen |
| Does installing it back help | No — --prod --frozen-lockfile still cannot produce it | Yes |
| Reproduces on a fresh temp project + fresh store | Always (three times in a row on the reporter's machine) | Depends on the cache |
| The real fix | Delete the assertion | Add the dependency / fix the lockfile |
On Windows 10 x64 with Node v24.17.0, the reporter ran prepare:runtime → prepare:packages → prepare:dsh three times with a fresh temp project and a fresh pnpm store, and all three runs broke on the same line (#6372). The "just try another machine" path is closed before you start.
Mechanism one: checkFsExt() cannot succeed on any platform
In one sentence: the fixture installs its payload with pnpm install --prod --frozen-lockfile, no manifest or lockfile entry can produce fs-ext, therefore requireRuntime('fs-ext') always throws, checkFsExt() always fails, and prepare-dsh.ts treats that as a hard failure.
The four steps of the collapse
checkFsExt() apps/desktop/tests/fixtures/runtime-payload-smoke.mjs:69
└─ requireRuntime('fs-ext') resolves inside the payload, always Cannot find module
└─ the exception bubbles to the fixture top-level try :124
└─ the child process exits non-zero
└─ the prepare-dsh.ts callback throws terminates the whole script
└─ resources/dsh is never produced
└─ electron-builder ENOENT …\resources\dsh\desktop-runtime.json
(verifyDesktopRuntime / afterPack fails while finishing)
The key is the second line: requireRuntime resolves against the installed payload, not the source tree. The payload's dependency set is decided by pnpm install --prod --frozen-lockfile, and a name that is in neither any package.json nor pnpm-lock.yaml can never appear in the result. So this is not "one machine is missing a package" — it is a structurally unsatisfiable assertion.
What replaced it
The removal of fs-ext was not a loss; it was a documented migration. The session write lease (the session.lock layer) now runs on a prebuilt Node-API system addon:
// packages/session/session-persistence-jsonl/src/lease.ts:34
import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock'
The migration is recorded in .agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md: prebuilt Node-API system addon replaces install-time-compiled NAN fs-ext. After the migration, the only code left in the tree that still references fs-ext / seekSync is this fixture itself.
Leftover inventory (does not break the build, but is semantically dead code)
| Location | Content | Nature |
|---|---|---|
apps/desktop/tests/fixtures/runtime-payload-smoke.mjs:69/124/137 | checkFsExt(), its call site and its summary key | This is the incident itself |
apps/desktop/scripts/runtime-file-policy.ts:26/30 | The fs-ext/build/** exclusion rule | Covered by a spec; change the rule and the spec together |
apps/desktop/src/project-manager.ts:109 | allowBuilds: fs-ext: true | Points at a package that cannot be installed |
vitest.config.ts:98 | One stale comment | Worth taking along with the cleanup |
Note the third column's classification — it is what many people get wrong. The two runtime-file-policy.ts rules are not inert leftovers: apps/desktop/tests/runtime-file-policy.spec.ts tests them with 7 vectors (including the nested-path positive case on line 73 and the retention vectors on lines 20–24 and 38). Deleting the rules alone turns the spec red, so they must travel as one change with the spec — and must not be mixed into this smoke fix. That ordering is agreed by both the reporter and the reviewer.
Mechanism two: why it stayed hidden — the summary line is a report nobody reads
In one sentence: the fixture computes one summary line {node, platform, arch, fsExt, koffi, sharp, html, pty} and prints it to stdout, prepare-dsh.ts:145 forwards it verbatim and never parses it, and nothing in the tree consumes it — so "a check was deleted" and "a check passed" look identical in a green run.
This one deserves its own section more than the first, because it explains why an assertion that never once succeeded survived to a release.
// apps/desktop/scripts/prepare-dsh.ts, around line 145 (before the fix)
else { process.stdout.write(stdout); accept() } // stdout written straight through, that JSON line never parsed
The reviewer confirmed with git grep that those keys (fsExt, koffi, …) have no reader anywhere in the tree. The fsExt key could therefore keep reporting true indefinitely with nobody objecting. Put even more bluntly:
Before the fix, deleting any single
check*call from the fixture was completely indistinguishable from it actually passing in a green run.
The failure path is fine; the missing half is the success path
To be fair: the failure path is not the problem.
prepare-dsh.ts:144rejects on any non-zero exit and wraps the child'sstderrinto the error message — break the patch and it will shout.DESKTOP_HOST_RUNTIME_FILESperforms existence checks andthrows, so a missing file is not silent either.
What is genuinely unverified is success: a green prepare:dsh currently asserts only that the child's exit code is 0, not that those six checks actually ran. Parsing the summary line takes one line of code and turns "someone deletes a check next time" from silent into a visible failure. That is precisely the part this fix should do while it is here — it is the property that would have caught this bug in the first place.
Do not delete the wrong thing: koffi must stay, and the reason is the reverse of the migration note
In one sentence: that migration note reads like a removal list, but it actually keeps koffi — the rejected alternative was "using koffi for POSIX calls", and Windows locking still runs on the existing koffi semaphore today.
This is the point the article most wants to stress. The original report contains the sentence "the remaining koffi / sharp / turndown+gfm / node-pty assertions target modules that really do exist in the package set; keep them unchanged" — the reviewer went back, checked, and explicitly endorsed it, for two reasons:
- The migration note's rejected-alternatives table lists "using koffi for POSIX calls" as the rejected alternative to fs-ext. In other words, the accepted direction is the prebuilt Node-API addon, and "replace the POSIX half with koffi" was the option that was turned down. Deleting
checkKoffi()as an old approach reads "rejected" as "adopted". - Windows locking is still carried by the koffi semaphore, an adopted conclusion of the note. Delete it and you delete a safeguard that really exists on Windows.
Add one hard number: koffi is declared by 6 manifests in the tree — fs-local, directory-picker-native, sandbox-windows-acl, session-persistence-jsonl, subprocess-local, win32-process. So "delete koffi" is not dead-code cleanup; it is trading a fixed error for a brand-new one.
DSH plugin: the three fixes — drop the assert, build a contract, add coverage
Fix one: the minimal deletion (the original report's single-file patch, +2/−19)
If your only goal is "make 0.1.5-rc.2 produce a package", this step is enough: delete the no-longer-satisfiable checkFsExt() together with its call site, its summary key and the now-unused node:fs imports.
--- a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs
+++ b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs
@@ -1,7 +1,7 @@
/** Exercise filtered Desktop native and HTML dependencies under its bundled Node. */
import assert from 'node:assert/strict'
-import { closeSync, mkdtempSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs'
+import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
@@ -64,22 +64,6 @@ async function checkPty() {
}
}
-/** fs-ext implements seek on Windows through SetFilePointerEx and on POSIX through lseek. */
-function checkFsExt() {
- const fsExt = requireRuntime('fs-ext')
- const file = join(scratch, 'seek.txt')
- writeFileSync(file, 'abcdef', { flag: 'wx', mode: 0o600 })
- const fd = openSync(file, 'r')
- try {
- assert.equal(fsExt.seekSync(fd, 2, fsExt.constants.SEEK_SET), 2)
- const bytes = Buffer.alloc(4)
- assert.equal(readSync(fd, bytes, 0, bytes.length, null), 4)
- assert.equal(bytes.toString(), 'cdef')
- } finally {
- closeSync(fd)
- }
-}
-
/** Resolve one system function through Koffi's packaged native module. */
function checkKoffi() {
const koffi = requireRuntime('koffi')
@@ -121,7 +105,6 @@ function checkHtml() {
}
try {
- checkFsExt()
checkKoffi()
await checkSharp()
checkHtml()
@@ -134,5 +117,5 @@ try {
process.once('beforeExit', () => {
console.log(JSON.stringify({ node: process.versions.node, platform: process.platform, arch: process.arch,
- fsExt: true, koffi: true, sharp: true, html: true, pty: true }))
+ koffi: true, sharp: true, html: true, pty: true }))
})
Do and don't: this step alone makes prepare:dsh pass, but it does not fix mechanism two (the summary line still has no reader). So the next two steps are treated by the reporter and the reviewer as "part of the same fix", not follow-up work — because mechanism two is the property that let this bug survive.
Fix two: upgrade "print a line" into a contract (recommended in the same batch as fix one)
In one sentence: give the summary line a prefix, make prepare-dsh.ts parse it and validate set equality in both directions; and deliberately convert a parse error into reject, because throwing from inside the execFile callback would leave the promise pending forever and hang prepare outright.
Fixture side: add the prefix + swap the check + reshape the summary
--- a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs
+++ b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs
@@ -17,6 +17,9 @@ assert.equal(process.arch, descriptor.arch)
const requireRuntime = createRequire(join(root, 'package.json'))
const scratch = mkdtempSync(join(tmpdir(), 'dsh-runtime-payload-'))
+/** Prefix of the summary line prepare-dsh.ts parses; keep both copies in sync. */
+const SUMMARY_PREFIX = 'desktop-runtime-payload-smoke '
+
/** Spawn only a fixed Node program and await the terminal's drained exit event. */
async function checkPty() {
const pty = requireRuntime('node-pty')
@@ -64,17 +67,19 @@ async function checkPty() {
}
}
-/** fs-ext implements seek on Windows through SetFilePointerEx and on POSIX through lseek. */
-function checkFsExt() {
- const fsExt = requireRuntime('fs-ext')
- const file = join(scratch, 'seek.txt')
- writeFileSync(file, 'abcdef', { flag: 'wx', mode: 0o600 })
+/**
+ * The prebuilt Node-API system addon replaced fs-ext on POSIX. Windows keeps its
+ * koffi semaphore, so only the `./flock` subpath has to resolve there.
+ */
+async function checkSystemFlock() {
+ const flock = requireRuntime('@deepseek-ai/node-addon-system/flock')
+ assert.equal(typeof flock.tryLockExclusive, 'function')
+ if (process.platform === 'win32') return
+ const file = join(scratch, 'flock.txt')
+ writeFileSync(file, '', { flag: 'wx', mode: 0o600 })
const fd = openSync(file, 'r')
try {
- assert.equal(fsExt.seekSync(fd, 2, fsExt.constants.SEEK_SET), 2)
- const bytes = Buffer.alloc(4)
- assert.equal(readSync(fd, bytes, 0, bytes.length, null), 4)
- assert.equal(bytes.toString(), 'cdef')
+ await flock.tryLockExclusive(fd)
} finally {
closeSync(fd)
}
@@ -121,7 +126,7 @@ function checkHtml() {
}
try {
- checkFsExt()
+ await checkSystemFlock()
checkKoffi()
await checkSharp()
checkHtml()
@@ -132,7 +137,9 @@ try {
}
// Natural event-loop drain includes node-pty's worker and console-list helper teardown.
+// prepare-dsh.ts asserts this summary, so a dropped check cannot pass as a green run.
process.once('beforeExit', () => {
- console.log(JSON.stringify({ node: process.versions.node, platform: process.platform, arch: process.arch,
- fsExt: true, koffi: true, sharp: true, html: true, pty: true }))
+ console.log(`${SUMMARY_PREFIX}${JSON.stringify({ runtime: { node: process.versions.node,
+ platform: process.platform, arch: process.arch },
+ checks: { systemFlock: true, koffi: true, sharp: true, html: true, pty: true } })}`)
})
Note that two things happen here: the fs-ext assertion is deleted (fix one) and the cover that was genuinely missing is added back (fix three, next section). The summary shape moves from flat keys to a nested {runtime, checks} only so that "which entries are checks" becomes explicitly enumerable in code.
Script side: parse + set equality + parse error becomes reject
--- a/apps/desktop/scripts/prepare-dsh.ts
+++ b/apps/desktop/scripts/prepare-dsh.ts
@@ -38,6 +38,11 @@ const PACKAGE_SET_ROOT = BUILD_PATHS.packageSet
const NODE = join(RUNTIME_ROOT, 'node', process.platform === 'win32' ? 'node.exe' : 'node')
const PNPM = join(RUNTIME_ROOT, 'pnpm', 'bin', 'pnpm.mjs')
+/** Must match the summary prefix in tests/fixtures/runtime-payload-smoke.mjs. */
+const PAYLOAD_SMOKE_SUMMARY_PREFIX = 'desktop-runtime-payload-smoke '
+/** Native checks the bundled payload must report; a silent removal must not look like a pass. */
+const EXPECTED_PAYLOAD_SMOKE_CHECKS: readonly string[] = ['systemFlock', 'koffi', 'sharp', 'html', 'pty']
+
function manifestVersion(path: string, subject: string): string {
const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
if (typeof manifest.version !== 'string') throw new Error(`desktop runtime: ${subject} has no version`)
@@ -100,6 +105,17 @@ function runPnpm(args: readonly string[]): Promise<void> {
})
}
+function verifyPayloadSmokeSummary(stdout: string): void {
+ const line = stdout.split(/\r?\n/u).filter(entry => entry.startsWith(PAYLOAD_SMOKE_SUMMARY_PREFIX)).at(-1)
+ if (line === undefined) throw new Error('desktop runtime: payload smoke printed no summary line')
+ const summary = JSON.parse(line.slice(PAYLOAD_SMOKE_SUMMARY_PREFIX.length)) as { checks?: Record<string, unknown> }
+ const checks = Object.entries(summary.checks ?? {})
+ if (checks.length !== EXPECTED_PAYLOAD_SMOKE_CHECKS.length
+ || checks.some(([name, value]) => value !== true || !EXPECTED_PAYLOAD_SMOKE_CHECKS.includes(name))) {
+ throw new Error(`desktop runtime: payload smoke reported ${JSON.stringify(summary.checks ?? null)}`)
+ }
+}
+
async function main(): Promise<void> {
rmSync(DSH_OUTPUT_ROOT, { recursive: true, force: true })
rmSync(PNPM_BUILD_STATE, { recursive: true, force: true })
@@ -142,7 +158,15 @@ async function main(): Promise<void> {
execFile(NODE, [join(APP_ROOT, 'tests/fixtures/runtime-payload-smoke.mjs'), DSH_OUTPUT_ROOT],
{ timeout: 120_000, env: { ...process.env, NODE_OPTIONS: '' } }, (error, stdout, stderr) => {
if (error !== null) reject(new Error(`desktop native payload smoke failed: ${stderr}`, { cause: error }))
- else { process.stdout.write(stdout); accept() }
+ else {
+ try {
+ verifyPayloadSmokeSummary(stdout)
+ process.stdout.write(stdout)
+ accept()
+ } catch (cause) {
+ reject(cause instanceof Error ? cause : new Error(String(cause)))
+ }
+ }
})
})
await smokeDesktopRuntime(DSH_OUTPUT_ROOT, NODE, descriptor)
Two deliberate design decisions
The reporter says outright that these two are "expected to be questioned", so they are worth explaining one by one:
- Set equality is used, not "the expected keys exist". The check tests both
checks.length === EXPECTED.lengthand that every reported entry hasvalue === trueand a name in the expected table. Why not one-directional? Because checking only "everything expected is present" misses the opposite direction: a new check added to the fixture while the expected table is not updated. Set equality closes both directions. - A parse error is deliberately converted into
reject. If you simplythrowinside theexecFilecallback, that exception escapes the callback, the promise neitheraccepts norrejects, andpreparestays pending forever and hangs. An explicitrejectis what makes it "fail loudly" instead of "hang quietly". This is the easiest line in the whole block to get wrong.
Fix three: add the payload cover that is genuinely missing (checkSystemFlock)
In one sentence: the @deepseek-ai/node-addon-system that this migration added has zero coverage on the payload side, and that is exactly what this smoke exists for — add a checkSystemFlock() that really locks once on POSIX and on Windows only asserts that the subpath resolves.
The reviewer lists this as the second point "about the payload rather than the fixture", and the logic is clean:
- The lease imports
@deepseek-ai/node-addon-system/flock(lease.ts:34, verified), and that family carries its owntest/flock.test.jsin the source tree — that is the source-tree trust model, the mode the migration note calls "already covered before". - There is no counterpart on the payload side at all: the fixture used to assert only pty, koffi, sharp and html; not one line
requirednode-addon-systemor resolved its platform package.
And the failure mode this fixture guards is exactly "the native layout produced by install differs from the source tree", while the platform-package contract is its youngest and least-exercised part. Why can't "packaging" itself cover it? Because no prebuilt binary can be universal across OS, CPU, libc and Node ABI — that is precisely why the family is split per platform. And a missing platform package surfaces the moment you require that subpath, so the payload check is extremely cheap and needs no new fixture machinery.
The cross-platform semantics must be written down
The behaviour of checkSystemFlock() is asymmetric across the two platforms, and if that is not commented clearly it is easy for a later reader to misread it as "Windows verified the platform package too":
async function checkSystemFlock() {
const flock = requireRuntime('@deepseek-ai/node-addon-system/flock')
assert.equal(typeof flock.tryLockExclusive, 'function')
if (process.platform === 'win32') return // ← on Windows it stops here
const file = join(scratch, 'flock.txt')
writeFileSync(file, '', { flag: 'wx', mode: 0o600 })
const fd = openSync(file, 'r')
try {
await flock.tryLockExclusive(fd) // ← only POSIX really locks
} finally {
closeSync(fd)
}
}
The reason lives in the package declaration: native/system/packages/entry/package.json declares optionalDependencies only for darwin-* and linux-*, with no win32-*; flock.js is lazily loaded by design; and on Windows calling tryLockExclusive rejects with ERR_FLOCK_UNSUPPORTED_PLATFORM. Therefore:
- Linux / macOS: resolve the platform package plus a real
tryLockExclusive(fd)— this is the cover the payload had been missing all along. - Windows: only assert that the
./flocksubpath resolves inside the payload; the real Windows lock is the koffi semaphore, already covered bycheckKoffi(); thesystem.nodebytes are only ever exercised on the POSIX lane.
DSH plugin: cleanup, end-to-end measurement and the checklist
Related cleanup: policy rules and allowBuilds travel with the spec, not in this PR
In one sentence: the fs-ext rules in runtime-file-policy.ts are pinned by 7 test vectors, and the fs-ext: true entry in allowBuilds is the same batch of dead declarations — they should travel together as an independent change, not be stuffed into the payload smoke fix.
The original report calls these two "inert leftovers"; the reviewer points out that wording is inaccurate and gives the vector locations:
| Location | Content | Handling requirement |
|---|---|---|
runtime-file-policy.spec.ts:20–24, :38 | Retention vectors for the fs-ext rule | Must change in the same batch as the rule |
runtime-file-policy.spec.ts:73 | Nested-path positive case | Same; deleting the rule turns it red |
runtime-file-policy.ts:26/30 | The fs-ext/build/** exclusion rule | Same batch as the spec |
project-manager.ts:109 | allowBuilds: fs-ext: true | Best done with the policy cleanup |
vitest.config.ts:98 | Stale comment | No objection to taking it along |
Why does allowBuilds have to move too? Because it is itself a deliberately narrowed declaration set, listing each intentional install-time compilation one by one (node-pty, koffi, fs-ext). An entry pointing at a package that cannot be installed is internally consistent but no longer meaningful — and this kind of stale entry is exactly what made this bug expensive.
End-to-end measurement: the full chain on Windows 10 x64
In one sentence: after the two-file patch, prepare:packages and prepare:dsh both exit 0, the new validation consumes the summary line, the later smokeDesktopRuntime and verifyDesktopRuntime also pass, and resources/dsh comes out at 114.1 MB / 188 top-level packages.
The reporter eventually got the patch through, and gives the summary line the new validation in prepare-dsh.ts actually read:
desktop-runtime-payload-smoke {"runtime":{"node":"24.17.0","platform":"win32","arch":"x64"},"checks":{"systemFlock":true,"koffi":true,"sharp":true,"html":true,"pty":true}}
The gates that follow the fixture in the same process passed too: smokeDesktopRuntime (start the Host, mount an external plugin, fetch dsh-app://app/) and the final verifyDesktopRuntime — meaning that on this target machine, the recorded manifest and the tree actually on disk agree.
Three side notes, equally worth remembering:
- Windows really can resolve the
./flocksubpath inside the payload — that is the entire content of the new check's Windows assertion; thesystem.nodebytes are still only exercised on the POSIX lane, because the entry package has nowin32-*optionalDependency. Tightening the check in the future has to solve that premise first. prepare:runtimeexits with 13 on this machine, stderrDetected unsettled top-level await(insideprepare-runtime.ts), while the archive itself validates. This is an existing local quirk, unrelated to the patch: the runtime tree it had already written is complete (node v24.17.0,pnpm 11.7.0), so simply continue fromprepare:packages+prepare:dsh.- The downstream packaging path was verified too: electron-builder copies
resources/dsh/node_modules(node-pty's ConPTY helper, ripgrep), theafterPackverifyDesktopRuntimepasses on the packaged tree, and the smoke on the extracted copy builds 241 junctions from the profile with the window coming up in about ten seconds. The reporter notes this part is his local unsigned route and not an upstream change — the complete upstream-facing delta is just the two files, the fixture andprepare-dsh.ts.
Troubleshooting checklist
- On
Cannot find module 'X', first ask "is X still a dependency at all". In this case the reflex (install it back) points the wrong way;git grep -- '*package.json'andpnpm-lock.yamlsettle it in two commands. - Do not get pulled toward "try another machine". The reporter ran a fresh temp project and a fresh store three times and broke at the same point all three times — a structural problem does not disappear because the environment is clean.
- Separate "the assertion cannot succeed" from "the assertion could fail but never ran". The first must be deleted, the second needs the fixture logic fixed; the handling differs.
- When a migration note reads like a checklist, verify the direction of every line. fs-ext was removed, koffi was kept; reading "rejected alternative" as "adopted" makes you delete the wrong thing.
- A summary line nobody reads means a check can vanish silently. As long as a check's result feeds no decision, it is running naked; wiring the result into a decision matters more than writing one more check.
- Validate with set equality, not "the expected keys exist". A one-directional check closes only one direction, and a new check added without registering it leaks out the other way.
- Never bare-
throwinside anexecFilecallback. It mustreject, otherwise the promise stays pending forever and hangs the whole prepare — harder to diagnose than a failure. - When changing a rule that test data depends on, find its spec first. Deleting the
fs-extrule inruntime-file-policy.tsalone turnsruntime-file-policy.spec.tsred, so it must go in the same batch. - Whitelists like
allowBuildsrot too. An entry pointing at an uninstallable package looks harmless and is the next person's trap. - Comment the semantics of platform-package checks. "Only resolve the subpath" on Windows and "really lock once" on POSIX are not equivalent coverage; spell it out so a later reader does not take a green Windows run as proof the platform package was verified.
The real value of this smoke is not "one more package verified" but turning "the native layout that install produces" into a readable, decidable contract. If your redistribution also embeds a DSH runtime, or you run prepare:dsh in CI, do two things while you are here: wire the summary line into the decision so that "someone deletes a check next time" becomes an explicit failure; and on the next native-dependency migration, confirm the direction of every name first — removed ones go, kept ones stay, and especially do not sweep away a dependency like koffi that still carries the Windows lock. With DSH Plugin Hub, plugin install/uninstall, update confirmation and system logs all live in one panel, which saves a few detours when you are debugging this kind of environment problem.

Source: Discussion #6372.
FAQ
prepare:dsh is the desktop-side script that builds resources/dsh (the Host runtime that gets packed into the installer) when you build locally. It belongs to the "build the desktop app from source / produce a local package" chain; a normal user installing DSH from an installer never runs it. You meet it in two situations: you package the monorepo yourself, or you maintain a redistribution that embeds a DSH runtime. For ordinary plugin installs, look at a different set of error codes in the log.
Not recommended, and it basically cannot work. No package.json in the whole tree declares fs-ext and there is no pnpm-lock.yaml entry for it, while the fixture installs its payload with pnpm install --prod --frozen-lockfile. Adding the package by hand still fails the --frozen-lockfile gate, and the next run against a fresh temp project and a fresh store fails again. The real fix is to delete the assertion that can no longer hold, not to invite a removed dependency back.
Because it rides a different trust chain. In the migration note, the source-tree side is covered by that family's own test/flock.test.js; this fixture guards the failure mode where "what install produces differs from the source tree", which means the payload side is exactly where it belongs, and previously there was not a single line of it. checkSystemFlock() added by fix three fills that hole.
Because the migration note reads like a removal list, so it is easy to sweep koffi away as an obsolete old approach. The opposite is true: the note's rejected-alternatives table lists "using koffi for POSIX calls" as the *rejected* alternative to fs-ext, meaning the accepted direction is the prebuilt Node-API addon; the note also has a section that explicitly keeps Windows locking on the existing koffi semaphore. koffi is declared by 6 manifests in the tree, so deleting it trades a fixed error for a brand-new one.
Probably not. On Windows the reporter measured prepare:runtime exiting with 13 and stderr Detected unsettled top-level await, an existing local quirk unrelated to payload smoke — the runtime tree it had already written is complete (node v24.17.0, pnpm 11.7.0), so you can continue straight from prepare:packages + prepare:dsh. Judge by whether those two scripts exit 0 and whether resources/dsh is finally produced.
Related Terms
- payload smoke
- Refers to `apps/desktop/tests/fixtures/runtime-payload-smoke.mjs`: inside the *installed payload* that ships in the installer, using the desktop's own bundled Node, it `require`s each native and HTML dependency and does a minimal functional call, to prove the installed native layout is complete and usable. The failure mode it guards is "what install produces differs from the source tree", not "does the source tree still contain this package".— https://github.com/deepseek-ai/deepseek-harness/discussions/6372
- fs-ext
- A NAN addon compiled at install time, historically used to provide seek primitives such as POSIX `lseek` / Windows `SetFilePointerEx` (this fixture once asserted `seekSync` with it). It was removed in an architecture migration: leases moved to a prebuilt Node-API system addon, and no manifest or lockfile entry in the tree declares it any more.— https://github.com/deepseek-ai/deepseek-harness/discussions/6372
- @deepseek-ai/node-addon-system/flock
- The flock subpath inside the prebuilt Node-API system addon family that replaced fs-ext. Because prebuilt binaries cannot be universal across OS / CPU / libc / Node ABI, the family is split into per-platform packages; the `entry` package declares optionalDependencies only for `darwin-*` and `linux-*`, `flock.js` is lazily loaded, so calling `tryLockExclusive` on Windows throws `ERR_FLOCK_UNSUPPORTED_PLATFORM` and Windows write locks are still carried by the koffi semaphore.— https://github.com/deepseek-ai/deepseek-harness/discussions/6372
- allowBuilds
- A narrow, whitelist-style declaration in `apps/desktop/src/project-manager.ts` listing which install-time build scripts are allowed to run — `node-pty`, `koffi`, `fs-ext` and so on. It is a deliberately narrowed set rather than a general allow-list; keeping an entry that points at a package that can no longer be installed is internally consistent but makes the next reader believe it is still effective.— https://github.com/deepseek-ai/deepseek-harness/discussions/6372