Repository navigation
Description
Environment
- OS: Windows 10 22H2 (19045), x64
bskCLI / daemon: 0.2.1 (daemon protocol 1.1),~/.local/bin/bsk.exe,BSK_HOME=~/.bsk- Browser extension: ext-v0.3.0 (reports protocol 1.3)
- Host: DeepSeek Harness Desktop. Shell commands are invoked with a fixed per-call timeout, stdout captured through a pipe (no TTY); on timeout the harness kills the command's whole process tree.
- Plugin involved downstream:
@wxg-prc-cpg/browser-skill-dsh-plugin0.2.1
Actual behavior
bsk daemon start never returns in that context. When the harness kills the command at its 120 s timeout, the daemon dies with it — even though bsk daemon start --help says "Start the daemon (auto-detaches unless --foreground)".
bsk status behaves the same way, and it starts a daemon as a side effect: run while no daemon was alive, it printed a complete status block (daemon version 0.2.1, pid 25900, uptime 0s) and then hung; the harness killed it 90 s later — after which that daemon never logged again either.
So a caller that merely checks status can silently remove a healthy daemon. In agent harnesses that is a footgun rather than an edge case, because status / doctor are the first commands an agent runs (#265 and #24 report the unbounded wait; this is the side-effect half of it). The result: an unrelated later browser_session start has to auto-spawn a daemon, hits #180 / #183, hangs forever, and leaves the orphaned Agent Window of #245.
Evidence (~/.bsk/daemon.log.2026-09-17, UTC; local = UTC+8)
Only lines that are actually in the log:
01:12:36 daemon lock acquired; ws server listening 127.0.0.1:52800; daemon ready pid=27604
01:12:36 browser connected id=<ext-instance>
01:12:41 periodic update check failed (github.com unreachable)
01:15:11 daemon ready pid=25900
01:15:11 browser connected id=<ext-instance>
01:17:04 daemon ready pid=18152
01:17:07 browser connected id=<ext-instance>
01:22:34 idle session stopped session=cgya
How to read it (inferences, marked as such):
27604was started by mybsk daemon start; the harness killed that command at its 120 s timeout and the daemon stopped logging at the same moment. Inferred: it died with its launcher.25900was started implicitly bybsk status(the command that only meant to report); the harness killed it at its 90 s timeout and it likewise never logged again. Inferred: same cause.18152was auto-spawned by the browser extension / plugin. Its parent exited normally, so it became an orphan — this is the only one that survived (still running hours later).cgyais the session created during a 40-minute hang ofbrowser_session start(see [Bug][dsh-plugin] browser_session start hangs forever on Windows - bsk child exits fine, but plugin close event never fires; its own 120s timeout never surfaces #180 / [Bug][dsh-plugin] start 未返回时残留无人认领的 Agent Window,DSH 侧任何路径都关不掉它 | unregistered Agent Window orphaned by a non-returning browser_session start #245); the daemon idle-reaped it after 5 minutes.
No daemon exit is logged (no shutdown line, no reason), which is what makes this hard to diagnose from the outside.
Expected behavior (suggested)
- Either detach for real, or refuse. If the spawned daemon cannot survive the launcher (
DETACHED_PROCESS/CREATE_NEW_PROCESS_GROUP, and on WindowsCREATE_BREAKAWAY_FROM_JOBso a job-object kill of the caller cannot take it down),daemon startshould fail fast with a message pointing atbsk daemon start --foreground(the pattern docs: clarify remote setup and persistent daemon startup #263 documents for persistent host tasks). The current middle ground — appears detached, then dies silently — is the worst case: the caller has no way to notice. daemon startshould be idempotent and return immediately when a healthy daemon is already running; today it blocks in agent contexts even though the daemon is up.- Read-only commands should not auto-start a daemon (
status,doctor,browsers), or should behave asBSK_AUTO_START=0when stdout is not a TTY. Starting a service as a side effect of a query is what makes agents kill daemons by accident. - Log a reason when the daemon exits (
parent-killed/signal/idle). Diagnosing this from timestamps alone cost me a while; a single line would have made it obvious.
Related
- CLI hangs forever when stdout is captured by a caller that waits for EOF (daemon inherits stdout pipe) #55 (closed, 0.1.9) — daemon inherits the caller's stdout pipe → EOF-waiters hang. The stdio half looks fixed (the daemon now writes
~/.bsk/daemon.log), but the process-lifetime half is not. - Windows: bsk daemon start auto-detached process exits immediately, extension cannot connect #218 — sibling failure mode on the same platform (auto-detached process exits immediately).
bsk doctorhangs indefinitely when no browser extension is connected (Windows, agent context) #265, fix: resolve CLI hanging issues in bsk status --json command #24 — unbounded waits indoctor/status --jsonwhen auto-start happens.- [Bug][dsh-plugin] browser_session start hangs forever on Windows - bsk child exits fine, but plugin close event never fires; its own 120s timeout never surfaces #180, fix(dsh-plugin): settle runner promises when the child never emits close #183, [Bug][dsh-plugin] start 未返回时残留无人认领的 Agent Window,DSH 侧任何路径都关不掉它 | unregistered Agent Window orphaned by a non-returning browser_session start #245 — what users see when the daemon disappears mid-handshake.
Workaround (verified on 0.2.1)
Let the extension/plugin start the daemon (its spawner exits normally, so the daemon survives as an orphan), or start it fully detached:
Start-Process -FilePath "$env:USERPROFILE\.local\bin\bsk.exe" -ArgumentList 'daemon','start' -WindowStyle HiddenDo not run bsk daemon start / bsk status from a shell tool that kills its process tree on timeout. Read-only liveness probing that never starts anything: bsk browsers, or the owner of TCP port 52800.
Activity
aron-intframe commentedon Sep 17, 2026
Reproduced the "never returns" half, and it explains the "dies with the launcher" half too. Cause: the auto-detached daemon inherits the launcher's stdout pipe handle, so the harness never sees EOF on the command; it waits until its timeout, and the tree kill it does at that point takes the daemon with it.
spawn_detached_aton Windows (crates/bsk-cli/src/daemon/start.rs) usesstd::process::CommandwithStdio::null()andDETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP. That only sets the child's own std handles to NUL. Rust'sCommandstill callsCreateProcessWwithbInheritHandles = TRUE(stdsys/process/windows.rs,inherit_handles: trueby default; theinherit_handles(false)extension is unstable, rust-lang/rust#146407), so every inheritable handle in the launcher is copied into the daemon. The launcher's stdout, when a harness gave it a pipe, is exactly such a handle. The daemon holds the write end for its whole life, and the harness reading to EOF blocks. No breakaway flag is set either, so a job-object ortaskkill /Ttree kill from the harness reaches the daemon (DETACHED_PROCESSchanges neither the parent PID nor job membership).Evidence: release MSVC binaries run under Wine (which applies the same CreateProcess handle-inheritance rules), harness simulated with Python
Popen(stdout=PIPE)+communicate(timeout=...), wineserver/services started beforehand so only handles passed on by the launcher count,BSK_AUTO_UPDATE=off,BSK_HOMEset to a scratch dir:Same on 0.3.0:
$ bsk daemon start -> NO stdout EOF after 30s; launcher bsk.exe already exited rc=0, holder = thebsk.exe daemon startchild. (wineserver shows up because it backs every Windows handle; the holder that matters is the daemon.) Note that in every hanging case the launcher had already exited with rc=0: the daemon came up anddaemon startitself returned within seconds. Only the pipe keeps the harness waiting, which matches your log (daemon ready at 01:12:36, command "hung" for 120 s, then the kill).Fix, stable Rust,
windows-sysis already a dependency (add theWin32_System_Consolefeature forGetStdHandle): clear the inherit bit on the launcher's own std handles before spawning. The child gets NUL for its stdio anyway, so nothing is lost:The complete version is
PROC_THREAD_ATTRIBUTE_HANDLE_LISTwith an empty list (also covers any other inheritable handle the launcher has open, e.g. a log file or socket), but that means a rawCreateProcessWcall, since std'sraw_attributeis unstable as well. For the lifetime half: addCREATE_BREAKAWAY_FROM_JOBto the creation flags, retry without it onERROR_ACCESS_DENIED(job forbids breakaway), and when the retry path is taken print that the daemon will not survive a tree kill and point at--foreground. Once the pipe leak is gone,daemon startandstatusreturn in seconds, so a harness like yours never reaches the timeout that kills the tree in the first place.On your point 3: 0.3.0 has
BSK_AUTO_START=0, which makes read-only commands refuse instead of spawning. Verified:BSK_AUTO_START=0 bsk statuswith no daemon ->stdout EOF after 0.1s, exit=2, "automatic daemon startup is disabled (BSK_AUTO_START=0)". It is opt-in though, not the default when stdout is not a TTY. Until the fix lands, that plus yourStart-Processworkaround (which hands the daemon no pipe) is the safe combination.We hit the harness side of this ourselves (a wait that never unblocks although the job already finished, liveness judged from log staleness rather than process presence); the check we use for it is in our repo (ours): https://github.com/aron-intframe/agent-guards
blacksamuraiiii commentedon Sep 17, 2026
Independent reproduction of both behaviors from a different calling context — confirming this is not harness-specific.
Environment
bsk0.2.1 (daemon protocol 1.1), Edge extension 0.3.0 (protocol 1.3)powershell.exeinterop — eachbskinvocation is a fresh non-interactive PowerShell process with stdout piped, no TTY, no process-tree kill by default (unlike the DeepSeek Harness in the OP). Daemon was fully stopped and verified gone (Get-Process bsk*→ 0) before each test.Reproduction 1:
statushangs indefinitely with no daemonResult: hangs >90 s (killed by WSL-side
timeout). Same hang with plainbsk status --json. Observed repeatedly (3/3 attempts, 60–90 s kills).Per the docs (docs/sandboxed-agents.md),
BSK_AUTO_START=0should makestatus"report the problem and ask for host-side startup. It does not spawn a replacement" — neither happened: it neither reported nor exited.Reproduction 2: implicit daemon spawn despite
BSK_AUTO_START=0After one of the killed
statusruns,Get-Process bsk*showed a newly spawned daemon (fresh pid, uptime ≈ 100 s) — i.e. the status command spawned a daemon as a side effect even withBSK_AUTO_START='0'explicitly set, and that daemon kept running after the killed CLI process.statusafter that returned a healthy JSON block with the new pid.Same footgun as the OP describes, reproduced without any harness process-tree semantics: the CLI process was killed client-side by WSL
timeout, and the spawned daemon survived independently.Suggestion
Echoing the OP: read-only commands (
status,doctor) should never start a daemon, and withBSK_AUTO_START=0they must fail fast with a clear "daemon not running" message instead of blocking. Additionally the hang may be related to stdout pipe semantics in non-TTY contexts — a fast-fail would fix both.Happy to provide
~/.bsk/daemon.logexcerpts if useful.fix(cli): preserve shared daemons after launcher timeout
iuyo5678 commentedon Sep 20, 2026
Fixed by #282, now merged into main. Background daemons no longer inherit caller pipe handles, and Windows startup verifies Job breakaway or fails with setup guidance. Launcher timeouts no longer terminate a shared daemon. Regression tests, including first-call status, pass in CI.
Query commands retain their existing auto-start defaults; BSK_AUTO_START=0disables implicit startup. Changing those defaults can be tracked separately.
Closing this issue as fixed.
fix(cli): isolate Windows daemon startup from caller jobs