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:
@@ -8,6 +8,11 @@ export type ClientEvent =
|
||||
|
||||
export class SessionBridge {
|
||||
private pending: any = null;
|
||||
// Pi (rpc-mode createDialogPromise) assegna a ogni ctx.ui.input un id RPC PROPRIO
|
||||
// (crypto.randomUUID) e correla extension_ui_response su quell'id — NON sull'id interno
|
||||
// del descriptor (che viaggia opaco nel `title`). Va memorizzato e rimandato indietro,
|
||||
// altrimenti Pi scarta la risposta e ctx.ui.input non si risolve mai (stuck senza output).
|
||||
private pendingPiId: string | null = null;
|
||||
private cbs = new Set<(e: ClientEvent) => void>();
|
||||
|
||||
constructor(private rpc: RpcClient) {
|
||||
@@ -16,6 +21,7 @@ export class SessionBridge {
|
||||
let descriptor: any;
|
||||
try { descriptor = JSON.parse(m.title); } catch { return; }
|
||||
this.pending = descriptor;
|
||||
this.pendingPiId = m.id;
|
||||
this.fan({ type: "ui_request", ui_request: descriptor });
|
||||
} else if (m.type === "extension_ui_request" && m.method === "notify") {
|
||||
this.fan({ type: "info", level: m.notifyType ?? "info", text: m.message ?? "" });
|
||||
@@ -38,8 +44,11 @@ export class SessionBridge {
|
||||
onClientEvent(cb: (e: ClientEvent) => void): void { this.cbs.add(cb); }
|
||||
|
||||
respond(uiResponse: object & { id: string }): void {
|
||||
this.rpc.send({ type: "extension_ui_response", id: uiResponse.id, value: JSON.stringify(uiResponse) });
|
||||
if (this.pending && uiResponse.id === this.pending.id) this.pending = null;
|
||||
// Correla sull'id RPC di Pi; `value` porta l'uiResponse (con l'id del descriptor) cosi'
|
||||
// il check interno del gate (resp.id === descriptor.id) regge.
|
||||
const piId = this.pendingPiId ?? uiResponse.id;
|
||||
this.rpc.send({ type: "extension_ui_response", id: piId, value: JSON.stringify(uiResponse) });
|
||||
if (this.pending && uiResponse.id === this.pending.id) { this.pending = null; this.pendingPiId = null; }
|
||||
}
|
||||
|
||||
steer(text: string): void { this.rpc.send({ type: "steer", message: text }); }
|
||||
|
||||
Reference in New Issue
Block a user