mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:34:35 +00:00
* fix(dream): stamp path-derived sources so --dir runs land cycle freshness (#1869) gbrain dream --dir <path> (and the configured sync.repo_path fallback) never wrote last_source_cycle_at / last_full_cycle_at because runCycle's stamp gate reads opts.sourceId and dream only set it from --source. Doctor's cycle_freshness stayed perpetually stale on path-scoped brains. Fix at the command level: dream derives the source id from the resolved brain dir via resolveSourceForDir (now exported from cycle.ts) and passes it as opts.sourceId. runCycle's stamp/lock semantics are untouched, so legacy global callers (autopilot-global-maintenance runs GLOBAL_PHASES with a brainDir and no sourceId) cannot falsely stamp per-source freshness — the flaw that sank the runCycle-wide variant in PR #2549. A derived match on an archived source is skipped (mirrors the explicit --source archived guard). Takeover of #2549. Co-authored-by: javieraldape <javieraldape@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(autopilot): close the engine on SIGTERM/SIGINT instead of hard-exiting (#1872) systemctl stop (SIGTERM) previously hard-exited autopilot without ever closing the engine. On PGLite the cycle steps run INLINE in the autopilot process, so a mid-write exit kills WASM Postgres with the WAL dirty and can corrupt the brain. Now both exit paths close the engine first: - autopilot's own shutdown() (SIGINT + internal stops like max_crashes / cycle-failure-cap) aborts the in-flight inline cycle via an AbortController threaded into runCycle, drains it briefly, and awaits engine.disconnect() before process.exit(0). - process-cleanup's SIGTERM handler (installed at cli.ts module load, exits within its 3s cleanup deadline) reaches the same closeEngine via a registered 'autopilot-engine-close' cleanup callback. PGLite's disconnect() drains the pending query and checkpoints before closing; a second call is a no-op, so both paths firing is safe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(dream): conform dream-dir-source-stamp to canonical PGLite isolation pattern check:test-isolation R3/R4 flagged the new test file: engine was created in beforeEach (outside beforeAll) and never disconnected in afterAll. Switch to the canonical shared-engine pattern (beforeAll create, beforeEach resetPgliteState, afterAll disconnect) per test/helpers/reset-pglite.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: javieraldape <javieraldape@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
/**
|
|
* #1872 — autopilot SIGTERM/SIGINT must close the engine before exit.
|
|
*
|
|
* On PGLite the cycle steps run INLINE in the autopilot process, so a hard
|
|
* `process.exit` mid-write (systemctl stop → SIGTERM) kills WASM Postgres
|
|
* with the WAL dirty and can corrupt the brain. Two exit paths must both
|
|
* close the engine:
|
|
*
|
|
* - autopilot's own shutdown() (owns SIGINT + internal stops like
|
|
* max_crashes / cycle-failure-cap), and
|
|
* - process-cleanup's SIGTERM handler (installed at cli.ts module load,
|
|
* which exits within its 3s cleanup deadline) — reached via the
|
|
* registered 'autopilot-engine-close' cleanup callback.
|
|
*
|
|
* Because the shutdown path is deep inside `runAutopilot()` (a long-running
|
|
* daemon loop that ends in process.exit), a behavioral test would have to
|
|
* spawn + signal a real daemon. Following the established precedent
|
|
* (test/autopilot-supervisor-wiring.test.ts, test/autopilot-fanout-wiring.test.ts),
|
|
* these static-shape regressions pin the load-bearing wiring instead.
|
|
*/
|
|
import { describe, expect, it } from 'bun:test';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const AUTOPILOT_SRC = readFileSync(
|
|
join(import.meta.dir, '..', 'src', 'commands', 'autopilot.ts'),
|
|
'utf8',
|
|
);
|
|
|
|
describe('autopilot.ts graceful engine shutdown (#1872)', () => {
|
|
it('registers an engine-close callback in the process-cleanup registry (SIGTERM path)', () => {
|
|
// process-cleanup owns SIGTERM (installed at cli.ts:10) and hard-exits
|
|
// after its cleanup pass; without this registration the engine is never
|
|
// closed on `systemctl stop`.
|
|
expect(AUTOPILOT_SRC).toContain(
|
|
"import { registerCleanup } from '../core/process-cleanup.ts';",
|
|
);
|
|
expect(AUTOPILOT_SRC).toContain(
|
|
"registerCleanup('autopilot-engine-close', closeEngine)",
|
|
);
|
|
});
|
|
|
|
it('closeEngine aborts the in-flight inline cycle then disconnects the engine', () => {
|
|
// Abort first (runCycle checks the signal between phases and threads it
|
|
// into phase sub-work), bounded drain, then disconnect.
|
|
expect(AUTOPILOT_SRC).toMatch(
|
|
/const closeEngine = async \(\) => \{[\s\S]{0,900}shutdownAbort\.abort\([\s\S]{0,900}engine\.disconnect\(\)/,
|
|
);
|
|
});
|
|
|
|
it('the inline runCycle call carries the shutdown abort signal and is tracked as in-flight', () => {
|
|
// PGLite / --inline path: the cycle runs in-process, so shutdown must be
|
|
// able to (a) signal it to wind down and (b) await it before closing.
|
|
expect(AUTOPILOT_SRC).toMatch(/signal:\s*shutdownAbort\.signal/);
|
|
expect(AUTOPILOT_SRC).toMatch(/inflightInlineCycle\s*=\s*cyclePromise/);
|
|
});
|
|
|
|
it('shutdown() awaits closeEngine() before process.exit(0) (SIGINT + internal-stop path)', () => {
|
|
expect(AUTOPILOT_SRC).toMatch(
|
|
/await closeEngine\(\);[\s\S]{0,400}process\.exit\(0\)/,
|
|
);
|
|
});
|
|
});
|