mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 11:22:34 +00:00
Autopilot now dispatches each cycle as a single `autopilot-cycle` Minion
job (with idempotency_key on the cycle slot) instead of running steps
inline. A forked `gbrain jobs work` child drains the queue durably,
supervised by autopilot. The user runs ONE install step
(`gbrain autopilot --install`) and gets sync + extract + embed + backlinks
+ durable job processing, with no separate worker daemon to manage.
Mode selection:
- minion_mode=always OR pain_triggered (default), engine=postgres →
Minions dispatch. Spawn child, submit autopilot-cycle each interval.
- minion_mode=off, OR engine=pglite, OR `--inline` flag → run steps
inline in-process, same as pre-v0.11.1. PGLite has an exclusive file
lock that blocks a second worker process, so the inline path is the
only path that works there.
Worker supervision:
- spawn(resolveGbrainCliPath(), ['jobs', 'work'], { stdio: 'inherit' }).
stdio:'inherit' avoids pipe-buffer blocking (Codex architecture #2).
- On worker exit: 10s backoff + restart. Crash counter caps at 5 →
autopilot stops with a clear error.
- resolveGbrainCliPath() prefers argv[1] (cli.ts / /gbrain), then
process.execPath (compiled binary suffix check), then `which gbrain`
(installed to $PATH). NEVER blindly uses process.execPath, which on
source installs is the Bun runtime, not `gbrain` (Codex architecture
#1).
Shutdown:
- Async SIGTERM/SIGINT handler: sends SIGTERM to worker, awaits its
exit for up to 35s (the worker's own drain is 30s; we add buffer for
signal-delivery latency), then SIGKILL if still alive.
- Drops the old `process.on('exit')` lock-cleanup handler — its
callback runs synchronously and can't wait for the worker drain.
Lock file cleanup moved inside the async shutdown.
Lock-file mtime refresh every cycle (Codex C) so a long-lived autopilot
doesn't get declared "stale" by the next cron-fired invocation after 10
minutes.
Inline fallback path calls the new Core fns (runExtractCore, runEmbedCore)
instead of the CLI wrappers. That way a bad arg from inside the loop
can't process.exit() the autopilot itself (matches Codex #5).
test/autopilot-resolve-cli.test.ts: 3 tests covering argv[1]-as-gbrain,
argv[1]-as-cli.ts, and graceful error when no path resolves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
/**
|
|
* Tests for resolveGbrainCliPath() — picks the right executable to supervise
|
|
* as the Minions worker child. Codex caught that the earlier plan's use of
|
|
* process.execPath is wrong on source installs (points at the Bun runtime,
|
|
* not `gbrain`).
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { resolveGbrainCliPath } from '../src/commands/autopilot.ts';
|
|
|
|
describe('resolveGbrainCliPath', () => {
|
|
test('returns a non-empty string', () => {
|
|
// Whatever the test environment is (bun run ...), the resolver should
|
|
// find *something* — either argv[1] (cli.ts entry), execPath (compiled
|
|
// binary), or `which gbrain`. If none of those work, it throws; in
|
|
// test, argv[1] is the test runner path which usually ends in .ts, so
|
|
// the first branch or the `which` fallback catches it.
|
|
let path: string;
|
|
try {
|
|
path = resolveGbrainCliPath();
|
|
} catch (e) {
|
|
// If we throw, that means neither argv[1] nor execPath nor $PATH has
|
|
// gbrain — on a machine without gbrain installed, this is expected.
|
|
expect((e as Error).message).toContain('resolve');
|
|
return;
|
|
}
|
|
expect(typeof path).toBe('string');
|
|
expect(path.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('accepts /gbrain suffix (compiled binary)', () => {
|
|
// Simulate compiled-binary detection by setting argv[1] to /usr/local/bin/gbrain
|
|
const orig = process.argv[1];
|
|
process.argv[1] = '/usr/local/bin/gbrain';
|
|
try {
|
|
const path = resolveGbrainCliPath();
|
|
expect(path).toBe('/usr/local/bin/gbrain');
|
|
} finally {
|
|
process.argv[1] = orig;
|
|
}
|
|
});
|
|
|
|
test('accepts /cli.ts suffix (source install)', () => {
|
|
const orig = process.argv[1];
|
|
process.argv[1] = '/some/path/src/cli.ts';
|
|
try {
|
|
const path = resolveGbrainCliPath();
|
|
expect(path).toBe('/some/path/src/cli.ts');
|
|
} finally {
|
|
process.argv[1] = orig;
|
|
}
|
|
});
|
|
});
|