Skip to content

fix(sessions): recover and resend prompts after "session not found"#3170

Open
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/recover-stale-session-prompt-send
Open

fix(sessions): recover and resend prompts after "session not found"#3170
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/recover-stale-session-prompt-send

Conversation

@richardsolomou

Copy link
Copy Markdown
Member

Problem

Replying to a local (or worktree) task that has been idle for a while sometimes fails with "Session not found" — and the typed prompt is lost, forcing the user to re-type it.

Local agent sessions are idle-killed by the workspace server after 15 minutes. The renderer normally gets notified and reconnects, but when it misses that event (laptop sleep, dropped subscription, host restart) the UI still shows the session as connected. The reply then hits the dead session, the server throws Session not found, and the error path only showed a toast — while the composer had already cleared the prompt optimistically on submit.

Changes

  • @posthog/core SessionService.sendLocalPrompt: on a fatal prompt failure (e.g. "session not found"), await the existing in-place auto-recovery and resend the prompt once on the recovered session, so replies to stale sessions land instead of erroring. An isRecoveryResend guard prevents retry loops; if recovery fails, the session is put into the error state with the Retry UI as before.
  • @posthog/ui useSessionCallbacks.handleSendPrompt: if a send still fails for any reason, restore the typed prompt into the composer (xmlToContent + setPendingContent, the same mechanism the cancel flow uses) and refocus it — skipped if the user has already started typing a new message.

How did you test this?

  • New unit tests in packages/core/src/sessions/sessionServicePromptRecovery.test.ts: recover-and-resend succeeds, recovery failure surfaces the error state, no retry loop when the resend also hits a fatal error, and recovery exceptions don't mask the original error.
  • Ran the full core sessions suite (29 files, 264 tests) and UI sessions/message-editor suites (32 files, 353 tests) — all passing.
  • pnpm --filter @posthog/core typecheck, pnpm --filter @posthog/ui typecheck, and biome lint packages/core (zero noRestrictedImports) — all clean.

Automatic notifications

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

🤖 Generated with Claude Code

Local agent sessions are idle-killed after 15 minutes. When the renderer
misses the idle-kill event (sleep, dropped subscription), replying to the
task hit the dead session, surfaced "Session not found", and the composer
had already cleared the typed prompt.

Now a fatal prompt failure awaits the existing in-place recovery and
resends the prompt once on the recovered session, so stale replies land
instead of erroring. If a send still fails for any reason, the typed
prompt is restored into the composer instead of being lost.

Generated-By: PostHog Code
Task-Id: 03ebf8b9-1bd2-49a9-ae91-d8f5e86ed683
@trunk-io

trunk-io Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@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 7edba26.

@richardsolomou richardsolomou requested a review from a team July 6, 2026 11:06
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(sessions): recover and resend prompt..." | Re-trigger Greptile

Comment on lines +89 to +137

expect(result).toEqual({ stopReason: "end_turn" });
expect(recoverSpy).toHaveBeenCalledTimes(1);
expect(promptMutate).toHaveBeenCalledTimes(2);
});

it("surfaces the error state when recovery fails", async () => {
const { service, store, promptMutate, recoverSpy } = createHarness();
promptMutate.mockRejectedValue(
new Error(`Session not found: ${TASK_RUN_ID}`),
);
recoverSpy.mockResolvedValue(false);

await expect(service.sendPrompt(TASK_ID, "hello again")).rejects.toThrow(
"Session not found",
);
expect(promptMutate).toHaveBeenCalledTimes(1);
expect(store.setSession).toHaveBeenCalledWith(
expect.objectContaining({ taskRunId: TASK_RUN_ID, status: "error" }),
);
});

it("does not loop when the resend hits another fatal error", async () => {
const { service, promptMutate, recoverSpy } = createHarness();
promptMutate.mockRejectedValue(
new Error(`Session not found: ${TASK_RUN_ID}`),
);
recoverSpy.mockResolvedValue(true);

await expect(service.sendPrompt(TASK_ID, "hello again")).rejects.toThrow(
"Session not found",
);
expect(recoverSpy).toHaveBeenCalledTimes(1);
expect(promptMutate).toHaveBeenCalledTimes(2);
});

it("swallows recovery exceptions and rethrows the original error", async () => {
const { service, promptMutate, recoverSpy } = createHarness();
promptMutate.mockRejectedValue(
new Error(`Session not found: ${TASK_RUN_ID}`),
);
recoverSpy.mockRejectedValue(new Error("auth restoring"));

await expect(service.sendPrompt(TASK_ID, "hello again")).rejects.toThrow(
"Session not found",
);
expect(promptMutate).toHaveBeenCalledTimes(1);
});
});

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 Prefer parameterised tests per project conventions

The four test cases share identical scaffolding — mock promptMutate, mock recoverSpy, call sendPrompt, assert. Per the team's style guide ("We always prefer parameterised tests"), these should be expressed as a single it.each table that varies the mock return values and the expected outcome, rather than four separate it(...) blocks.

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!

The three failure cases exercise the same fatal-error path with
different recovery behaviour, so express them as one it.each table
per review feedback. All failure cases now also assert the session
is surfaced in the error state.

Generated-By: PostHog Code
Task-Id: 03ebf8b9-1bd2-49a9-ae91-d8f5e86ed683
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