fix(harbor-manager): prevented dev-server teardown rejections from killing the manager

- Added a process-wide `__harborManagerHooks` flag on `globalThis` so signal and unhandled-rejection handlers are registered only once during Bun `--hot` re-execution.
- Added `isDevStreamTeardown` handling to ignore `ERR_STREAM_RELEASE_LOCK` unhandled rejections from dev stream teardown while rethrowing all other unhandled rejections.
- Added `react-refresh` to the Harbor Manager package manifest and lockfile dependencies.
This commit is contained in:
can1357
2026-07-13 19:22:06 +02:00
parent 4c1c5f40d8
commit 35d3e49d1f
4 changed files with 41 additions and 15 deletions
+3
View File
@@ -164,6 +164,7 @@
"@types/d3-shape": "^3.1.7",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"react-refresh": "^0.18.0",
},
},
"packages/hashline": {
@@ -1418,6 +1419,8 @@
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
+1 -1
View File
@@ -45,7 +45,7 @@
- Fixed backgrounded Bash blocks continuing to repaint with live and final job output; they now freeze with a compact job notice while completion is delivered separately
- Fixed the prewalk plan nudge silently ending the run with no code written when the model answered with a text-only reply (no tool call): the agent loop treats a tool-call-free turn as a natural stop and never prompts again, which the nudge's own "write the plan in your next reply" instruction makes common. The nudge now explicitly tells the model this is a checkpoint, not a final answer, and the session forces one more turn whenever a post-nudge reply lands with zero tool calls
- Fixed launch tool rendering stacking a stale pending header over a bare `✓ Launch` line and raw text: the tool now uses a merged registry renderer with one per-op status header (op, target, `state · pid · uptime` meta), stripped log cursor suffixes, capped collapsed log/list previews, and a launch tool glyph
- Fixed `launch logs` flattening PTY control sequences into repeated debugger frames: raw daemon output is now replayed through the shared xterm screen renderer used by Bash PTY mode, preserving cursor updates, colors, and text styles while model-facing log text remains sanitized.
- Fixed `launch logs` flattening PTY control sequences into repeated or diagonally wrapped debugger fragments: the bounded raw PTY stream is now replayed before row selection through the shared xterm screen renderer used by Bash PTY mode, blank terminal cells retain their columns, and the final colored/styled viewport renders in the same bordered output block as Bash while model-facing text remains sanitized
- Fixed confusing launch start/wait results when readiness timed out with the log pattern already matched (readiness needs log AND port): the result printed a contradictory `Ready: <match>` next to `Readiness timed out` without naming the failing condition. Daemon snapshots now carry the unmet conditions (`readyPending`), and start/wait results state exactly what never happened (e.g. `port 3100 on 127.0.0.1 never accepted connections`); the TUI shows a `waiting on port` badge on starting daemons
- Fixed the in-process `stat` builtin mangling BSD-style invocations like `stat -f "%Sm %N" file` (macOS muscle memory): GNU `-f` means `--file-system`, so the format string was treated as a file operand — printing filesystem info for the real operands and erroring with `cannot read file system information for '%Sm %N'`. A `-f` whose format value contains `%` is now detected as BSD syntax and translated to the GNU equivalent (`%Sm`→`%y`, `%N`→`%n`, `%z`→`%s`, epoch/`S`-form times, owner/group/permission and `H`/`L` sub-field directives, `-L`/`-n`/`-q`/`-F` flag clusters, with `%n`/`%t` as literal newline/tab); directives with no GNU counterpart fail with a clear `unsupported BSD format directive` error
- Fixed the remaining GNU-flavored shell builtins that broke under macOS/BSD muscle memory, using the same unambiguous-detection approach as the `stat` fix (only invocations that are invalid or nonsensical under GNU semantics are reinterpreted; unsupported BSD forms fail loudly instead of producing wrong output): `date -r <epoch>` formats the epoch when no such file exists (GNU `-r FILE` mtime preserved), signed `date -v±N<unit>` adjustments translate to `-d` relative dates and `-j` is accepted (`-j -f` strptime parse mode and field-set `-v` error clearly); `sed -i '' 's/…/…/' file` drops the BSD empty backup-suffix token instead of treating it as the script; `mktemp -t prefix` without X's creates `$TMPDIR/prefix.XXXXXXXXXX` (the GNU `too few X's` error path); `tail -r` reverses input by delegating to `tac` (with `-n`/`-c`/`-f` combinations erroring clearly); `find -E` maps to `-regextype posix-extended` ahead of the expression; `base64 -D` decodes as an alias of `-d`; and `ln -sfh` works via a `-h` alias of `--no-dereference` (clap's `-h` help short is dropped to match real GNU/BSD ln; `--help` unchanged)
+2 -1
View File
@@ -45,7 +45,8 @@
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.7",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0"
"@types/react-dom": "^19.1.0",
"react-refresh": "^0.18.0"
},
"engines": {
"bun": ">=1.3.14"
+35 -13
View File
@@ -182,10 +182,9 @@ export class ManagerServer {
// Bun bundles the dashboard (React + TSX) from the HTML import and
// serves it on the same port as the API — one process, no Vite.
routes: { "/": indexHtml },
// Only `hmr`: Bun's `console: true` mirror opens a server-read stream
// over the dev client that a `--hot` reload's force-close tears down
// mid-read, surfacing an unhandled `AbortError: ERR_STREAM_RELEASE_LOCK`
// that crashes the process.
// Only `hmr`: the `console: true` mirror adds another dev-client
// stream with the same teardown hazard (see isDevStreamTeardown)
// for little value.
development: process.env.NODE_ENV !== "production" && { hmr: true },
fetch: request => this.#route(request),
});
@@ -628,22 +627,45 @@ function readTextTail(file: string, cap: number): string {
}
}
/**
* Bun's dev server (HMR websocket, browser error reports, console mirror)
* reads client streams that a tab disconnect or `--hot` reload tears down
* mid-read. The resulting `AbortError: ERR_STREAM_RELEASE_LOCK` surfaces as
* an unhandled rejection from Bun internals — fatal by default, which would
* kill the manager and orphan every running benchmark job.
*/
function isDevStreamTeardown(err: unknown): boolean {
return err instanceof Error && (err as Error & { code?: string }).code === "ERR_STREAM_RELEASE_LOCK";
}
if (import.meta.main) {
// `bun --hot` re-evaluates this module in-place: retire the previous
// instance first, or its sync ticker and sqlite connection leak per reload.
const host = globalThis as typeof globalThis & { __harborManagerServer?: ManagerServer };
const host = globalThis as typeof globalThis & {
__harborManagerServer?: ManagerServer;
__harborManagerHooks?: boolean;
};
await host.__harborManagerServer?.stop();
const { port, jobsDir } = parseServerArgs(process.argv.slice(2));
const manager = new ManagerServer(jobsDir);
host.__harborManagerServer = manager;
const server = manager.start(port);
process.stdout.write(`harbor-manager listening on http://localhost:${server.port} (jobs: ${jobsDir})\n`);
const shutdown = async () => {
await manager.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
// Process-wide hooks register once; `--hot` re-evals reuse them via `host`.
if (!host.__harborManagerHooks) {
host.__harborManagerHooks = true;
const shutdown = async () => {
await host.__harborManagerServer?.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("unhandledRejection", err => {
if (isDevStreamTeardown(err)) {
process.stderr.write("ignored dev-server stream teardown (ERR_STREAM_RELEASE_LOCK)\n");
return;
}
throw err; // preserve fail-fast for real bugs
});
}
}
// hot-reload-probe