Skip to content

chore(code): backward-compat cloud queue drain for old agents#2023

Merged
VojtechBartos merged 1 commit intomainfrom
vojtab/backward-compat-cloud-queue-drain
May 5, 2026
Merged

chore(code): backward-compat cloud queue drain for old agents#2023
VojtechBartos merged 1 commit intomainfrom
vojtab/backward-compat-cloud-queue-drain

Conversation

@VojtechBartos
Copy link
Copy Markdown
Member

Problem

PR #1905 gates cloud message queue dispatch on _posthog/run_started and _posthog/turn_complete notifications. The sandbox base image hasn't been rebuilt since April 28 and runs agent v2.3.425, which doesn't emit _posthog/run_started. This means session.status never flips to "connected" and queued follow-up messages are stuck forever.

Solution

Treat _posthog/turn_complete as an implicit run_started — if the session is still disconnected when a turn completes, upgrade status to "connected" before draining the queue. No-op for new agents that already emitted run_started.

Test plan

  • New test: turn_complete without prior run_started upgrades status and drains queue
  • Existing tests pass

Created with PostHog Code

Cloud sandboxes running agent versions that predate the _posthog/run_started
notification never flip session.status to "connected", leaving queued
follow-up messages stuck. Upgrade status on the first turn_complete when
run_started hasn't been received so the queue drains normally.
@VojtechBartos VojtechBartos self-assigned this May 5, 2026
@VojtechBartos VojtechBartos requested a review from a team May 5, 2026 08:25
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/renderer/features/sessions/service/service.ts:1093-1097
The condition `session.status !== "connected"` is broader than the stated intent. The PR description says to upgrade status specifically when the session is "still disconnected", but this guard also fires for `"error"` and `"connecting"` states. A log-tailed `turn_complete` arriving while the session is in an `"error"` state would unexpectedly flip it back to `"connected"` and trigger a queue drain against a broken session. Narrowing to `=== "disconnected"` matches the prose description and avoids the unintended reset.

```suggestion
          if (session.status === "disconnected") {
            sessionStoreSetters.updateSession(taskRunId, {
              status: "connected",
            });
          }
```

### Issue 2 of 3
apps/code/src/renderer/features/sessions/service/service.test.ts:1055-1064
Misleading mock comment — the comment says the first `mockReturnValueOnce(sessionWithQueue)` is consumed "for the turn_complete handler", but the handler reads from `getSessions()` (not `getSessionByTaskId`). The `mockReturnValueOnce` is actually consumed by some earlier call inside `watchCloudTask` before `sendQueuedCloudMessages` runs. The comment should clarify what actually consumes the first return value, or the mock chain should be simplified to just `mockReturnValue(connectedSession)` if the first entry is a setup detail unrelated to the backward-compat path.

### Issue 3 of 3
apps/code/src/renderer/features/sessions/service/service.test.ts:1042-1125
Opportunity for a parameterised test — the new test is structurally very similar to the existing "flushes queued cloud messages on _posthog/turn_complete" test (line 921): both set up a `turn_complete` event with a queued message and assert the queue is drained. The only dimension that varies is the starting `status` (`"connected"` vs `"disconnected"`) and the expected side effect of the `updateSession` call. Per the team's "we always prefer parameterised tests" rule, these two cases could be collapsed into a single `it.each` table, keeping both scenarios and removing the duplicated fixture setup.

Reviews (1): Last reviewed commit: "chore(code): treat turn_complete as impl..." | Re-trigger Greptile

Comment on lines +1093 to +1097
if (session.status !== "connected") {
sessionStoreSetters.updateSession(taskRunId, {
status: "connected",
});
}
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.

P1 The condition session.status !== "connected" is broader than the stated intent. The PR description says to upgrade status specifically when the session is "still disconnected", but this guard also fires for "error" and "connecting" states. A log-tailed turn_complete arriving while the session is in an "error" state would unexpectedly flip it back to "connected" and trigger a queue drain against a broken session. Narrowing to === "disconnected" matches the prose description and avoids the unintended reset.

