framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
62 lines • 2.64 kB
JavaScript
/**
* Holds the Node event loop open while the agent is parked on the user (#1359).
*
* A daemon-spawned agent has nothing ref'd at a parked gate: the daemon spawns it detached with
* every stdio ignored and `--no-dashboard` (so no server), the claude-code driver spawns and
* reaps a child per prompt (so nothing runs between turns), and the control watcher is
* deliberately unref'd so steering alone never keeps a finished agent alive (#344). The moment
* `requestChoice` parked a bare Promise between turns, the event loop was empty and Node exited
* 0 mid-await — no `end` event, empty stderr, and picks landing in `control.jsonl` that nothing
* would ever read. The same silently-unwired-channel family as #905/#922, one layer down: the
* channel was wired, but the process waiting on it was not held.
*
* This is the narrow inverse of the watcher's unref: a ref'd (but idle) interval that exists
* only while at least one held promise is pending. Waiting for the answer IS the agent's work at
* that moment, so holding the loop is correct exactly there — and nowhere else, which is why the
* watcher itself stays unref'd (the actions-abort hang is what unref'ing it fixed).
*/
/**
* The idle interval's period. The callback never matters — only the ref does — so it is set far
* beyond any run's life to make the no-op wakeups free.
*/
const IDLE_INTERVAL_MS = 2 ** 30;
/**
* The real timers: a ref'd no-op interval (`setInterval` refs by default — the ref IS the
* fix, so a test asserts `hasRef()` on what `start` returns rather than trusting this comment).
*/
export const nodeKeepaliveTimers = {
start: () => setInterval(() => { }, IDLE_INTERVAL_MS),
stop: timer => clearInterval(timer),
};
/**
* Create the agent's keepalive. Counter-based: the first pending hold starts one ref'd idle
* timer, overlapping holds share it, and the last one to settle stops it — so the process can
* exit the moment nothing is parked, exactly as before this existed.
*/
export function createGateKeepalive(timers = nodeKeepaliveTimers) {
let held = 0;
let timer;
const acquire = () => {
if (held++ === 0)
timer = timers.start();
};
const release = () => {
if (--held === 0 && timer) {
timers.stop(timer);
timer = undefined;
}
};
return {
held: () => held,
async hold(pending) {
acquire();
try {
return await pending;
}
finally {
release();
}
},
};
}
//# sourceMappingURL=gate-keepalive.js.map