fix(vibe): deferred IRC wakes during mode exit
This commit is contained in:
@@ -776,7 +776,14 @@ export class AgentSession {
|
||||
* the agent responds to the peer. Skip only when a queued steer/follow-up will itself drive a
|
||||
* resume turn whose aside poll already consumes these (no double-wake). */
|
||||
#resumeStrandedIrcAsides(): void {
|
||||
if (this.#isDisposed || this.isStreaming || !this.#irc.hasPending()) return;
|
||||
if (
|
||||
this.#modeExitDrainSuppressionDepth > 0 ||
|
||||
this.#isDisposed ||
|
||||
this.isStreaming ||
|
||||
!this.#irc.hasPending()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages()) return;
|
||||
const records = this.#irc.drainPending();
|
||||
if (this.#planModeState?.enabled) {
|
||||
@@ -805,6 +812,10 @@ export class AgentSession {
|
||||
* because #canAutoContinueForFollowUp suppresses follow-up auto-resume while a user interrupt is
|
||||
* in effect, even though the wake left a provider-valid tail. */
|
||||
#wakeForIrc(records: CustomMessage[]): void {
|
||||
if (this.#modeExitDrainSuppressionDepth > 0) {
|
||||
this.#irc.deferWake(records);
|
||||
return;
|
||||
}
|
||||
// Park only a *blocked* follow-up (one a user interrupt is intentionally holding); an
|
||||
// already-resumable follow-up can ride the wake turn normally without reordering.
|
||||
const parkedFollowUps =
|
||||
@@ -5969,6 +5980,7 @@ export class AgentSession {
|
||||
this.#modeExitDrainSuppressionDepth--;
|
||||
if (this.#modeExitDrainSuppressionDepth === 0) {
|
||||
this.#scheduleIdleQueueDrain();
|
||||
this.#resumeStrandedIrcAsides();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ export class IrcBridge {
|
||||
return records;
|
||||
}
|
||||
|
||||
/** Queues records whose idle wake must wait for a session transition to finish. */
|
||||
deferWake(records: CustomMessage[]): void {
|
||||
this.#asides.push(...records);
|
||||
}
|
||||
|
||||
/** Surfaces and consumes queued incoming records before automatic injection. */
|
||||
drainInboxMessages(agentId: string, opts?: { from?: string; limit?: number }): IrcMessage[] {
|
||||
const messages: IrcMessage[] = [];
|
||||
|
||||
@@ -250,6 +250,64 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
expect(session.getToolByName("vibe_spawn")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("holds IRC wakes during Vibe teardown until the tools are removed", async () => {
|
||||
const toolNamesPerCall: string[][] = [];
|
||||
const firstStarted = Promise.withResolvers<void>();
|
||||
streamFn = (_model, context, options) => {
|
||||
toolNamesPerCall.push((context.tools ?? []).map(tool => tool.name));
|
||||
const isFirst = toolNamesPerCall.length === 1;
|
||||
const stream = new AssistantMessageEventStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({ type: "start", partial: createAssistantMessage("") });
|
||||
if (isFirst) {
|
||||
options?.signal?.addEventListener(
|
||||
"abort",
|
||||
() => stream.push({ type: "error", reason: "aborted", error: createAssistantMessage("Aborted") }),
|
||||
{ once: true },
|
||||
);
|
||||
firstStarted.resolve();
|
||||
} else {
|
||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("Resumed") });
|
||||
}
|
||||
});
|
||||
return stream;
|
||||
};
|
||||
|
||||
await mode.handleVibeModeCommand();
|
||||
const prompt = session.prompt("Delegate this");
|
||||
await firstStarted.promise;
|
||||
await session.deliverIrcMessage({ id: "m1", from: "peer", to: "me", body: "first", ts: Date.now() });
|
||||
|
||||
const abortSettled = Promise.withResolvers<void>();
|
||||
const releaseTeardown = Promise.withResolvers<void>();
|
||||
const abort = session.abort.bind(session);
|
||||
vi.spyOn(session, "abort").mockImplementation(async options => {
|
||||
await abort(options);
|
||||
abortSettled.resolve();
|
||||
await releaseTeardown.promise;
|
||||
});
|
||||
const exit = mode.handleVibeModeCommand();
|
||||
await abortSettled.promise;
|
||||
await session.deliverIrcMessage({ id: "m2", from: "peer", to: "me", body: "second", ts: Date.now() });
|
||||
for (let index = 0; index < 5; index++) await Promise.resolve();
|
||||
expect(toolNamesPerCall).toHaveLength(1);
|
||||
releaseTeardown.resolve();
|
||||
|
||||
await exit;
|
||||
await prompt;
|
||||
await session.waitForIdle();
|
||||
|
||||
expect(toolNamesPerCall).toHaveLength(2);
|
||||
for (const name of VIBE_TOOL_NAMES) {
|
||||
expect(toolNamesPerCall[1]).not.toContain(name);
|
||||
}
|
||||
expect(
|
||||
session.agent.state.messages.filter(
|
||||
message => message.role === "custom" && message.customType === "irc:incoming",
|
||||
),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps a same-named non-built-in Todo tool unavailable in Vibe mode", async () => {
|
||||
const model = session.model;
|
||||
if (!model) throw new Error("Expected active model");
|
||||
|
||||
Reference in New Issue
Block a user