From fd2c4d5012bd287b7097002c30b4d0ad0b1c35d5 Mon Sep 17 00:00:00 2001 From: Duncan Date: Mon, 19 Oct 2026 16:20:32 -0400 Subject: [PATCH 1/2] fix(goose): stop posting replies the agent did not send Every agent is told its assistant text is not published and it must use send, so Goose posting the unsent final text published private narration and broke deliberate silence. Claude Code and Codex never post it; the text now stays in local activity for Goose too. Signed-off-by: Duncan --- src/bundled/goose/runtime.test.ts | 52 ++++++++++++++----------------- src/bundled/goose/runtime.ts | 7 ++--- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/bundled/goose/runtime.test.ts b/src/bundled/goose/runtime.test.ts index 4d1cdc0839..23a9040b9d 100644 --- a/src/bundled/goose/runtime.test.ts +++ b/src/bundled/goose/runtime.test.ts @@ -240,6 +240,22 @@ describe("GooseRuntime", () => { ]); }); + it("posts nothing goose wrote without `send`", async () => { + // Text the agent writes outside `send` is narration, not a reply. + for (const reply of ["I've replied to Will.", "Plain answer"]) { + const { deliver, published } = setup({ reply }); + let activity: Promise = Promise.resolve(); + await deliver(message("1", "@Gander hi"), { + workingUntil: (done: Promise) => { + activity = done; + }, + }); + await flush(100); + expect(published, reply).toEqual([]); + expect(await activity, reply).toMatchObject({ ok: true, text: reply }); + } + }); + it("tells a subscriber when a new conversation's first turn starts working", async () => { const { deliver, goose, runtime } = setup({ hold: true }); // As a view does: each notification reads the snapshot again. @@ -268,7 +284,6 @@ describe("GooseRuntime", () => { const { deliver, goose, published } = setup({ scope: "channel", hold: true, - reply: "Forgot to send", }); await deliver(reply("3", one)); await flush(100); @@ -277,17 +292,14 @@ describe("GooseRuntime", () => { const process = goose(); expect(process?.turns).toHaveLength(1); expect(process?.turns[0]?.steered).toHaveLength(1); - process?.turns[0]?.finish(); + process?.exit(1, "crashed\n"); await flush(100); - expect(published).toEqual([ - { - kind: 9, - content: "Forgot to send", - tags: [ - ["h", channel], - ["e", two.id, "", "reply"], - ], - }, + expect(published.map((event) => event.tags)).toEqual([ + [ + ["h", channel], + ["e", two.id, "", "reply"], + ["p", alice], + ], ]); }); @@ -304,24 +316,6 @@ describe("GooseRuntime", () => { expect(memoryOpen).not.toHaveBeenCalled(); }); - it("posts goose's own text in the thread, or at a Solo chat's top level", async () => { - const plain = setup({ reply: "Plain answer" }); - await plain.deliver(message("1", "@Gander hi")); - await flush(100); - expect(plain.published.map((event) => event.tags)).toEqual([ - [ - ["h", channel], - ["e", "1".repeat(64), "", "reply"], - ], - ]); - const solo = setup({ reply: "Plain answer", solo: new Set([channel]) }); - await solo.deliver(message("1", "@Gander hi")); - await flush(100); - expect(solo.published.map((event) => event.tags)).toEqual([ - [["h", channel]], - ]); - }); - it("names the agent in a timer's prompt, so its session is listed as the agent's", async () => { const { runOnce, goose } = setup(); await expect( diff --git a/src/bundled/goose/runtime.ts b/src/bundled/goose/runtime.ts index 0d2f12eba9..a6cffc9a0d 100644 --- a/src/bundled/goose/runtime.ts +++ b/src/bundled/goose/runtime.ts @@ -316,13 +316,10 @@ export class GooseRuntime { this.notify(); void done .then((result) => { - // Of the messages one turn answered, only one has its error or text. + // Of the messages one turn answered, only one has its error. An + // answer goose wrote without `send` stays in local activity. if (result.error !== undefined) return this.report(agent, channel, event, result.error, topLevel); - // An answer goose wrote without `send` is posted for it, so the - // thread is not left waiting. - if (result.text) - return this.post(agent, channel, event, result.text, topLevel); }) .catch((error) => console.warn("goose turn failed", error)) .finally(() => this.notify()); From ada3f60fb93a4e7bca579e3d0e10d0995fd8b647 Mon Sep 17 00:00:00 2001 From: Duncan Date: Mon, 19 Oct 2026 17:59:12 -0400 Subject: [PATCH 2/2] test(goose): wait for the report step before checking nothing posted The no-post check ran after a fixed microtask flush, so a post made later in the report step could land after the assertion. Signed-off-by: Duncan --- src/bundled/goose/runtime.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/bundled/goose/runtime.test.ts b/src/bundled/goose/runtime.test.ts index 23a9040b9d..ac9fcea1c9 100644 --- a/src/bundled/goose/runtime.test.ts +++ b/src/bundled/goose/runtime.test.ts @@ -243,16 +243,22 @@ describe("GooseRuntime", () => { it("posts nothing goose wrote without `send`", async () => { // Text the agent writes outside `send` is narration, not a reply. for (const reply of ["I've replied to Will.", "Plain answer"]) { - const { deliver, published } = setup({ reply }); + const { deliver, published, runtime } = setup({ reply }); let activity: Promise = Promise.resolve(); await deliver(message("1", "@Gander hi"), { workingUntil: (done: Promise) => { activity = done; }, }); - await flush(100); - expect(published, reply).toEqual([]); expect(await activity, reply).toMatchObject({ ok: true, text: reply }); + // The turn's report step ends by notifying, after any post it makes. + await new Promise((resolve) => { + const stop = runtime.subscribe(() => { + stop(); + resolve(); + }); + }); + expect(published, reply).toEqual([]); } });