diff --git a/apps/opencode-plugin/native-commands.test.ts b/apps/opencode-plugin/native-commands.test.ts index ac3eede9fb..2b885a039a 100644 --- a/apps/opencode-plugin/native-commands.test.ts +++ b/apps/opencode-plugin/native-commands.test.ts @@ -545,6 +545,10 @@ describe("V2 session URL delivery", () => { // Regression: without `resume: false` upstream calls `execution.wake` // (packages/core/src/session/session.ts), so merely showing a URL would start // a model turn the reviewer never asked for and burn tokens on every command. + // #1459 extension: resume: false only defers the immediate wake; the host + // default delivery is "steer", which any later wake (including spurious + // idle wakes on OpenCode 2 betas) promotes into its own model turn. The + // notice must therefore also pin queue delivery. test("the notice never wakes a model turn", async () => { const { synthetic, ctx } = makeSyntheticCtx(); const client = createV2BridgeClient({ ctx, getAgents: async () => [], sessionID: "session-1" }); @@ -552,7 +556,7 @@ describe("V2 session URL delivery", () => { pushUrlLine(client); await Promise.resolve(); - expect(synthetic.mock.calls[0]![0]).toMatchObject({ resume: false }); + expect(synthetic.mock.calls[0]![0]).toMatchObject({ resume: false, delivery: "queue" }); }); // Regression: `session.synthetic` is absent on older V2 hosts, and a session diff --git a/apps/opencode-plugin/server.test.ts b/apps/opencode-plugin/server.test.ts index c132f500da..873ad88ae9 100644 --- a/apps/opencode-plugin/server.test.ts +++ b/apps/opencode-plugin/server.test.ts @@ -401,6 +401,8 @@ describe("V2 plan review URL delivery", () => { sessionID: "session-1", description: formatSessionUrlNotice(SESSION_URL), resume: false, + // #1459: queue delivery keeps the notice out of steer-scoped promotion. + delivery: "queue", }); }); diff --git a/apps/opencode-plugin/v2-client.ts b/apps/opencode-plugin/v2-client.ts index 3d3f7b2e95..91270d0d0d 100644 --- a/apps/opencode-plugin/v2-client.ts +++ b/apps/opencode-plugin/v2-client.ts @@ -247,8 +247,12 @@ export function formatSessionUrlNotice(url: string): string { * - Setting no `metadata.source` keeps it on the plain "Notice" row rather * than the subagent/shell completion row. * - * `delivery` is left at the host default, matching upstream's own synthetic - * notices; only feedback (`FEEDBACK_DELIVERY`) needs an explicit queue. + * `delivery` is an explicit "queue", mirroring `FEEDBACK_DELIVERY` (#1459). + * The host default resolves to "steer", and a pending steer row is promoted + * FIRST by any wake, including spurious idle wakes observed on OpenCode 2 + * betas where `resume: false` defers the immediate wake but a later wake + * turns the notice into its own model turn. Queue delivery keeps the notice + * out of every steer-scoped promotion and is a no-op on well-behaved hosts. * * This does not contradict the reason feedback avoids synthetic injection. * Upstream #44788 is about a synthetic message not reliably reaching the MODEL @@ -267,7 +271,14 @@ export function createSessionUrlNotifier( if (typeof synthetic !== "function" || !sessionID) return undefined; return async ({ url }) => { const notice = formatSessionUrlNotice(url); - return await synthetic({ sessionID, text: notice, description: notice, resume: false }); + return await synthetic({ + sessionID, + text: notice, + description: notice, + resume: false, + // #1459: never ride the "steer" default; see the delivery note above. + delivery: FEEDBACK_DELIVERY, + }); }; }