From 67727c8de5cc5dc5dc5cd9082c2958e29bb42f90 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 17 Jul 2026 08:03:03 +0000 Subject: [PATCH] fix(session): resumed queued messages after compaction reconnects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #5800 drain guard suppressed the abort-finally stranded-message drain while the session was disconnected from the agent event stream. newSession/switchSession drop the agent queues on transition, so nothing is lost there. compact() preserves the queues and only reconnected in its finally — it never re-drained — so a steer/follow-up arriving during compaction (async IRC, an xd:// mount notice, an SDK steer) stayed stranded until the next explicit prompt. Re-drain in compact()'s finally after #reconnectToAgent (and after the compaction AbortController is cleared, so isCompacting is false and the scheduled agent.continue() actually runs). Added a regression test that queues a follow-up mid-compaction and asserts it resumes. Fixes #5800 --- packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/session/agent-session.ts | 13 +++- ...gent-session-auto-compaction-queue.test.ts | 59 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f94e3f821..a911c0e21 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed `/new` starting an unsolicited old-context provider turn when a hidden steer (e.g. an `xd://` mount notice) was queued: the session transition is now an atomic boundary, so a queued steer/follow-up can no longer auto-resume against the pre-`/new` context while the session is disconnected mid-transition ([#5800](https://github.com/can1357/oh-my-pi/issues/5800)). +- Fixed `/new` starting an unsolicited old-context provider turn when a hidden steer (e.g. an `xd://` mount notice) was queued: the session transition is now an atomic boundary, so a queued steer/follow-up can no longer auto-resume against the pre-`/new` context while the session is disconnected mid-transition. `/compact` still resumes a steer/follow-up that arrives while it runs, draining the queue once it reconnects ([#5800](https://github.com/can1357/oh-my-pi/issues/5800)). ## [17.0.2] - 2026-07-17 diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 757581fda..ab6c2af95 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -2248,8 +2248,10 @@ export class AgentSession { // the still-old context (the transition hasn't reached agent.reset() yet), start a // stale provider turn that races the reset, and — once reconnected — append its // output to the fresh session (issue #5800). A disconnected session never owns the - // queue: the transition does. Leave any queued steer/follow-up for the post-transition - // state (reset drops them; an explicit prompt flushes them). + // queue: the transition does. newSession/switchSession drop the queue (reset / + // clearAllQueues), so nothing survives; compaction preserves it and re-drains itself + // after #reconnectToAgent (see compact()'s finally); an explicit prompt flushes it + // in every case. if (this.#unsubscribeAgent === undefined) return; // A concern steered into a resumed streaming run after a user interrupt can // strand at the turn tail (steered past the loop's final boundary poll). While @@ -11023,6 +11025,13 @@ export class AgentSession { this.#compactionAbortController = undefined; } this.#reconnectToAgent(); + // Compaction disconnected before `await abort()`, so abort's finally drain + // (and any steer/follow-up that arrived mid-compaction — async IRC, an + // `xd://` mount notice, an SDK/RPC steer) was suppressed while disconnected + // (issue #5800). Unlike `/new`/switchSession, compaction preserves the agent + // queues, so nothing else resumes them: re-drain now that the listener is back + // and `isCompacting` is false, or the queued turn hangs until the next prompt. + this.#drainStrandedQueuedMessages(); } } diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 7f3425923..6cbd57c5e 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -268,6 +268,65 @@ describe("AgentSession auto-compaction queue resume", () => { expect(compactingDuringAbort).toBe(true); }); + it("resumes a message queued during manual compaction once it completes (#5800)", async () => { + // Regression for #5800 review: manual /compact disconnects the agent + // listener before `await abort()`, so the abort-finally stranded-message + // drain is suppressed while disconnected. Unlike /new (which resets the + // queue), compaction preserves the agent queues, so a steer/follow-up that + // arrives mid-compaction (async IRC, an xd:// mount notice, an SDK steer) + // would hang until the next explicit prompt unless compact() re-drains + // after reconnecting. + session.settings.set("compaction.keepRecentTokens", 1); + sessionManager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "previous answer" }], + api: "anthropic-messages", + provider: "anthropic", + model: "claude-sonnet-4-5", + stopReason: "stop", + usage: { + input: 1_000, + output: 100, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 1_100, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + timestamp: Date.now(), + }); + session.agent.replaceMessages(session.buildDisplaySessionContext().messages); + + const continueSpy = vi.spyOn(session.agent, "continue").mockImplementation(async () => { + session.agent.clearAllQueues(); + }); + + // Park compaction inside its awaited hook so we can queue a follow-up while + // the session is disconnected and abort has already run its finally. + const gate = Promise.withResolvers(); + (globalThis as typeof globalThis & { __ompManualCompactGate?: Promise }).__ompManualCompactGate = + gate.promise; + + const compactPromise = session.compact(); + while (!getRuntimeSignals().includes("before_compact:enter")) { + await Promise.resolve(); + } + + // A message arrives DURING compaction (post-abort, still disconnected). + session.agent.followUp({ + role: "user", + content: "please respond after compaction", + timestamp: Date.now(), + }); + expect(session.agent.hasQueuedMessages()).toBe(true); + + gate.resolve(); + await compactPromise; + await session.waitForIdle(); + + // compact()'s finally re-drained the stranded queue after reconnecting. + expect(continueSpy).toHaveBeenCalledTimes(1); + }); + it("cancels an in-flight auto-compaction when manual compact startup aborts", async () => { // Give the branch something to summarize so auto-compaction reaches the // awaited session_before_compact hook, where the test parks it.