Skip to content

fix(tasks): survive workspace-server respawns during task creation#3171

Open
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/fix-workspace-server-respawn-stale-client
Open

fix(tasks): survive workspace-server respawns during task creation#3171
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/fix-workspace-server-respawn-stale-client

Conversation

@richardsolomou

Copy link
Copy Markdown
Member

Problem

Starting a worktree task would sometimes fail with a "Failed to detect repository: fetch failed" toast, even though repo detection is only optional metadata.

Root cause: the workspace-server child respawns on a new random port and secret after a crash (and serve.ts has no uncaughtException/unhandledRejection handlers, so any stray rejection kills it). The renderer rebuilds its client on reconnect, but the Electron main process built its tRPC client once at boot and bound it as a DI constant. After any respawn, every main-process call — repo detection during task creation, file watching, connectivity — kept hitting the dead port with fetch failed until the app was relaunched.

Changes

  • packages/core — repo detection in TaskCreationSaga is now best-effort: it only fills the org/repo metadata tag on the task, so a transport failure logs a warning and creates the task untagged instead of aborting the saga and toasting.
  • packages/workspace-client — new createReconnectingWorkspaceClient(getConnection): a proxy that re-reads the current connection at the start of every call chain and rebuilds the underlying client when the port/secret changes. While the server is down it falls back to the last known client so calls fail fast instead of hanging.
  • apps/code main — all main-process workspace-client bindings now use the reconnecting client. The two long-lived SSE subscriptions (FileWatcherBridge, ConnectivityPortAdapter) resubscribe when WorkspaceServerService reports ready again, since the old streams retry the dead port forever.

How did you test this?

  • New unit test: TaskCreationSaga creates the task without a repository when detectRepo rejects with fetch failed (21 saga tests pass).
  • New @posthog/workspace-client test suite (vitest added to the package) covering the reconnecting client: build-once caching, rebuild on connection change, fallback while the server is down, error before first connection.
  • Empirically reproduced the exact failure: a workspace client pointed at a dead localhost port throws TRPCClientError: fetch failed, matching the toast verbatim.
  • All 273 apps/code unit tests pass; @posthog/core, @posthog/workspace-client, and apps/code typecheck clean; Biome clean; check-host-boundaries.mjs reports no new violations.

Automatic notifications

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

The workspace-server child respawns on a new random port and secret after a crash, but the Electron main process built its tRPC client once at boot and bound it as a DI constant. After any respawn, every main-process call kept hitting the dead port with "fetch failed" for the rest of the session — surfacing as a "Failed to detect repository: fetch failed" toast that aborted worktree task creation.

Three layers of fix:
- Repo detection in TaskCreationSaga is now best-effort: it only fills the org/repo metadata tag, so a transport failure degrades to an untagged task instead of aborting the saga.
- New createReconnectingWorkspaceClient re-reads the current connection on every call chain and rebuilds the underlying client when the port/secret changes; the main process now uses it for all workspace-client bindings.
- The two long-lived main-process SSE subscriptions (file watcher bridge, auth connectivity adapter) resubscribe when the server reports ready again, instead of retrying the dead port forever.

Generated-By: PostHog Code
Task-Id: d333d9be-05ed-410e-803f-8f0b304d5753
@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 5201c28.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(tasks): survive workspace-server res..." | Re-trigger Greptile

Comment on lines +55 to +57
return new Proxy({} as WorkspaceClient, {
get: (_target, prop) => Reflect.get(resolve() as object, prop),
});

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 Proxy only traps get

The reconnecting proxy intercepts only get, so any code that uses in, Object.keys(), Object.getOwnPropertyDescriptor(), or getPrototypeOf() against the wrapped client will see the empty {} target instead of the real tRPC client. This isn't a problem today — tRPC clients don't rely on those traps — but it makes the wrapper silently wrong for callers that might probe the object. Adding has and getPrototypeOf traps would make it a complete transparent proxy.

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!

Comment on lines +170 to +172
workspaceServer.on(WorkspaceServerEvent.StatusChanged, ({ status }) => {
if (status === WorkspaceServerStatus.Ready) this.subscribe();
});

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 StatusChanged listener is never removed

workspaceServer.on(...) is called in the constructor but there is no matching off() or disposal path. The handler closes over this, so WorkspaceServerService holds a permanent reference back to ConnectivityPortAdapter. For singleton DI services in a single-session Electron app this doesn't cause a leak, but there is no way to stop the resubscription logic if the adapter is ever torn down — future StatusChanged.Ready events would still invoke this.subscribe() on the stale instance. Storing the unsubscribe handle and exposing a dispose() method (or at minimum a Symbol.dispose implementation) would make this symmetric with the sub tracking already done for the tRPC subscription.

@richardsolomou richardsolomou requested a review from a team July 6, 2026 11:23
Two Greptile P2s: add has/getPrototypeOf traps so the reconnecting proxy stays transparent to callers that probe the object, and give ConnectivityPortAdapter a dispose() that detaches its StatusChanged listener and SSE subscription so a torn-down instance can't keep resubscribing.

Generated-By: PostHog Code
Task-Id: d333d9be-05ed-410e-803f-8f0b304d5753
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