fix(session): resumed queued messages after compaction reconnects
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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void>();
|
||||
(globalThis as typeof globalThis & { __ompManualCompactGate?: Promise<void> }).__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.
|
||||
|
||||
Reference in New Issue
Block a user