Pull Request Overview
- Opened on September 7, 2026
- Status Merged
- Commit count 2 with first commit September 7, 2026
Total Delta
Open Days
Test Delta
How long has this pull request spent in each phase of its lifecycle?
Data pending calculation for pull request
Tie the anytypeHelper process lifetime to the Electron main process
Problem
anytypeHelper could outlive the Electron main process. If the app was killed, crashed, or failed to reap the child, the helper kept running and held the account lock, so the next launch could not start.
Two independent holes:
-
No lifeline. Nothing told the helper its owner was gone. -
stop()was unreliable. It resolved immediately wheneverisRunningwas false even with a livecpβ exactly the quit-during-startup case that orphans a helper β and whenisRunningwas true it waited onexitwith no timeout at all, so a hung helper hung the quit forever.
What this does
Opt into the middleware lifeline. anytype-heart 0.51.0-rc7 (cmd/grpcserver/lifeline.go, PR anyproto/anytype-heart#3256) reads ANYTYPE_PARENT_LIFELINE=stdin and treats EOF on stdin as proof its owner disappeared, then shuts down under its own 10s hard-exit deadline. Both spawn sites β the Electron main process and scripts/start-web.js β now set the env var and keep stdin piped for the helper's whole lifetime. This covers the case no client-side code can: the main process dying without running any cleanup.
Make stop() reliable.
- Deduplicate concurrent calls behind a single promise.
- Escalate to SIGKILL after a 12s graceful deadline; resolve false if no exit is observed 5s after that, so start() never spawns a second helper on top of a live one.
- Fall back to force-stop when the Windows stdin shutdown pipe fails or is missing.
- Clear cp/isRunning on exit, and only for the process we actually own.
Fix ownership and promise settlement in start() (second commit, from review):
- The error/stdout/stderr handlers wrote shared state with no ownership check. Node flushes a child's buffered output after it exits, so a killed helper's late ready line could publish the dead process's address and flip isRunning β which then caused the live helper's real ready line to be discarded and its start() to never settle.
- start() only ever resolved from the stdout ready line, so if the helper was stopped or died first, the promise never settled and main.ts's waitLibraryPromise stayed pending forever. It now resolves false on an intentional stop and rejects on a crash.
- start() is serialized against itself, mirroring stop(); two concurrent calls both spawned and the first child was dropped with no reference left to stop it.
Why 12s
The helper arms its own 10s hard-exit deadline once the lifeline fires. A matching 10s client timer made the two race non-deterministically β our SIGKILL would usually pre-empt the helper's own clean exit. At 12s the helper's path wins where it has one, and the extra 2s is only ever paid when the helper has already missed its own deadline. Note the helper's POSIX signal path is explicitly not covered by that deadline ("OS-signal shutdown is not subject to the desktop parent's lifeline deadline"), which is what the client-side timer really guards.
Also included
scripts/generate-protos.sh β pass CLIENT_DESKTOP_PATH="$ROOT_DIR" to make install-dev-js. Heart declares CLIENT_DESKTOP_PATH ?= ../anytype-ts relative to its own dir, so with a renamed heart checkout or a git worktree the old invocation either installed the freshly built binary and pb bindings into a different anytype-ts checkout β a silent binary-vs-bindings mismatch β or aborted under set -euo pipefail. Needed to test this feature locally against a heart build.
β οΈ Worth knowing: running that script in local mode overwrites the pinned dist/anytypeHelper (rc7) with a dev build from your heart working tree.
Testing
vitest.config.ts now includes electron/**/*.test.ts, and electron/ts/server.test.ts covers the lifecycle: lifeline opt-in, stdin staying open, ownership races, crash reporting, the Windows stdin path and its failure modes, dedup, force-kill escalation, and give-up semantics.
- 22/22 new tests pass; stable across shuffled runs.
- Full suite: 90 failed / 836 passed β the same 90 pre-existing failures as
develop, zero new (836 = 821 + the 15 tests added here). -
bun run typecheckclean for both tsconfigs;bun run linthas 0 errors. -
Mutation-tested. The first version of these tests caught only 1 of 12 seeded lifecycle defects. The current suite catches 15 of 15 β including dropping thestopTriggeredreset, ignoringkill()returning false, removing the abort-on-stop-failure branch, never releasingstopPromise, and removing either ownership guard.
Known limits (deliberately not addressed here)
-
stop()'s dedup ignores the requestedsignal, so aSIGKILLarriving during an in-flightSIGTERMdegrades to the graceful path. Unreachable today β every caller passes''βSIGTERM. - After a give-up (
finish(false)),stopTriggeredstaystrue, so a later real crash of that zombie is not reported. -
readyPromiseis created once in the constructor and not re-armed, sostart()is not genuinely retryable. It is called once, fromConfigManager.init. - Quit awaits
stop()(api.ts:524), so a hung helper can delay quit by up to 17s behind an already-hidden window. Strictly better than the previous unbounded wait, but reworking the quit path is a separate change.
Review
Three parallel review passes (lifecycle correctness, integration/packaging, tests/build scripts). Findings on ownership, promise settlement, start serialization, the timeout race, and test strength are folded into the second commit. Verified as non-issues: the stdio: ['pipe','pipe','pipe'] addition is Node's default and both sites already drained stdout and stderr (no backpressure risk); no other spawn sites exist; a failed spawn sets exitCode to the negative errno, not null, so the already-exited guard handles it correctly. Incidental win: env is now a copy, so GOLOG_FILE no longer leaks into the main process's own process.env.
β οΈ CI note: no workflow runs vitest today, and eslint.config.js ignores electron/**, so these tests only run locally. bun run typecheck does cover them.
PR was closed without comments.