🐛 Resolve the in-process singleton when avenor_ask omits supervisor_id - #196
Conversation
getSupervisorClient treated supervisorId as required, so avenor_ask — whose tool schema never exposed supervisor_id — always failed with `The "path" argument must be of type string. Received undefined` from path.dirname(undefined) inside validateSupervisorSocketPath. Resolve the in-process singleton (Supervisor.get()) when supervisorId is omitted, matching the fallback spawn/result already use. Also expose the optional supervisor_id on avenor_ask for parity with avenor_reply.
pi serialises AgentMessage fields in camelCase (stopReason) with
OpenAI-style values ("stop" for a successful turn), but the agent_end
translator read the snake_case alias only and passed the value through
unmapped. Every completed pi run therefore produced a session.end with
no stop_reason, WaitForSession returned the generic exit code 1, and
runChild classified the successful turn as a retryable failure:
- runs terminated immediately with a FAILED sentinel (STOP_REASON=exit_1)
even though the agent answered fine — the watch-dialog follow-up
"immediately ends the run";
- while the runtime idled awaiting prompts, the session attempt stayed
owned, so avenor_follow_up's spawn-resume was rejected with "session
ID is already owned by another active provider attempt";
- no DONE sentinel meant sentinel-based resume found nothing to read.
Read stopReason first (keep the snake_case alias), and normalise pi's
raw values (stop→end_turn, length→max_tokens, aborted→cancelled,
end_of_turn→end_turn) so successful turns exit 0, write DONE sentinels,
release the session, and make avenor_follow_up work.
Also gate the e2e follow-up test's sentinel waits on real-integration
mode (the fake fixture never writes sentinels) and wait for run
completion before resuming, which the session-ownership guard requires.
Split the stopReason mapping table into t.Run subtests for isolated failure reporting, and pin the passthrough contract for unmapped values (toolUse, unknown_reason) so a future remapping is a conscious choice.
|
Added two commits fixing the follow-up failures (root cause found and verified end-to-end against the real binary): Root cause: pi's
Fix ( Tests ( Validation: |
Why
avenor_askfailed on every call that omittedsupervisor_id. The tool schema never exposed that parameter, soaskToolalways passedsupervisorId: undefinedintogetSupervisorClient, which fed it tovalidateSupervisorSocketPath.path.dirname(undefined)threwThe "path" argument must be of type string. Received undefined— the agent saw a tool error regardless of what arguments it supplied.Changes
getSupervisorClientnow acceptssupervisorId: string | undefined. An omitted id resolves the in-process singleton throughSupervisor.get()— the same fallbackspawn.tsandresult.tsalready use — instead of validating an undefined socket path. This also coversbroker-replyandcancel, which pass optional ids through the same helper.avenor_askexposes the optionalsupervisor_idparameter, matchingavenor_reply, so the tool can also target an external supervisor.askToolwith nosupervisorIdagainst a patchedSupervisor.getsingleton, and asserts the singleton client is not closed in thefinallyblock.Validation
bun test→ all packages 0 fail (ask.test.ts: 6 pass, including the new regression test). Verified the regression test fails against the pre-fix source (git stashofget-supervisor-client.ts) and passes with the fix.bun run buildinpackages/core→ tsdown clean.Notes
When no supervisor runs in-process and
supervisor_idis omitted,Supervisor.get()starts a lazy singleton rather than erroring, consistent withresult.ts. A caller that needs a different supervisor still passes an explicit socket path.Review triage: two parallel reviewers (general + tests) ran on the diff. All Must/Should findings were dropped after verification — the events.ts "incomplete type annotation" claim is refuted by the code (the inline annotation at events.ts:126-127 already declares
supervisorId?: string), and the remaining findings flagged pre-existing code outside the diff or patterns that match existing repo conventions (as anyon supervisor mocks,afterAllrestore, shared description strings). No open findings.