mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-25 06:11:02 +02:00
fix(hooks): memorable hook survives host termination and brace-bearing banners
- The bash shim runs bun as a job and forwards SIGTERM/SIGINT/SIGHUP (bash holds a signal until a foreground child exits); the .ts kills the in-flight vendor's process group on the way out (runExternal exposes the group kill through onSpawn), so a hook the host terminates cannot leave the vendor running with the prompt on its stdin. - The tolerant stdout parser tries every complete top-level object (bounded) and takes the first carrying a string additionalContext, so a banner with braces or quotes, or a progress object, no longer costs the answer. - git runs with LC_ALL=C and the not-a-repository check is anchored to the start of its message: a localized git or a repository path containing the phrase can no longer flip the lookup. - The rate limiter remembers up to 32 live keys, so alternating failures cost two lines, not one per prompt. - Unicode format characters (bidi overrides, zero-width spaces) are stripped from vendor text at egress; the zero-width joiner stays for emoji. - A killed child (timeout, ENOBUFS) resolves on exit without the stdout drain, and the post-kill grace is 100 ms, so the timeout outcome fits the reserve. - The ledger size warning, which the host discards from an exit-0 hook's stderr, is logged where status looks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ed7b7888a0
commit
3b6a859c79
@@ -48,8 +48,8 @@ export function runBin(name: string, args: string[], opts: SpawnSyncOptions) {
|
||||
|
||||
/** Kept tail of the child's stderr, for the caller's error log. */
|
||||
const STDERR_TAIL_BYTES = 500;
|
||||
/** After a group kill, how long to wait for 'close' before resolving anyway. */
|
||||
const KILL_GRACE_MS = 250;
|
||||
/** After a group kill, how long to wait for 'exit'/'close' before resolving anyway (kept under the callers' post-spawn reserve). */
|
||||
const KILL_GRACE_MS = 100;
|
||||
/** After the direct child exits, how long to keep draining stdout before resolving. */
|
||||
const EXIT_DRAIN_MS = 150;
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface RunExternalOptions {
|
||||
/** the child's COMPLETE environment (callers allowlist; never pass process.env for a third-party binary) */
|
||||
env?: Record<string, string | undefined>;
|
||||
cwd?: string;
|
||||
/** called once the child is running with a function that SIGKILLs its whole process group (for a caller's signal handler) */
|
||||
onSpawn?: (killGroup: () => void) => void;
|
||||
/** test seam: override process.platform */
|
||||
platform?: NodeJS.Platform;
|
||||
}
|
||||
@@ -151,10 +153,11 @@ export function runExternal(exe: string, args: string[], opts: RunExternalOption
|
||||
timedOut = true;
|
||||
error = error ?? 'ETIMEDOUT';
|
||||
killGroup();
|
||||
// If 'close' never arrives (a grandchild holding the pipes open past the
|
||||
// If 'exit' never arrives (a grandchild holding the pipes open past the
|
||||
// kill), resolve anyway: the caller's own deadline is what matters.
|
||||
graceTimer = setTimeout(() => finish(null, 'SIGKILL'), KILL_GRACE_MS);
|
||||
}, Math.max(1, opts.timeoutMs));
|
||||
opts.onSpawn?.(killGroup);
|
||||
child.on('error', (e) => { error = (e as NodeJS.ErrnoException)?.code ?? 'ESPAWN'; finish(null, null); });
|
||||
child.stdout?.on('data', (d: Buffer) => {
|
||||
if (done) return;
|
||||
@@ -165,6 +168,9 @@ export function runExternal(exe: string, args: string[], opts: RunExternalOption
|
||||
child.stderr?.on('data', (d: Buffer) => { stderrTail = (stderrTail + d.toString('utf8')).slice(-STDERR_TAIL_BYTES); });
|
||||
child.on('exit', (code, signal) => {
|
||||
if (done) return;
|
||||
// A killed child (timeout, ENOBUFS) has nothing worth draining: resolve
|
||||
// now so the caller keeps its post-spawn reserve.
|
||||
if (timedOut || error) { finish(code, signal); return; }
|
||||
// stdio may still be open (a background grandchild inherited the pipes):
|
||||
// drain what the child itself wrote, then resolve with its real exit and
|
||||
// kill whatever is still holding the group.
|
||||
|
||||
Reference in New Issue
Block a user