Replies: 5 comments
|
不用等修复,同一份配置目录(同一 DSH_HOME / 同一 profile)不要并发启动多个实例:
根因是每次启动都会把组合配置写回配置文件,写入用的是固定同名的临时文件再加重命名,进程之间没有锁;两个进程同时启动就会互相覆盖同一个临时文件并竞争重命名,偶发报加载失败。等官方改成带锁或随机后缀的原子写之后,并发启动才安全。 English: Don't start two instances on the same DSH_HOME/profile at once; serialize the startup phase (flock or wait until the first is fully up), and since the config file stays intact after a failed boot (tmp-file plus rename never leaves a partial file), just retry once. |
|
Thanks for the workaround — the serialization advice matches what I ended up doing (flock around just the boot window; no recurrence in four days of scheduled concurrent runs). That said, I don't think the write path you described is the one that runs at boot. There are two writers in the published package:
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG); // PROFILE_ROOT_FILENAME === "cordis.yml"No temp file, no rename, no lock, executed unconditionally on every boot. This is the boot path, and it is what the report is about. The observable difference is the inode: a rename replaces it, an in-place write keeps it. Measured across a single boot on 0.1.0-rc.6: Same inode, same size, mtime advanced — and it has been the same inode across a week of daily scheduled boots. So the hazard is not two processes racing on a shared Two consequences:
if (!existsSync(patchPath)) writeFileSync(patchPath, PROFILE_PATCH_TEMPLATE);
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE);Guarding Checked against 0.1.0-rc.7 (published 2026-08-17): Minor and separate: the rename retry in (1) treats only |
|
对,启动那条路我说错了。前面写的 tmp + rename 是 include 写回,不是 boot。 boot 走的是 用户侧做法不用改:同一 profile 还是只把启动阶段串行(flock 包住 boot 窗口就够),失败直接重试。官方那边更干净的修法是像旁边的 patch / workspace 一样,文件已经在就不写;内容本来就是编译期常量。 English: Agreed — the boot writer is the unconditional writeFileSync in prepareProfile, not the tmp+rename path. Serialization still works; skipping the rewrite when cordis.yml already exists would close the window without a lock. |
|
Thanks for going back and checking — that matches what I found, and your fix shape is better than a lock. Operational data since the report, in case it is useful for prioritising: the flock-around-boot workaround has now covered 207 headless launches (195 benchmark runs plus 12 scheduled daily inspections) across seven days, with zero On the fix: skipping the write when Writing it the way the include path already does would avoid having to choose:
That is atomic for the rare upgrade case and a no-op for every ordinary boot, so concurrent launches never observe a partial document, with no lock and no staleness. It also means the two writers in the profile directory work the same way, which is one less thing to know. Happy to open a PR for that shape if it is acceptable — it is a small change and I have a reproduction environment that exercises the boot path a few hundred times a week. |
|
Correction to my last comment: I offered to open a PR without checking Branch: What it does. Why compare rather than write-if-absent. This is the part I would have got wrong if I had implemented your suggestion literally. A sync sibling rather than Verification, on Linux with Node 22.22.1:
It includes an Agent Note under Nine files, +300/−9, of which the Agent Note and tests are most of it; the behavioural change is about fifteen lines. No urgency from our side — the flock workaround has now held for 207 headless launches over seven days. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Every launch rewrites the profile's
cordis.ymlin place (O_TRUNC+ write, same inode) even when the content is byte-identical. The file is therefore zero-length for a brief window on each boot. A second process starting in that window reads an empty document, fails the top-level-array check, and dies before the agent exists.This makes
dsh --profile headlessunsafe to schedule concurrently against oneDSH_HOME.Environment
@deepseek-ai/dsh0.1.0-rc.6 (installed from npm)DSH_HOMEWhat happens
Launching two or more headless runs at the same moment against the same profile:
one of them intermittently exits non-zero at boot with:
A closely related variant appears when the overlay is read during an unrelated editor save, because that read is not atomic either:
Observed roughly 2 failures across ~14 concurrent launches — intermittent, as a race would be. The profile files are intact afterwards, which is what made this confusing to diagnose.
Evidence
The rewrite is in place, not a replacement.
cordis.ymlkeeps its inode and size across a boot whilemtimeadvances, so it is not being written to a temp file and renamed:The rewrite truncates first.
inotifyon the file during one boot:Two
MODIFYevents between oneOPENand itsCLOSE_WRITEis theO_TRUNC-then-write signature (writeFileSync). The finalOPEN10 ms later is another process reading the same file.The zero-length window is sub-millisecond: polling
statat 1 ms for 25 s (23,693 samples) never observed a size other than 223, so the window is only reachable by a reader that happens to open inside it — which is exactly what concurrent boots do.Content is unchanged by the rewrite; the file is the stock
[]root written at profile initialization.Reproduction
dsh --profile headless "reply OK"DSH_HOME:Intermittent — expect to repeat a few rounds. Adding unrelated load lengthens the boot and widens the window.
Impact
Fail-fast rather than fail-open: the process dies during loader init, before any tool runs, so nothing executes unapproved. The cost is reliability, not safety — a scheduled run is silently skipped unless the scheduler inspects exit codes, and the error text points at a config file that is valid by the time anyone looks at it.
Suggested fix
Write the profile root and any launcher-managed config through a temp file in the same directory followed by
rename(2), so a concurrent reader sees either the old file or the new one. Skipping the write entirely when the content is unchanged would also remove the window for the common case, though the atomic write is the more complete fix since it also protects readers during a genuine content change.The overlay read path deserves the same treatment from the other side:
cordis.patch.ymlis user-edited, and most editors save by truncating, so a boot racing a save hits the same class of failure.Workaround
Serializing only the boot phase is enough, since the rewrite happens within the first moments of startup:
With this in place, the concurrent runs that previously lost one launch out of three complete reliably.
All reactions