Spike: a tool.call hook that holds a call across repeated long-polls

<h1>nixie</h1>
A personal assistant platform that its owner runs and controls: every rule readable and editable, every decision recorded, and personal data kept on the owner's own hosts.
<a href="./docs/README.md">Documentation</a> • <a href="./docs/README.md#decisions">Decisions</a> • <a href="./AGENTS.md">Agent Guidelines</a>
nixie is in Phase 2, Research, and has no product code. The work runs in 4 phases: brainstorm, research, design, and decide. Decisions are recorded as research settles each question.
bun install
bun run check # format, lint, and the prose check
hooks/register.js 51 lines1// Holds MARK_HOLD calls until the owner's decision service answers. Each long-poll stays under the
2// 30 s cap on $.http.fetch, and the wait inside it does not count against the hook's 10 s limit.
3async function readDecision($, url) {
4 const response = await $.http.fetch(url);
5 return JSON.parse(response.text).decision;
6}
7
8async function waitForDecision($, request, poll = 1) {
9 const decision = await readDecision($, request.url);
10 await $.ui.log(
11 `nixie poll ${poll} decision=${decision} budget=${JSON.stringify(request.budget)}`,
12 {
13 to: 'debug',
14 },
15 );
16 if (decision === 'pending') {
17 return waitForDecision($, request, poll + 1);
18 }
19 return { decision, polls: poll };
20}
21
22async function handleToolCall($, e, next) {
23 if (!e.command.includes('MARK_HOLD')) {
24 return next(e);
25 }
26 const base = await $.env.get('NIXIE_DECIDER_URL'),
27 started = Date.now(),
28 verdict = await waitForDecision($, {
29 budget: next.budget,
30 url: `${base}/decide?tool_use_id=${e.tool_use_id}`,
31 });
32 if (verdict.decision === 'allow') {
33 return next(e);
34 }
35 return {
36 deny: `nixie-hold-poll: the owner refused this command after ${Date.now() - started} ms and ${verdict.polls} polls.`,
37 };
38}
39
40function handleFailure($, e, next) {
41 return {
42 deny: `nixie policy failed (${next.error.kind}: ${next.error.message}), so the command did not run.`,
43 };
44}
45
46export function register(on) {
47 // `.catch` here is the mods API's error handler on a registration, not a promise method.
48 // oxlint-disable-next-line promise/prefer-await-to-then
49 on('tool.call', { tool: 'Bash' }, handleToolCall).catch(handleFailure);
50}
51