Skip to content

test(agent): cover background wake-up forwarding between turns#3200

Merged
charlesvien merged 1 commit into
chore/sync-claude-adapter-v0.54.1from
test/agent-wake-up-forwarding
Jul 6, 2026
Merged

test(agent): cover background wake-up forwarding between turns#3200
charlesvien merged 1 commit into
chore/sync-claude-adapter-v0.54.1from
test/agent-wake-up-forwarding

Conversation

@charlesvien

@charlesvien charlesvien commented Jul 6, 2026

Copy link
Copy Markdown
Member

Problem

Agent wake-ups (Bash run_in_background task notifications, Monitor and ScheduleWakeup timers) fire after the turn has settled. On main the SDK stream is only read inside prompt(), so wake-up output sits unread until the next user message and then appears all at once. The session looks hung for anyone waiting on a background task or a scheduled wake-up.

Closes #1919
Closes #2414

Changes

The fix itself lands in #3076 below this PR: its upstream #780 port replaces the per-prompt read loop with a session-lifetime consumer that forwards between-turn output live. This PR stacks regression tests on top so that behavior can't silently regress:

  • wake-up messages and task notifications forward live after the turn settles
  • wake-up streaming dedupes against the assembled assistant message
  • a task-notification result cannot settle the next queued user turn
  • a normal turn still runs after wake-up output

How did you test this?

  • pnpm --filter @posthog/agent test: 931 tests pass, including the 4 new ones
  • Live repro of both issues against the real SDK on this stack:
    • monitors not working #1919: agent ran sleep 10 && echo finished in the background, turn ended with end_turn, "the test succeeded" streamed in live 9s later with no follow-up prompt
    • handle ScheduleWakeUp tool call properly #2414: agent called ScheduleWakeup for 60s out, turn ended with end_turn, "the wakeup succeeded" streamed in live when the timer fired with no follow-up prompt

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 8f12c5e.

@charlesvien charlesvien changed the title add wake-up forwarding regression tests test(agent): cover background wake-up forwarding between turns Jul 6, 2026
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "add wake-up forwarding regression tests" | Re-trigger Greptile

Comment on lines +28 to +200

interface ClientMocks {
sessionUpdate: ReturnType<typeof vi.fn>;
extNotification: ReturnType<typeof vi.fn>;
}

function makeAgent(): { agent: Agent; client: ClientMocks } {
const client: ClientMocks = {
sessionUpdate: vi.fn().mockResolvedValue(undefined),
extNotification: vi.fn().mockResolvedValue(undefined),
};
const agent = new ClaudeAcpAgent(client as unknown as AgentSideConnection);
return { agent, client };
}

function installFakeSession(
agent: Agent,
sessionId: string,
): { query: MockQuery; input: Pushable<SDKUserMessage> } {
const query = createMockQuery();
const input = new Pushable<SDKUserMessage>();
const abortController = new AbortController();

const session = {
query,
queryOptions: { sessionId, cwd: "/tmp/repo", abortController },
buildInProcessMcpServers: () => ({}),
localToolsServerNames: [] as string[],
input,
cancelled: false,
interruptReason: undefined,
settingsManager: { dispose: vi.fn(), getRepoRoot: () => "/tmp/repo" },
permissionMode: "default" as const,
abortController,
accumulatedUsage: {
inputTokens: 0,
outputTokens: 0,
cachedReadTokens: 0,
cachedWriteTokens: 0,
},
sessionResources: new Set(),
configOptions: [],
turnQueue: [],
activeTurn: null,
pendingOrphanResults: 0,
queryGeneration: 0,
cwd: "/tmp/repo",
notificationHistory: [] as unknown[],
taskRunId: "run-1",
lastContextWindowSize: 200_000,
modelId: "claude-sonnet-4-6",
taskState: new Map(),
};

(agent as unknown as { session: typeof session }).session = session;
(agent as unknown as { sessionId: string }).sessionId = sessionId;

return { query, input };
}

function tick(): Promise<void> {
return new Promise((resolve) => setImmediate(resolve));
}

async function send(query: MockQuery, message: unknown): Promise<void> {
query._mockHelpers.sendMessage(message as SDKMessage);
await tick();
}

// Replays the prompt's own user message back through the query so the consumer
// activates its Turn before the terminal result arrives.
async function echoUserMessage(
query: MockQuery,
input: Pushable<SDKUserMessage>,
): Promise<void> {
const { value: pushed } = await input[Symbol.asyncIterator]().next();
await send(query, pushed);
}

function messageStart(sessionId: string, apiId: string) {
return {
type: "stream_event",
parent_tool_use_id: null,
session_id: sessionId,
uuid: `start-${apiId}`,
event: { type: "message_start", message: { id: apiId, usage: {} } },
};
}

function textDelta(sessionId: string, text: string) {
return {
type: "stream_event",
parent_tool_use_id: null,
session_id: sessionId,
uuid: `delta-${text}`,
event: {
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text },
},
};
}

function assistantMessage(sessionId: string, apiId: string, text: string) {
return {
type: "assistant",
parent_tool_use_id: null,
session_id: sessionId,
uuid: `assistant-${apiId}`,
message: {
id: apiId,
role: "assistant",
content: [{ type: "text", text }],
},
};
}

function resultSuccess(sessionId: string, uuid: string) {
return {
type: "result",
subtype: "success",
session_id: sessionId,
uuid,
result: "",
is_error: false,
usage: {},
modelUsage: {},
};
}

function taskNotificationResult(sessionId: string, uuid: string) {
return {
...resultSuccess(sessionId, uuid),
origin: { kind: "task-notification" },
};
}

function taskNotification(sessionId: string) {
return {
type: "system",
subtype: "task_notification",
session_id: sessionId,
uuid: "task-note-1",
task_id: "task-1",
status: "completed",
output_file: "/tmp/task-1.out",
summary: "Background command completed",
};
}

function messageChunkTexts(
calls: ClientMocks["sessionUpdate"]["mock"]["calls"],
): string[] {
return calls
.map(
([call]) =>
(
call as {
update?: { sessionUpdate?: string; content?: { text?: string } };
}
).update,
)
.filter((update) => update?.sessionUpdate === "agent_message_chunk")
.map((update) => update?.content?.text ?? "");
}

async function runTurn(
agent: Agent,
client: ClientMocks,
query: MockQuery,
input: Pushable<SDKUserMessage>,
sessionId: string,
text: string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Verbatim duplication of shared test helpers

ClientMocks, makeAgent, installFakeSession, tick, send, echoUserMessage, messageStart, textDelta, assistantMessage, and messageChunkTexts are copied character-for-character from claude-agent.streamed-text.test.ts. A third test file for this adapter will copy them again. Extracting these into a shared module (e.g., claude-agent.test-helpers.ts) would satisfy the OnceAndOnlyOnce rule and keep all adapter test files in sync automatically.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@charlesvien charlesvien merged commit 8f12c5e into chore/sync-claude-adapter-v0.54.1 Jul 6, 2026
16 of 17 checks passed
@charlesvien charlesvien deleted the test/agent-wake-up-forwarding branch July 6, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant