import { AsyncLocalStorage } from 'node:async_hooks'; import type { Sql } from '../../db.js'; import type { PermissionMode } from './types.js'; /** * Per-run inference context for write tools. * * Write tools need ambient state (sql, sessionId, the permission gate) that the * BooChat tool-phase `execute(input, projectRoot, extraRoots?)` signature can't * carry. This used to be a single module-level `let current` — but the inference * runner's `enqueue()` is fire-and-forget, so two overlapping runs (a user * message racing a dispatcher-polled native task; two chat tabs streaming) would * clobber each other's context, and `cancel()` cleared it for ALL in-flight runs. * * AsyncLocalStorage gives each run its own context: `enqueue()` starts its async * loop synchronously inside `runWithInferenceContext`, so the store propagates * through every awaited tool execution in that run — and only that run. */ export interface InferenceContext { sql: Sql; sessionId: string; taskId: string | null; /** Native-BooCode permission gate, set per run from the request/task mode. */ permissionMode?: PermissionMode; } const storage = new AsyncLocalStorage(); /** * Bind `ctx` for the duration of the (possibly detached) async chain `fn` starts. * The inference runner kicks off its loop synchronously within this call, so all * downstream `await`s — including write-tool `execute` via the adapter — read the * same store. Concurrent runs each get their own; nothing is shared or cleared * out from under an in-flight run. */ export function runWithInferenceContext(ctx: InferenceContext, fn: () => T): T { return storage.run(ctx, fn); } export function getInferenceContext(): InferenceContext { const ctx = storage.getStore(); if (!ctx) { throw new Error( 'Write tool called outside inference context — runWithInferenceContext() did not wrap this run', ); } return ctx; }