/** * The `llm-relay mcp` server — one dispatch verb, callable from any MCP host. * * WHAT PROBLEM THIS SOLVES. `llm-relay dispatch --next-command` answers "which lane" correctly and * then hands the caller a COMMAND to execute. Two things go wrong with that, and both are measured: * * 1. The answer's SHAPE depends on the host. A routed Claude Code session gets a `target:` spec to * address as a subagent; a bypassed or headless one gets a `run:` command. The caller must * branch on mechanism, which is exactly what the owner asked never to be necessary. * 2. Executing the command correctly is hard. `lane-runner.ts` lists five distinct measured ways * to get it wrong, each of which cost a release or a wasted lane run. * * This server removes both. Every host makes the same call and receives an ANSWER, not a command. * * WHERE IT RUNS. As a stdio child of the HOST, launched as `llm-relay mcp`. It is NOT part of the * relay daemon, and it answers no HTTP. The daemon's rule that no HTTP turn may spawn a lane is * therefore untouched — this process is the host's own spawn mechanism wearing a protocol. * * SEAMS. Every environment dependency is injected (`buildView`, `spawn`, `now`, `cwd`), so the * suite exercises the whole surface without spawning a real lane or spending real quota. Same * discipline as `lane-quota-probe.ts` and `availability-snapshot.ts`. */ import type { Config } from "../config.js"; export { buildLaneEnv, laneCredentialEnvNames } from "../lane-launch-env.js"; import type { DispatchView } from "../dispatch.js"; import { type TreeSnapshotReader } from "./tree-delta.js"; import type { ProcessCpuReader } from "./process-cpu.js"; import type { DispatchedTelemetryReport, DispatchMode } from "../dispatch-lane-stats.js"; import { type AnswerFetch, type LaneJob, type LaneSpawner, type DispatchedQuotaReport } from "./lane-runner.js"; import { type AgyLogSnapshot } from "./agy-quota-log.js"; import { type JobJournal } from "./job-journal.js"; import { type JobArchive } from "./job-archive.js"; import { type LaneExecutionClient } from "./lane-execution-client.js"; export declare const MCP_SERVER_NAME = "llm-relay"; /** `max_tokens` for an answer-mode call when the caller names none. */ export declare const DEFAULT_ANSWER_MAX_TOKENS = 4096; /** How many jobs `dispatch_status` lists when it is called without a jobId. */ export declare const RECENT_JOBS_LISTED = 20; /** * How a dispatch view is obtained. Injected rather than imported so this module never decides * whether to consult the running relay — the caller owns that, exactly as `buildDispatch` takes * the host verdict as an argument instead of sniffing for it. */ export type DispatchViewBuilder = (opts: { task: string | undefined; tier: string | undefined; lane: string | undefined; /** * How this dispatch will run its lanes, so the relay hands back run times measured in the SAME mode * (`DispatchMode`). Absent from `dispatch_lanes`, which runs nothing. */ mode?: DispatchMode | undefined; /** A routing spec to run as its own one-lane view instead of the ladder (`dispatch`'s `model`). */ model?: string | undefined; }) => Promise; /** A lane's traffic as the relay daemon reports it. */ export interface LaneTraffic { /** Requests with the lane's tag the daemon is serving now. */ inFlight: number; /** Epoch ms of the tag's last request start, response write or request end. */ lastActivityAt: number; } export interface McpServerDeps { config: Config; buildView: DispatchViewBuilder; spawn?: LaneSpawner; /** * Answer-mode's direct-HTTP seam — a POST to THIS relay's own `/v1/messages`, reading * `config.host`/`.port`. Never the vendor egress fetches other modules inject; this is the one * call answer mode makes. */ fetch?: AnswerFetch; now?: () => number; /** Default working directory for a lane when the caller names none. */ cwd?: () => string; /** Operator bound on caller-supplied directories; absent ⇒ existence check only. */ allowedRoots?: readonly string[]; /** Reads `git status` for the tree delta (`tree-delta.ts`); defaults to the real git reader. */ treeSnapshot?: TreeSnapshotReader; /** * Reads a lane's live traffic from the relay daemon (`GET /dispatch/activity`, `lane-activity.ts`). * Absent, or null for a tag, means no signal from the daemon — never "idle". */ readLaneActivity?: (tag: string) => Promise; /** * Reads cumulative CPU milliseconds for the spawned process tree this job owns. The first * successful reading is a baseline; only a later increase can count as activity. */ readProcessCpu?: ProcessCpuReader; /** The platform whose environment rules the lane launcher applies; defaults to `process.platform`. */ platform?: NodeJS.Platform; maxDepth?: number; /** * Version reported in `serverInfo`. Injected because `process.env.npm_package_version` is only * set when npm launched the process, and a host launches this one directly — so reading it here * reported "0.0.0" to every real client. Measured on the first live handshake. */ version?: string; /** Reports positive lane quota evidence to the relay's exhaustion state. */ reportExhaustion?: (report: DispatchedQuotaReport) => Promise | void; /** * Forwards one metadata-only lane-execution report per settled agent-mode job to the * daemon's `POST /dispatch/telemetry`. Counts and lengths only — never the task text, * never the lane's output. */ reportTelemetry?: (report: DispatchedTelemetryReport) => Promise | void; /** * The running-job journal. Absent ⇒ `nullJobJournal`, which records nothing: a programmatic * embed that does not want a file gets the pre-journal behaviour exactly. `cli.ts` passes the * real one, so a host-launched server can report the jobs a previous instance died holding. */ journal?: JobJournal; /** * The finished-job archive. Absent ⇒ `nullJobArchive`, which keeps nothing. `cli.ts` passes the * real one, so a job that ENDED before a restart still answers `dispatch_result` afterwards and * job ids continue past the highest one the previous process minted (`job-archive.ts`). */ archive?: JobArchive; /** * D1 daemon execution client. Optional until the ownership switchover; when present, broker-backed * orphan rows are reconciled instead of being declared killed from the MCP owner pid alone. */ laneExecutionClient?: LaneExecutionClient; /** * The llm-relay version INSTALLED on disk now, read fresh on each call — or null when unknown. * When it differs from `version` (the code this process started with), every tool reply says so: * the Claude desktop app keeps one MCP process alive across sessions and releases, and on * 2026-09-10 two processes from before v0.78.0 were still serving old code with no sign of it. */ installedVersion?: () => string | null; /** * AGY's log, read after an AGY lane ends or is stopped, so a quota death AGY stated only in its * log still reaches the relay (`agy-quota-log.ts`). Absent ⇒ never read. */ readAgyLog?: () => AgyLogSnapshot | null; write: (chunk: string) => void; } /** * The `initialize` instructions. An MCP host puts this in the model's system prompt * unconditionally, so it is the ONE channel here that cannot be deferred, collapsed to a bare * tool name, or missed because the model never went looking. The tool descriptions below say * WHAT each tool does; a host only reads them once the model has already decided to delegate. * * ⚠ So this text must state WHEN to delegate, not just what the tool is. It carried only the * "what" until 2026-08-30, and the measured consequence was that the operator had to say * "use llm-relay for offload" out loud — on a machine whose own CLAUDE.md already said * "PREFER THE MCP TOOL" in bold. Prose the model must go and find is not a trigger. * * Keep it short: every host pays for it in every session. Pinned by `test/mcp-server.test.ts`. */ export declare const MCP_INSTRUCTIONS: string; /** * What one `dispatch` call may block for, decided in exactly one place. * * An MCP host tool call fails between 45 s and 100 s on this machine and destroys the job * handle above that, so no call may block past `routing.mcp.maxWaitMs` (the `ceiling`): absent * waits the full ceiling, a larger `waitMs` is clamped to it (and `awaitOrPoll` announces the * clamp), and a `waitMs` the server cannot honour at all — negative, zero, non-finite, or not * a number — is a `refusal`, the property's second branch. The union is narrowed with `in` * at the one call site, never an unconditional `else` resolving to a wait. */ export type ResolvedWaitMs = { waitMs: number; clamped: boolean; requested: number; } | { refusal: string; }; export declare function resolveWaitMs(requested: unknown, ceiling: number): ResolvedWaitMs; /** * A RUNNING spawned attempt's output so far: how long the lane has been silent, or how much it has * written and how long ago. Null for anything else — a finished job, or an answer-mode call, which * has no output stream and must not read as "silent". * * ⚠ The zero-output line says why silence alone proves nothing: `claude -p` buffers its whole * answer until exit. Without that, a caller reading "silent for 300 s" on the free pool would cancel * a healthy run — the false failure the backlog item names as worse than an honest slow status. */ export declare function describeActivity(job: LaneJob, now: number): string | null; /** * What a caller is told when the walk tried every lane it had and none of them answered. * * ⚠ **This IS the last rung of the ladder.** The owner's request ends *"until finally reaching the * base agent's own subagents"*, and the relay cannot start the caller's subagent — it decides * ORDER, the host executes, which is the standing boundary this project keeps everywhere else. So * the final fallback is an ANSWER, and the text carries the whole instruction: what to do now, and * what NOT to do. Without the second half a caller retries `dispatch` for the same task, which is * the loop this feature exists to end. * * Pinned by `test/dispatch-lane-walk.test.ts` on its CLAIMS rather than its wording — reword it * freely, but change the assertion deliberately instead of deleting it. (This said * `test/mcp-server.test.ts` until an independent closeout audit caught it on 2026-09-08; the * assertions never lived there. A citation that sends the reader to the wrong file is the exact * failure the repository's cite-symbols-not-line-numbers rule exists to avoid.) */ export declare const LANE_LADDER_EXHAUSTED_ADVICE: string; /** * What a caller is told when lanes REMAIN untried — the walk stopped at its own `maxLanes` bound. * * ⚠ This exists because the advice above was firing on a walk that had not exhausted anything * (found by adversarial review, 2026-09-06). Both of its sentences were then false: lanes remained, * and "it would pick the same lanes" is wrong precisely because the walk has just DEMOTED every * lane it tried, so the next dispatch reorders around them. Telling an autonomous caller to stop * delegating, on a false premise, abandons capacity that was never contacted. */ export declare const LANE_LADDER_PARTIAL_ADVICE: string; /** * What a caller is told when the walk STOPPED an idle lane and nothing answered after. * * ⚠ The stopped lane did not fail on its own: the walk stopped it because it showed no activity the * relay could see. So "every dispatch lane has now been tried" is false there, and would end the * caller's use of dispatch for a task the lane might still finish * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §5). A NAMED lane is never stopped for idleness, * so this names the call that lets it run to its own timeout. */ export declare function laneStoppedAdvice(laneId: string): string; /** * What a caller is told when it NAMED the lane or the model and that one lane did not answer. Only * that lane ran, so "every dispatch lane has now been tried" would be false — measured 2026-09-10 on * jobs 0023 and 0024, which each ran one forced lane and were told to stop delegating. */ export declare const FORCED_LANE_ADVICE: string; /** Loopback broker status cadence while the original MCP process still owns the walk. */ export declare const BROKER_STATUS_POLL_MS = 1000; /** How often the walk checks a running lane for activity. */ export declare const IDLE_POLL_MS = 15000; /** * MCP clients (`initialize` `clientInfo.name`) measured to survive a long tool call, so `dispatch` * may wait for the answer the way the host's own subagent does, up to `routing.mcp.blockingWaitMs`. * * Measured 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code 2.1.237 (`claude-code`) * completed a 240 s tool call headless, with and without progress. Two hosts are deliberately NOT * here: the Claude desktop chat client (`claude-ai`) cancelled at exactly 60 s, and Codex runs a * call inside a code-mode `exec` that yields at 31 s. An unknown client keeps `maxWaitMs`. */ export declare const BLOCKING_WAIT_CLIENTS: readonly string[]; /** * MCP clients whose tool-call limit is known and above `maxWaitMs`, but which send no progress * token and never reset that limit. `dispatch` waits up to this figure for them, with no progress. * * `claude-ai` is the Claude Desktop app (read from its 2.110.1 bundle, 2026-09-17). It calls a local * server on two paths: desktop chat passes a 300 s timeout, and a Code tab session passes none, so * the MCP SDK default of 60 s applies. The server cannot tell the two paths apart, so the figure * stays 10 s under the smaller one. `blockingWaitMs: 0` turns this off too. */ export declare const HOST_WAIT_CEILING_MS: Readonly>; /** How often a blocking `dispatch` sends `notifications/progress`. */ export declare const PROGRESS_INTERVAL_MS = 30000; export declare class McpDispatchServer { private readonly deps; private readonly jobs; private readonly spawn; private readonly fetchImpl; private readonly now; private readonly cwd; private readonly maxDepth; private buffer; /** `clientInfo.name` from `initialize`; undefined until the host sends it. */ private clientName; /** In-flight `tools/call` requests the host may cancel, by request id. */ private readonly cancellable; /** The `git status` each running agent-mode job started from, with the caller's scope. */ private readonly trees; /** The activity tag (`lane-activity.ts`) of the lane each running job has started last. */ private readonly activityTags; /** Last cumulative process-tree CPU reading for the current attempt of each locally-owned job. */ private readonly processCpu; /** Last daemon-reported cumulative CPU reading for each recovered broker execution. */ private readonly brokerCpu; /** Latest broker snapshot for a fresh daemon-owned attempt in this still-running MCP walk. */ private readonly liveBrokerSnapshots; /** Full restart enrichment; status/result wait for final tree fidelity. */ private readonly startup; /** Broker orphan claiming/reconciliation only; explicit cancel waits on this, never on git. */ private readonly brokerStartup; constructor(deps: McpServerDeps); /** * Feed raw stdin bytes. Complete messages are handled concurrently; a partial tail is carried * over. Resolves once every handler in THIS chunk has settled. * * ⚠ Deliberately not `async`: the split runs to completion before the first handler starts, so * two calls in flight at once cannot interleave or reorder the buffer — message order is the * write order whatever the caller awaits. `serve` depends on exactly that. */ ingest(chunk: string): Promise; /** * Serve a stream of stdin chunks until the source ends. * * ⚠ Reads every chunk the moment it arrives and NEVER awaits a handler. Until 2026-09-05 * `cli.ts` ran `for await (chunk) { await server.ingest(chunk) }`, and `ingest` resolves only * when every handler in the chunk has settled — so a `tools/call` written while another was in * flight was not even READ until the first returned. Two requests were concurrent only when * they landed in the same chunk; a host that issues parallel tool calls in separate writes * (Claude Code does) had its second `dispatch` wait behind the first's full `waitMs`, and * `dispatch_status` / `dispatch_cancel` could not reach a job while a blocking `dispatch` held * the loop. Each handler now settles on its own promise; the per-job wait/poll policy * (`awaitOrPoll`) is unchanged. Responses may therefore leave out of request order, which * JSON-RPC permits — ids correlate. * * A rejected `ingest` (a response write failed — the host closed the pipe mid-answer; every * handler error is already contained inside `handleLine`) is reported on stderr and never * takes the loop down. Resolves after the source ends AND every handler it started has settled. */ serve(source: AsyncIterable): Promise; /** * Kill every LOCALLY owned running child, leave daemon-owned D1 executions running, then flush the * finished-job archive. Explicit dispatch_cancel is the path that asks the daemon owner to stop a * broker execution; MCP/host shutdown is the lifetime boundary D1 is designed to survive. */ shutdown(): void; private send; private handleLine; private cancelRequest; private route; private initialize; private callTool; private runTool; /** * Append a notice to a tool reply when this process runs OLDER code than the version installed on * disk. The Claude desktop app keeps one MCP process alive across sessions and releases, and on * 2026-09-10 two processes from before v0.78.0 were still serving old code with nothing to say so. * Only a plain one-block text reply is touched; anything else passes through unchanged. A version * that cannot be read is unknown, and unknown adds nothing. */ private withVersionNotice; private toolLanes; /** * ⚠ A TERMINAL job's status IS its result. A native subagent hands its answer back the moment it * ends; a caller that polls `dispatch_status` and never thinks to call `dispatch_result` was * measured polling one finished job 2,023 times over 71 minutes (2026-09-16). So the first poll * that sees the job end already holds the answer; a running job keeps the short form. */ private toolStatus; private toolResult; /** * The newest jobs this machine's llm-relay MCP servers know, one line each, newest first — the * list a caller reads when it lost a jobId (a host that timed a call out destroys the handle). */ private recentJobs; /** * The reply for a job id this process cannot answer for. A job another host's server is still * running is NAMED as such — `unknown jobId` there sent callers to re-dispatch work that was in * progress. Not an error: the job exists, and its answer appears here once it ends. */ private unknownJob; private toolCancel; /** * The blocking-wait cap for this call, or null when the call gets the ordinary `maxWaitMs`. * Three conditions: the host is one measured to survive a long call, the call asked for progress * (so the host shows the wait and, per its documentation, resets its idle timer), and the * operator did not turn the blocking wait off. A host in `HOST_WAIT_CEILING_MS` instead gets its * known ceiling, with no progress and no token required. */ private blockingWaitFor; private toolDispatch; /** * The lanes this dispatch will try, best first, and how many selectable lanes it leaves untried. * * `view.order` is the ONE definition of selection order (`dispatch.ts`). With the walk turned off * — or with a view that carries no order — this collapses to EXACTLY the pre-walk behaviour: the * single lane the view named as `next`, tried once, and never stopped for idleness. * * ⚠ The `Array.isArray` test is a VERSION SKEW guard, not defensive noise. `buildView` reaches the * running daemon over HTTP, and a daemon started before this field existed answers without it — an * MCP child upgraded ahead of a long-running daemon is the normal state on a machine that starts * the relay at logon and leaves it up for days. Reading `.length` off `undefined` there would throw * on EVERY dispatch, turning a new optional field into a total outage. * * ⚠ A second skew guard: a daemon older than `requester=mcp` still offers a pass-through rung as * ready, and this process can never run one (`mcpPassThroughReason`). Such a lane is dropped from an * UNFORCED walk here; a caller who named it still reaches it and reads why it cannot run. */ private walkOrder; /** A pass-through relay lane — one this process cannot run (`walkOrder`). Never throws. */ private isPassThroughLane; /** * Try each lane in turn until one answers. THE JOB IS THE WALK. * * ⚠ That is the load-bearing choice, and it is forced by a measurement: an MCP client tool call * on this machine fails somewhere between 45 s and 100 s, and above that ceiling it destroys the * job handle as well (a filed machine-wide defect; five lanes were lost to it in one night). So * the walk cannot finish inside the blocking call. It continues in the background behind ONE * handle, `dispatch_status` reports the lane running now, and `attempts` records the rest. * Re-pointing the handle at each new lane instead would break polling outright. * * Never rejects: every failure a lane can produce is an ATTEMPT, and the walk decides what to do * with it. Only a caller error ends the walk early — see `refusal` on `LaneAttemptOutcome`. */ private runWalk; /** * Record the `git status` an agent-mode walk starts from (`tree-delta.ts`). Answer mode spawns no * harness for a relay lane, so it has nothing to compare. A cwd outside a git work tree records * nothing, and the answer then carries no delta block. */ private startTreeDelta; /** * Reconcile daemon-backed rows this MCP process atomically claimed from the running journal. * A row is materialized as RUNNING first, so transport/auth failure remains an honest * "recovery unavailable" state rather than a fabricated death. A reachable daemon 404 is the * only absence that becomes killed. */ private restoreBrokerOrphans; /** Refresh every recovered daemon execution before rendering the machine-wide recent-job list. */ private refreshAllBrokerJobs; /** * Refresh one daemon-owned job. Returns true when it was broker-backed, even if the broker is * temporarily unreachable. Never converts transport failure or malformed success JSON into death. */ private refreshBrokerJob; /** A broker outage is uncertainty, not evidence that the lane stopped. */ private noteBrokerRecoveryUnavailable; /** * Publish liveness for a recovered daemon execution. This process is only an observer/collector: * it never idle-stops or advances the old walk, hence the no-idle-stop verdict. */ private noteRecoveredBrokerLiveness; /** * Complete a killed job's tree report by comparing its journaled start with the tree as it exists * when this restarted server adopts the job. This can include edits made after the old process * died, so the result is explicitly labelled as an adoption-time measurement. */ private restoreKilledTreeDeltas; /** * Reset only the liveness baseline when the walk advances to another lane. The job-wide * `before` snapshot stays untouched so the final tree delta still reports every lane's edits. * A failed read removes the attempt baseline: the next successful poll establishes a baseline * and proves no activity by itself. */ private resetLaneTreeActivity; /** * Read the tree again and put the delta on the job, BEFORE the job goes terminal on the walk's own * paths so the archived record carries it. Report only: nothing here refuses or reverts. */ private recordTreeDelta; /** The snapshot reader, contained: a throwing injected reader reads as "no git tree". */ private readTree; /** * Is the lane at position `i` never stopped, even when idle? The last lane is not — there is * nowhere to move to. Nor is a lane whose later lanes are all unlikely to answer: each is on a * streak of `LANE_UNRELIABLE_STREAK` own failures or more, or marked `failing`. Stopping a lane * that may still answer in order to reach those trades an answer for a near-certain failure — * measured 2026-09-10, when the walk stopped `free-pool` to try lanes that had answered 0 of 12, * 0 of 34 and 0 of 21 runs (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §1). A lane with no * record counts as reliable: unmeasured is no opinion, never a failure. */ private stopWithheld; /** * A `cli` rung whose configured `maxConcurrent` this MCP server process has already reached for * — SKIP it for this walk rather than starting a competing process. Records the skip as an * attempt (so the walk's own "lanes tried" rendering shows it) and grows `lanesNotTried` (so a * walk that skips everything reports the PARTIAL advice, never the EXHAUSTED one — a skipped * rung "counts as NOT TRIED", not as a lane that ran and failed). Spawns nothing, demotes * nothing: no telemetry is forwarded for a skip, so `lane-affinity.ts` never hears about it. * * Only `cli` rungs carry a `maxConcurrent` at all (`dispatch.ts` `toLane`) — a `relay` lane's is * always `null`, so this returns `false` for one without needing a `kind` check of its own. */ private skipIfAtConcurrencyCap; /** The skips decided before a lane starts: its concurrency cap, then its declared capability. */ private skipBeforeStart; /** * A rung that declares a `capability` below this dispatch's tier — SKIP it, so the walk never * moves a packet to a weaker lane only because the operator listed it in that tier's ladder * (measured 2026-09-16: a `high` implementation packet was stopped on `free-pool` and restarted on * a lane kept for short advisory work). Recorded like a concurrency-cap skip. A caller who NAMED * the lane or a model still reaches it, and a tier or capability outside the effort vocabulary * limits nothing. */ private skipIfBelowTier; /** * The running lane's most recent ACTIVITY, or null when nothing was seen. Evidence: a request with * the lane's tag in flight at the relay daemon (that is activity NOW), the daemon's last traffic * for the tag, the lane's own last output, and its git working tree (a change since the last * reading is activity now; otherwise the newest dirty file's mtime). `claude -p` holds its output * until exit, so for a pool lane the daemon's traffic is the signal that matters. The tree is read * only when nothing newer than one poll was seen, because each reading runs `git status`. */ private latestActivity; /** * For a read-only dispatch, the invocation this lane will be spawned with — its own CLI's * read-only tool binding (`readOnlyInvoke`) — or a SKIP when its CLI offers none. Recorded exactly * like a concurrency-cap skip: an attempt in the walk's own record, `lanesNotTried` grown so the * terminal advice is PARTIAL rather than EXHAUSTED, nothing spawned, no telemetry, no demotion — * a lane the relay declined to run unbound has not failed. * * Not consulted at all for an ordinary dispatch, for an answer-mode relay call (no harness, so * nothing to bind), or for a lane with no invocation (the existing "cannot be run from here" path * reports that on its own). */ private bindReadOnlyOrSkip; /** * Run ONE lane. Returns what happened; never throws. * * With `idleMs === null` the lane is simply awaited — its own `timeoutMs` is the only bound. * Otherwise the walk checks the lane every `IDLE_POLL_MS`, and when it has shown no activity * (`latestActivity`) for `idleMs`, the lane is KILLED and reported as `abandoned`. How long the * lane has run does not matter: a slow lane that still works is never stopped. * * ⚠ It kills rather than hedges. The HTTP request path hedges — it starts a second attempt * beside the first — and that is deliberate there, bounded to free deployments by an owner * amendment. A lane hedge is a different trade: it spends two lanes' quota at once, and this * machine already carries a filed defect in which lane processes are never reaped and go on * burning processor time after their job returns. So the walk leaves nothing running behind it. */ private runOneLane; /** Broker client containment: an injected client may throw, production never should. */ private brokerRequest; private brokerSnapshotMatches; private brokerRun; /** Wait for a started/possibly-started daemon execution without ever duplicating it locally. */ private waitBrokerLane; /** Idempotently cancel one broker attempt before the walk advances to another rung. */ private stopBrokerLane; /** * Try daemon ownership. null = no broker client (legacy/embed); {fallback} = broker unavailable * BEFORE any start request, so a local spawn is safe; otherwise return the daemon-owned handle. */ private tryStartBrokerLane; /** * Start ONE lane and hand back its promise plus a kill handle. Two shapes, decided exactly as * `toolDispatch` used to decide them: a `relay` rung in answer mode is a direct HTTP call to * this relay's own `/v1/messages`; everything else is a spawned command. * * ⚠ A lane that cannot run HERE returns a failed OUTCOME, not a refusal, so the walk moves on. * Only a bad working directory is a `refusal`, because that is the caller's own error and every * remaining lane would hit it identically. */ private startLane; /** * Settle a spawned lane's run. A quota death the lane states — in its own output, or for an AGY * lane in AGY's own log — is reported to the relay; a content-empty answer is a failure. * * ⚠ It also runs for a lane the walk STOPPED: the killed child still settles here, so a death AGY * stated only in its log reaches the relay although the walk has moved on. The outcome itself is * discarded in that case — `runOneLane` already returned `abandoned`. */ private settleSpawnedRun; /** * The quota death an AGY lane stated only in AGY's own log (`agy-quota-log.ts`). AGY retries a * spent quota in silence, so a lane stopped by the walk or by its own timeout printed nothing, and * the death reached the relay only when a run happened to last its whole length * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4). Undefined unless the lane is AGY, names its * model, and the log is provably this run's — `agyQuotaStatement` refuses everything else. */ private agyLogReport; /** * Forward one lane-execution report per ATTEMPT, for both lane kinds: the daemon records the * lane's stats and routing memory from these, and skips the ledger row for a `relay` lane on its * own because the HTTP pipeline already accounts it. Never for a cancelled job, and never for an * ad-hoc lane (`DispatchLane.adHoc`), which the daemon has no rung to record against. * * Fire-and-forget by design: the reporter is never awaited on the response path, and a * throwing or rejecting reporter is swallowed after ONE metadata-only stderr line (lane id * and job id; never the task text), so forwarding can never change the dispatch result, * the job, or the stdio protocol. */ private forwardTelemetry; /** * The one HTTP round trip answer mode makes. Never throws for an ordinary outcome — a non-2xx * response, an unparseable body, or a timeout are all represented in the returned * `LaneRunResult`, exactly as a spawned lane's own nonzero exit or timeout would be. Only a * genuine transport error (the relay is not running, DNS failure, …) propagates, for the * caller's `.catch()` to turn into `jobs.fail()` — the same shape a spawn that throws * synchronously already produces. */ private runAnswerFetch; /** * Block for the resolved `waitMs`, then hand back a job handle. A fast lane therefore costs * ONE call, and a slow one degrades to polling instead of hitting the client's tool timeout. * Shared by both dispatch modes so there is exactly one wait/poll/render policy — the "two * paths, one policy" shape this repo's own history warns against. * * When the caller's `waitMs` was clamped to the ceiling, `clamp` carries the ask and the * ceiling and the reply announces it on its own line — the SAME code path that renders * "Still running after N s", so the announcement can never drift from the wait it explains. * Without a clamp the text below is byte-for-byte what it always was. */ private awaitOrPoll; /** * One `notifications/progress` for a blocking `dispatch`. `progress` is the elapsed whole seconds, * which the specification requires to increase; `total` is omitted because nothing states it. */ private sendProgress; } /** The first non-empty line of a task, cut to 80 characters — enough to recognise a job in a list. */ export declare function taskLabel(task: string): string;