Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/resources/extensions/async-jobs/await-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ test("await_job returns immediately when no running jobs exist", async () => {
const result = await tool.execute("tc1", {}, noopSignal, () => {}, undefined as never);
const text = getTextFromResult(result);
assert.match(text, /No running background jobs/);

manager.shutdown();
});

test("await_job returns immediately when all watched jobs are already completed", async () => {
Expand All @@ -36,6 +38,8 @@ test("await_job returns immediately when all watched jobs are already completed"
const text = getTextFromResult(result);
assert.match(text, /fast-job/);
assert.match(text, /completed/);

manager.shutdown();
});

test("await_job returns on timeout when jobs are still running", async () => {
Expand Down Expand Up @@ -165,3 +169,23 @@ test("unawaited jobs still get follow-up delivery (#2248)", async () => {

manager.shutdown();
});

test("completed jobs use unref'd eviction timers so await-tool tests can exit cleanly", async () => {
const manager = new AsyncJobManager();

const jobId = manager.register("bash", "completed-job", async () => "done");
const job = manager.getJob(jobId)!;
await job.promise;

const timers = (manager as unknown as {
evictionTimers: Map<string, ReturnType<typeof setTimeout>>;
}).evictionTimers;
const timer = timers.get(jobId);

assert.ok(timer, "Expected eviction timer for completed job");
if (typeof timer === "object" && "hasRef" in timer) {
assert.equal(timer.hasRef(), false, "Eviction timer should not keep the process alive");
}

manager.shutdown();
});
1 change: 1 addition & 0 deletions src/resources/extensions/async-jobs/job-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export class AsyncJobManager {
this.evictionTimers.delete(id);
this.jobs.delete(id);
}, this.evictionMs);
if (typeof timer === "object" && "unref" in timer) timer.unref();

this.evictionTimers.set(id, timer);
}
Expand Down
9 changes: 9 additions & 0 deletions src/resources/extensions/claude-code-cli/stream-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ function extractMessageText(msg: { role: string; content: unknown }): string {
return "";
}

function isInformationalFollowUpMessage(msg: { role: string; content: unknown }): boolean {
const message = msg as { customType?: unknown; isFollowUp?: unknown };
return message.customType === "async_job_result" || message.isFollowUp === true;
}

/**
* Build a full conversational prompt from GSD's context messages.
*
Expand All @@ -101,6 +106,10 @@ export function buildPromptFromContext(context: Context): string {
}

for (const msg of context.messages) {
if (msg.role === "user" && isInformationalFollowUpMessage(msg)) {
continue;
}

const text = extractMessageText(msg);
if (!text) continue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,44 @@ describe("stream-adapter — full context prompt (#2859)", () => {
assert.ok(prompt.includes("Follow-up"), "prompt must include follow-up message");
});

test("buildPromptFromContext skips informational async job follow-ups", () => {
const context: Context = {
messages: [
{ role: "user", content: "Audit this page" } as Message,
{
role: "user",
content: "**Background job done: bg_123**\n\nlint passed",
customType: "async_job_result",
timestamp: Date.now(),
} as unknown as Message,
{ role: "assistant", content: [{ type: "text", text: "I found three issues." }] } as Message,
],
};

const prompt = buildPromptFromContext(context);
assert.ok(prompt.includes("Audit this page"), "real user prompt should remain");
assert.ok(prompt.includes("I found three issues."), "assistant context should remain");
assert.ok(!prompt.includes("Background job done"), "async follow-up should not become the next SDK prompt");
});

test("buildPromptFromContext skips generic follow-up user messages when flagged", () => {
const context: Context = {
messages: [
{ role: "user", content: "Continue with the original task" } as Message,
{
role: "user",
content: "Queued informational follow-up",
isFollowUp: true,
timestamp: Date.now(),
} as unknown as Message,
],
};

const prompt = buildPromptFromContext(context);
assert.ok(prompt.includes("Continue with the original task"));
assert.ok(!prompt.includes("Queued informational follow-up"));
});

test("buildPromptFromContext returns empty string for empty messages", () => {
const context: Context = { messages: [] };
const prompt = buildPromptFromContext(context);
Expand Down
Loading