fix(backend): echo Pi's RPC id so reviewer gates unblock after answer
ctx.ui.input in `pi --mode rpc` correlates extension_ui_response on its own
top-level RPC id (crypto.randomUUID), not the descriptor id the gate carries
in `title`. SessionBridge replied with the descriptor id, so Pi silently
dropped the response and the model never resumed — every reviewer widget hung
after the human answered.
SessionBridge now stores Pi's top-level m.id (pendingPiId) and replies
extension_ui_response{ id: pendingPiId, value: <uiResponse> }; value still
carries the descriptor id so the gate's internal resp.id === descriptor.id
check still holds.
The fake-pi double had masked the bug by forcing m.id == descriptor.id; it now
mirrors real Pi (distinct randomUUID, correlate on it, drop unknown ids), with
a negative regression test. SKILL.md Phase 1 also now steers multi-answer
disambiguation to reviewer_decide (multiselect).
Tests: backend 67/67, tsc clean, fake-pi contract 2/2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,23 +28,29 @@ test("real Pi message_update (assistantMessageEvent text_delta) becomes a text_d
|
||||
expect(seen).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("extension_ui_request nativo (method:input, title=json) diventa ui_request ed è il pendente", () => {
|
||||
test("extension_ui_request nativo (method:input, title=json) diventa ui_request col descriptor ed è il pendente", () => {
|
||||
const { rpc, fire } = fakeRpc();
|
||||
const b = new SessionBridge(rpc);
|
||||
const seen: any[] = [];
|
||||
b.onClientEvent((e) => seen.push(e));
|
||||
const descriptor = { id: "u1", widget: "select" };
|
||||
fire({ type: "extension_ui_request", id: "u1", method: "input", title: JSON.stringify(descriptor) });
|
||||
// Pi assegna a ctx.ui.input un proprio id RPC (crypto.randomUUID), distinto dall'id
|
||||
// interno del descriptor che il gate mette nel `title`. Al frontend va il descriptor.
|
||||
fire({ type: "extension_ui_request", id: "pi-req-1", method: "input", title: JSON.stringify(descriptor) });
|
||||
expect(seen[0]).toEqual({ type: "ui_request", ui_request: descriptor });
|
||||
expect(b.pendingWidget()).toEqual(descriptor);
|
||||
});
|
||||
|
||||
test("respond invia extension_ui_response con payload in value e azzera il pendente", () => {
|
||||
test("respond correla sull'id RPC di Pi (non sull'id del descriptor) e azzera il pendente", () => {
|
||||
const { rpc, sent, fire } = fakeRpc();
|
||||
const b = new SessionBridge(rpc);
|
||||
fire({ type: "extension_ui_request", id: "u1", method: "input", title: JSON.stringify({ id: "u1", widget: "select" }) });
|
||||
// Pi emette la richiesta con il SUO id RPC ("pi-req-1"); il descriptor nel title ha id "u1".
|
||||
fire({ type: "extension_ui_request", id: "pi-req-1", method: "input", title: JSON.stringify({ id: "u1", widget: "select" }) });
|
||||
// Il frontend rimanda l'id del descriptor ("u1").
|
||||
b.respond({ id: "u1", choices: ["a"] });
|
||||
expect(sent.at(-1)).toEqual({ type: "extension_ui_response", id: "u1", value: JSON.stringify({ id: "u1", choices: ["a"] }) });
|
||||
// Pi correla la risposta sul SUO id ("pi-req-1") per risolvere ctx.ui.input; il value
|
||||
// continua a portare l'id del descriptor, cosi' il check interno del gate regge.
|
||||
expect(sent.at(-1)).toEqual({ type: "extension_ui_response", id: "pi-req-1", value: JSON.stringify({ id: "u1", choices: ["a"] }) });
|
||||
expect(b.pendingWidget()).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user