Skip to content

Commit 3ce4396

Browse files
committed
Skip leftover ingest for director wake text
Leftover hops used operator ingest, so a parked @path was rewritten before echo matching could consume the raw wake.
1 parent 72d4278 commit 3ce4396

3 files changed

Lines changed: 126 additions & 2 deletions

File tree

src/tui/agent-ask-wake.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { createAppShell } from "./shell/index";
55
import { withTestRenderer } from "./harness";
66
import type { PendingAskWake } from "../subagent/fleet-report.js";
77
import { classifySubmission, createSubmitHandler } from "./runner/submit.js";
8-
import { routeQueuedDelivery } from "./queued-delivery.js";
8+
import {
9+
createDeliveryGeneration,
10+
createLeftoverSend,
11+
routeQueuedDelivery,
12+
} from "./queued-delivery.js";
13+
import { createSessionOperationQueue } from "./session-operation-queue.js";
14+
import { ingestOperatorPrompt } from "./prompt-attachments.js";
915
import {
1016
armFeedbackCapture,
1117
cancelFeedbackCapture,
@@ -482,4 +488,84 @@ describe("agent ask wake delivery", () => {
482488
{ width: 80, height: 24 },
483489
);
484490
});
491+
492+
test("idle leftover wake keeps an @path in the question raw and consumes the echo", async () => {
493+
await withTestRenderer(
494+
async (h) => {
495+
const shell = createAppShell(h.renderer, {
496+
terminal: { columns: 80, rows: 24 },
497+
wireKeys: false,
498+
run: "idle",
499+
});
500+
const sent: string[] = [];
501+
const attachments: number[] = [];
502+
const ingested: string[] = [];
503+
const queue = createSessionOperationQueue();
504+
const leftoverSend = createLeftoverSend({
505+
enqueue: queue.enqueue,
506+
ingest: async (text, pending) => {
507+
ingested.push(text);
508+
return ingestOperatorPrompt(
509+
text,
510+
"/repo",
511+
async () => {
512+
throw new Error("wake leftover must not load image paths");
513+
},
514+
pending,
515+
);
516+
},
517+
send: (text, pending) => {
518+
sent.push(text);
519+
attachments.push(pending.length);
520+
},
521+
captureGeneration: createDeliveryGeneration().capture,
522+
onFailure: (error) => {
523+
throw error;
524+
},
525+
});
526+
const send = (text: string) => {
527+
leftoverSend(text);
528+
};
529+
const bridge = attachSessionBridge(
530+
shell,
531+
createLiveSessionPort({
532+
send,
533+
deliver: routeQueuedDelivery({
534+
send,
535+
deliverSteer: () => {
536+
throw new Error("wake must not live-inject");
537+
},
538+
parentCycleLive: () => bridge.parentCycleLive,
539+
}),
540+
interrupt: () => {},
541+
}),
542+
);
543+
try {
544+
const ask = {
545+
...wake("a1", "q1"),
546+
question: "Should I edit @src/foo.ts?",
547+
};
548+
bridge.handle({ type: "agent-ask", asks: [ask] });
549+
await queue.awaitTail();
550+
expect(sent).toHaveLength(1);
551+
const wakeText = sent[0];
552+
if (wakeText === undefined) throw new Error("expected wake text");
553+
expect(wakeText).toContain("@src/foo.ts");
554+
expect(wakeText).not.toContain("(not found)");
555+
expect(attachments).toEqual([0]);
556+
expect(ingested).toEqual([]);
557+
expect(shell.streamLog.filter((row) => row.role === "user")).toHaveLength(1);
558+
bridge.handle({
559+
type: "message.received",
560+
data: { message: { content: wakeText } },
561+
});
562+
expect(shell.streamLog.filter((row) => row.role === "user")).toHaveLength(1);
563+
} finally {
564+
bridge.dispose();
565+
shell.dispose();
566+
}
567+
},
568+
{ width: 80, height: 24 },
569+
);
570+
});
485571
});

src/tui/queued-delivery.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,35 @@ describe("createLeftoverSend", () => {
240240
expect(recorded).toEqual(["follow-up"]);
241241
});
242242

243+
test("leftover send skips ingest for ask_director wake and still ingests operator prompts", async () => {
244+
const sent: string[] = [];
245+
const ingested: string[] = [];
246+
const { enqueue, awaitTail } = createSessionOperationQueue();
247+
const leftoverSend = createLeftoverSend({
248+
enqueue,
249+
ingest: async (text, pending) => {
250+
ingested.push(text);
251+
return { text: `${text} ingested`, attachments: pending };
252+
},
253+
send: (text) => {
254+
sent.push(text);
255+
},
256+
captureGeneration: () => () => true,
257+
onFailure: (err) => {
258+
throw err;
259+
},
260+
});
261+
262+
leftoverSend("ask_director wake — see @src/foo.ts");
263+
leftoverSend("please read @src/foo.ts");
264+
await awaitTail();
265+
expect(ingested).toEqual(["please read @src/foo.ts"]);
266+
expect(sent).toEqual([
267+
"ask_director wake — see @src/foo.ts",
268+
"please read @src/foo.ts ingested",
269+
]);
270+
});
271+
243272
test("generation bump drops leftover send but not a sibling Enter send", async () => {
244273
const leftoverSent: string[] = [];
245274
const enterSent: string[] = [];

src/tui/queued-delivery.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,18 @@ export function createLiveSteerDeliver(
121121
/**
122122
* Leftover / queue drain hop: capture generation at hop time, ingest, then
123123
* send only if /clear|/new has not bumped. Operator Enter must not use this.
124+
* `ask_director wake` leftover is passed through raw so worker @paths and
125+
* image mentions are not rewritten as operator attachments.
124126
*/
125127
export function createLeftoverSend(
126128
args: CreateLeftoverSendArgs,
127129
): (text: string, attachments?: readonly PendingImageAttachment[]) => void {
128-
return createGenerationGatedHop({ ...args, hop: args.send });
130+
return createGenerationGatedHop({
131+
...args,
132+
hop: args.send,
133+
ingest: async (text, pending) =>
134+
text.startsWith("ask_director wake")
135+
? { text, attachments: pending }
136+
: args.ingest(text, pending),
137+
});
129138
}

0 commit comments

Comments
 (0)