Suggested change
if (session.status !== "connected") {
sessionStoreSetters.updateSession(taskRunId, {
status: "connected",
});
}
if (session.status === "disconnected") {
sessionStoreSetters.updateSession(taskRunId, {
status: "connected",
});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/service/service.ts
Line: 1093-1097

Comment:
The condition `session.status !== "connected"` is broader than the stated intent. The PR description says to upgrade status specifically when the session is "still disconnected", but this guard also fires for `"error"` and `"connecting"` states. A log-tailed `turn_complete` arriving while the session is in an `"error"` state would unexpectedly flip it back to `"connected"` and trigger a queue drain against a broken session. Narrowing to `=== "disconnected"` matches the prose description and avoids the unintended reset.

```suggestion
          if (session.status === "disconnected") {
            sessionStoreSetters.updateSession(taskRunId, {
              status: "connected",
            });
          }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1055 to +1064
status: "disconnected",
isCloud: true,
cloudStatus: "in_progress",
events: [],
messageQueue: [queuedMessage],
});
// After the turn_complete handler flips status to "connected",
// sendQueuedCloudMessages reads the session again via
// getSessionByTaskId. We return the disconnected version first
// (for the turn_complete handler) then the connected version
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 Misleading mock comment — the comment says the first mockReturnValueOnce(sessionWithQueue) is consumed "for the turn_complete handler", but the handler reads from getSessions() (not getSessionByTaskId). The mockReturnValueOnce is actually consumed by some earlier call inside watchCloudTask before sendQueuedCloudMessages runs. The comment should clarify what actually consumes the first return value, or the mock chain should be simplified to just mockReturnValue(connectedSession) if the first entry is a setup detail unrelated to the backward-compat path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/service/service.test.ts
Line: 1055-1064

Comment:
Misleading mock comment — the comment says the first `mockReturnValueOnce(sessionWithQueue)` is consumed "for the turn_complete handler", but the handler reads from `getSessions()` (not `getSessionByTaskId`). The `mockReturnValueOnce` is actually consumed by some earlier call inside `watchCloudTask` before `sendQueuedCloudMessages` runs. The comment should clarify what actually consumes the first return value, or the mock chain should be simplified to just `mockReturnValue(connectedSession)` if the first entry is a setup detail unrelated to the backward-compat path.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1042 to 1125
it("upgrades status to connected on turn_complete when run_started was never received", async () => {
const service = getSessionService();
mockBuildAuthenticatedClient.mockReturnValue(mockAuthenticatedClient);
const queuedMessage = {
id: "q-1",
content: "follow up",
queuedAt: 1700000000,
};
// Session starts disconnected — simulates an old agent that never
// emitted _posthog/run_started.
const sessionWithQueue = createMockSession({
taskRunId: "run-123",
taskId: "task-123",
status: "disconnected",
isCloud: true,
cloudStatus: "in_progress",
events: [],
messageQueue: [queuedMessage],
});
// After the turn_complete handler flips status to "connected",
// sendQueuedCloudMessages reads the session again via
// getSessionByTaskId. We return the disconnected version first
// (for the turn_complete handler) then the connected version
// (for the queue dispatcher's canSendNow check).
const connectedSession = createMockSession({
...sessionWithQueue,
status: "connected",
});
mockSessionStoreSetters.getSessions.mockReturnValue({
"run-123": sessionWithQueue,
});
mockSessionStoreSetters.getSessionByTaskId
.mockReturnValueOnce(sessionWithQueue)
.mockReturnValue(connectedSession);
mockSessionStoreSetters.dequeueMessages.mockReturnValue([queuedMessage]);
mockTrpcLogs.readLocalLogs.query.mockResolvedValue("");
mockTrpcLogs.fetchS3Logs.query.mockResolvedValue("{}");
mockTrpcLogs.writeLocalLogs.mutate.mockResolvedValue(undefined);
mockTrpcCloudTask.sendCommand.mutate.mockResolvedValue({
success: true,
result: { stopReason: "end_turn" },
});

const turnCompleteEvent = {
type: "acp_message" as const,
ts: 1700000001,
message: {
jsonrpc: "2.0" as const,
method: "_posthog/turn_complete",
params: { sessionId: "acp-session", stopReason: "end_turn" },
},
};
mockConvertStoredEntriesToEvents.mockReturnValueOnce([turnCompleteEvent]);

service.watchCloudTask(
"task-123",
"run-123",
"https://api.anthropic.com",
123,
undefined,
"https://logs.example.com/run-123",
);

await vi.waitFor(() => {
expect(mockSessionStoreSetters.updateSession).toHaveBeenCalledWith(
"run-123",
{ status: "connected" },
);
});

await vi.waitFor(() => {
expect(mockTrpcCloudTask.sendCommand.mutate).toHaveBeenCalledWith(
expect.objectContaining({
taskId: "task-123",
method: "user_message",
params: expect.objectContaining({ content: "follow up" }),
}),
);
});
});

it("clears isPromptPending from structured turn completion logs on hydration", async () => {
const service = getSessionService();
const hydratedSession = createMockSession({
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 Opportunity for a parameterised test — the new test is structurally very similar to the existing "flushes queued cloud messages on _posthog/turn_complete" test (line 921): both set up a turn_complete event with a queued message and assert the queue is drained. The only dimension that varies is the starting status ("connected" vs "disconnected") and the expected side effect of the updateSession call. Per the team's "we always prefer parameterised tests" rule, these two cases could be collapsed into a single it.each table, keeping both scenarios and removing the duplicated fixture setup.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/service/service.test.ts
Line: 1042-1125

Comment:
Opportunity for a parameterised test — the new test is structurally very similar to the existing "flushes queued cloud messages on _posthog/turn_complete" test (line 921): both set up a `turn_complete` event with a queued message and assert the queue is drained. The only dimension that varies is the starting `status` (`"connected"` vs `"disconnected"`) and the expected side effect of the `updateSession` call. Per the team's "we always prefer parameterised tests" rule, these two cases could be collapsed into a single `it.each` table, keeping both scenarios and removing the duplicated fixture setup.

How can I resolve this? If you propose a fix, please make it concise.

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!

@VojtechBartos VojtechBartos enabled auto-merge (squash) May 5, 2026 08:32
@VojtechBartos VojtechBartos merged commit bec5aa0 into main May 5, 2026
15 checks passed
@VojtechBartos VojtechBartos deleted the vojtab/backward-compat-cloud-queue-drain branch May 5, 2026 08:34
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.

2 participants