fix(hub): forward discarded pending prompts to hub clients

The pending_prompts snapshot that empties a queue on teardown carries
the removed entries in a `discarded` field so a subscribed client can
offer the text back, but both ends of the hub boundary rebuilt the
payload as { sessionId, prompts }: the session event projector when
publishing session.pending_prompts, and HubRuntimeHost when turning it
back into a core event. No hub client and nothing behind HubRuntimeHost
ever saw the field. Both now pass it through when present.
This commit is contained in:
Mikołaj Kondratek
2026-10-23 12:04:52 +02:00
parent a1fbef4b70
commit 0b23837be8
3 changed files with 82 additions and 0 deletions
@@ -187,6 +187,81 @@ describe("HubRuntimeHost", () => {
});
});
it("carries discarded pending prompts across the hub boundary", async () => {
let onEvent: ((event: HubEventEnvelope) => void) | undefined;
subscribeMock.mockImplementation((listener) => {
onEvent = listener;
return () => {};
});
commandMock.mockResolvedValue({
payload: {
session: {
sessionId: "sess-1",
status: "running",
createdAt: Date.now(),
updatedAt: Date.now(),
workspaceRoot: "/tmp/project",
cwd: "/tmp/project",
},
},
});
const events: unknown[] = [];
const { HubRuntimeHost } = await import("./hub-runtime-host");
const host = new HubRuntimeHost({ url: "ws://127.0.0.1:25463/hub" });
host.subscribe((event) => events.push(event));
await host.startSession({
config: createConfig(),
source: SessionSource.CLI,
prompt: "Hey",
});
const ctx = {
sessionHost: {
getAccumulatedUsage: vi.fn().mockResolvedValue(undefined),
},
buildEvent: vi.fn<HubTransportContext["buildEvent"]>(
(event, payload, sessionId) => ({
version: "v1",
event,
payload,
sessionId,
}),
),
publish: (event: HubEventEnvelope) => {
onEvent?.(JSON.parse(JSON.stringify(event)));
},
} as unknown as HubTransportContext;
const discarded = [
{
id: "pending-1",
prompt: "typed before the task was closed",
delivery: "queue" as const,
attachmentCount: 0,
},
];
// The snapshot that empties the queue on teardown must still tell a
// hub client what was removed, or it cannot offer the text back.
await projectSessionEvent(ctx, {
type: "pending_prompts",
payload: { sessionId: "sess-1", prompts: [], discarded },
});
await projectSessionEvent(ctx, {
type: "pending_prompts",
payload: { sessionId: "sess-1", prompts: [] },
});
expect(events).toEqual(
expect.arrayContaining([
{
type: "pending_prompts",
payload: { sessionId: "sess-1", prompts: [], discarded },
},
{
type: "pending_prompts",
payload: { sessionId: "sess-1", prompts: [] },
},
]),
);
});
it("reconstructs tool content updates from hub events", async () => {
let onEvent: ((event: HubEventEnvelope) => void) | undefined;
subscribeMock.mockImplementation((listener) => {
@@ -2008,6 +2008,9 @@ export class HubRuntimeHost implements RuntimeHost {
return;
}
case "session.pending_prompts": {
const discarded = Array.isArray(event.payload?.discarded)
? (event.payload.discarded as SessionPendingPrompt[])
: [];
this.events.emit({
type: "pending_prompts",
payload: {
@@ -2015,6 +2018,7 @@ export class HubRuntimeHost implements RuntimeHost {
prompts: Array.isArray(event.payload?.prompts)
? (event.payload.prompts as SessionPendingPrompt[])
: [],
...(discarded.length ? { discarded } : {}),
},
});
return;
@@ -71,6 +71,9 @@ export async function projectSessionEvent(
{
sessionId: event.payload.sessionId,
prompts: event.payload.prompts,
...(event.payload.discarded?.length
? { discarded: event.payload.discarded }
: {}),
},
event.payload.sessionId,
),