Repository navigation
beforeInference names no server it skips, and leaves each call's 5 s timer armed after the call answers #736
Copy link
Copy link
Open
Description
Two small things in HookOrchestrator's beforeInference fan-out, src/mcpl/hook-orchestrator.ts at main 9fdc3a65:
- A server whose call rejects or times out is skipped without a line. The fan-out skips it with a bare
continue(:213-216), so the fan-out never says whose context is missing from a turn. A malformed answer, by contrast, gets a line naming its server (:237). - Each call's 5 s timer stays armed after the call answers.
withTimeout(:48-55) races the call against asetTimeoutit never clears. So each fan-out leaves one timer per server armed for up to 5 s, and a process with nothing else to do can't exit until they fire.
A run at main 9fdc3a65 (Linux, Node 24.22.1) called beforeInference directly, with server stubs:
- Three servers:
fastanswers at once,failsrejects with "transport closed", andslowanswers after 6 s. The hook returned after 5,005 ms withfast's injection alone, and printed nothing to stderr. - One server,
fast: the hook returned after 1 ms. One timer was still active (process.getActiveResourcesInfo()), and the process exited 5,005 ms after the hook returned, with nothing else to do.
A shape to weigh: the skip logs one line naming the server and whether it timed out or failed, as the malformed answer's does, and withTimeout clears its timer once the call settles.
Kestrel-2409 found both by reading, in our internal review of #610.
The probe and its output
// Probe (Barnaby-2576), F397 and F398: `HookOrchestrator.beforeInference`
// with three servers: `fast` answers at once, `fails` rejects (a transport
// error), and `slow` answers after 6 s, past the 5 s timeout. The probe
// prints what beforeInference returns and how long it took, every line it
// printed to stderr, and, once it returned, how many timers the process
// still holds (process.getActiveResourcesInfo) and how long until the
// process could exit, for the hook alone and for a hook that answered at once.
// Run with tsx from the agent-framework checkout's root.
import { HookOrchestrator } from '../src/mcpl/hook-orchestrator.js';
import { CapabilityGrant, ALL_CAPABILITY_PATHS } from '../src/mcpl/capability-grant.js';
import type { McplServerRegistry } from '../src/mcpl/server-registry.js';
import type { FeatureSetManager } from '../src/mcpl/feature-set-manager.js';
console.log(`node ${process.version}, ${process.platform}`);
const mode = process.argv[2] ?? 'mixed';
const grant = () => new CapabilityGrant(new Set(ALL_CAPABILITY_PATHS), []);
const answer = (text: string) => ({ featureSet: 'fs', contextInjections: [{ namespace: 'n', position: 'system', content: [{ type: 'text', text }] }] });
const servers = mode === 'mixed'
? [
{ id: 'fast', grant: grant(), sendBeforeInference: async () => answer('from fast') },
{ id: 'fails', grant: grant(), sendBeforeInference: async () => { throw new Error('transport closed'); } },
{ id: 'slow', grant: grant(), sendBeforeInference: () => new Promise((r) => setTimeout(() => r(answer('from slow')), 6000).unref()) },
]
: [{ id: 'fast', grant: grant(), sendBeforeInference: async () => answer('from fast') }];
const orchestrator = new HookOrchestrator(
{ getAllServers: () => servers } as unknown as McplServerRegistry,
{ validateInbound() {} } as unknown as FeatureSetManager,
);
const lines: string[] = [];
const original = console.error;
console.error = (...args: unknown[]) => { lines.push(args.map(String).join(' ')); };
const t0 = Date.now();
const injections = await orchestrator.beforeInference({
inferenceId: 'i', conversationId: 'c', turnIndex: 0, userMessage: null, model: { id: 'test' } as never,
} as never);
const took = Date.now() - t0;
console.error = original;
console.log(`\n== ${mode === 'mixed' ? 'servers fast, fails (rejects) and slow (answers after 6 s)' : 'one server, fast'}`);
console.log(`beforeInference returned after ${took} ms with ${injections.length} injection(s): ${JSON.stringify(injections.map((i) => (i.content as Array<{ text?: string }>)[0]?.text))}`);
console.log(`stderr lines during the hook: ${JSON.stringify(lines)}`);
const timers = () => process.getActiveResourcesInfo().filter((r) => r === 'Timeout').length;
console.log(`timers held right after it returned: ${timers()}`);
const exitStart = Date.now();
process.on('exit', () => {
console.log(`process exited ${Date.now() - exitStart} ms after the hook returned, with nothing else to do`);
});With mixed:
node v24.22.1, linux
== servers fast, fails (rejects) and slow (answers after 6 s)
beforeInference returned after 5005 ms with 1 injection(s): ["from fast"]
stderr lines during the hook: []
timers held right after it returned: 0
process exited 0 ms after the hook returned, with nothing else to do
With fast:
node v24.22.1, linux
== one server, fast
beforeInference returned after 1 ms with 1 injection(s): ["from fast"]
stderr lines during the hook: []
timers held right after it returned: 1
process exited 5005 ms after the hook returned, with nothing else to do
Written by Barnaby-2576 (Claude Opus 5.5), filed by Lottie-2661.
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity