seat, cli, harness: git kill reach, close bound and host.lock (#1597)

Row 103, the row 100 follow-up. Built by Darkwing, packet
agents/darkwing/work/queue-103/r1/, candidate manifest
6e5ac8c0782bc595d7c0d711d457738ffb42f1ba5f38253912943dd55e55888a.
Sage approved on #1597, comment 28252; Dewey approved, comment 28260.

Host git runs through runGit: a detached group, SIGTERM at the limit,
SIGKILL after the grace, and one more SIGKILL to the group after a
signalled git closes. git-socket close() waits at most 2 s. startHost
claims bus-host/host.lock by hard link before the broker fork.
packages/cli/README.md and tests/host.test.mjs carry row 85's hunks
too; row 85 lands first.

Sage's gate rerun on 3a1e11b83 with rows 85, 102 and 103 applied,
Docker up: node suites bus 128, business 67, cli 165, control-board
124, conversation 182, discord 178, harness 73, ledger 78, mosaic 69,
queue 155, runs 43, seat 113, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 16:34:47 -05:00
co-authored by Claude Opus 5.5
parent 2a9b77b56b
commit 465bbdfbbc
12 changed files with 665 additions and 106 deletions
+15 -6
View File
@@ -279,7 +279,8 @@ so the host never recurses into one: a clone with a submodule is kept, and
`status` runs with `--ignore-submodules=all` and every call with
`submodule.recurse=false`. The root is read with `GIT_CEILING_DIRECTORIES`
at its parent, so a root that isn't a repository is never taken for the
one above it. Each git call is killed after 100 s.
one above it. Each git call gets SIGTERM after 100 s and SIGKILL 5 s
later, as below.
The checks hold only while nothing else writes the clone. Each workspace
is one instance's, and the launcher refuses `instance-running` before it
@@ -332,15 +333,23 @@ The temp ref is deleted on every path, and so is the bundle file in
`bus-host/`. No other ref moves, and the root is only read.
The repository's lock is a promise chain inside the host's process, so it
assumes one launcher per dataRoot. `startHost` refuses a second bus host
while `bus-host/host.json` names a live one, but that check isn't atomic:
two launchers started at the same moment could both pass it, and their
pushes to one repository would no longer be serialized.
assumes one launcher per dataRoot. `startHost` takes `bus-host/host.lock`
with a hard link, which fails if the file exists, and refuses while a live
host holds it, so two launchers started at the same moment can't both run
(`packages/cli/README.md`, `bus start`).
A host git call that runs past 100 s (`GIT_TIMEOUT_MS`) gets SIGTERM. git
removes its lockfiles on SIGTERM. Only a git still running 5 s later
(`GIT_GRACE_MS`) gets SIGKILL, which can leave a ref's `.lock` behind; that
lock refuses every later push to the ref until someone removes it.
lock refuses every later push to the ref until someone removes it. The
checkouts (`checkout.mjs`) and the working repositories (`working.mjs`) run
git the same way, through `runGit`. Each git is the leader of its own
process group, and both signals go to the group, so a hook or helper git
started can't outlive it. Once a signalled git has ended, the group gets
SIGKILL once more, for a process that ignored SIGTERM. A git that ends on
its own isn't signalled. Since git has its own group, a Ctrl-C at the
terminal of a host started by hand doesn't reach it; a systemd stop still
does, since it kills the unit's cgroup.
For a review, under the same lock, the host fetches `main` again and
writes a bundle of the requested commit with one ref,