Skip to content

Deleted thread keeps its pull-request link, making the PR permanently unopenable #12336

Description

@chrisjfunk

Summary

A thread's pull-request link outlives the thread. Once the thread is deleted, the row in projection_thread_pull_requests remains and continues to be the resolution target for that PR, so the PR becomes permanently unopenable: every attempt resolves to the deleted thread, refreshes its linked_at, and never creates a replacement.

Recovered by deleting the orphaned row by hand. Nothing in the UI offered a way out.

Environment

  • T3 Code (Alpha) 0.0.42, serverVersion 0.0.42, darwin/arm64
  • Desktop-managed server
  • Client: a local app embedding T3 in an iframe and driving it through /api/orchestration/shell and /api/orchestration/dispatch

What happens

A thread linked to a PR was deleted at 15:20:06Z. For the next eight hours, every attempt to open that PR resolved to the deleted thread:

projection_thread_pull_requests
  thread_id   d261fce4-…          ← projection_threads.deleted_at = 2026-09-17T15:20:06.731Z
  repository  <org>/<repo>
  number      <N>
  source      manual
  linked_at   2026-09-17T23:49:44.494Z   ← refreshed by the most recent attempt, 8h after deletion

It is the only row for that PR, and no replacement thread was ever created — projection_threads has no new row for that project across 90 minutes of repeated attempts, while every other PR in the same project has a live thread.

The git side succeeds. Server traces for the 23:49:44Z attempt show GitVcsDriver.remoteExists, fetchPullRequestHeadCommit, and resolveCommit all exiting Success, finishing 82ms before linked_at was refreshed. So the PR head is fetched and resolved, and the work is then attributed to a thread that no longer exists.

Expected

Any one of these would have prevented it:

  1. Deleting a thread clears (or tombstones) its rows in projection_thread_pull_requests.
  2. PR-thread preparation skips a link whose thread is deleted and creates a new thread instead, rather than resolving to the deleted one and refreshing its linked_at.
  3. /api/orchestration/shell exposes deletion, so a client can tell. The thread shape currently carries settledAt, snoozedUntil, archivedAt-adjacent state but nothing for deletion, so a client that receives a deleted thread cannot distinguish it from a live one. Filtering deleted threads out of the shell would work equally well; exposing the field lets clients report it.

Item 3 is what made this undiagnosable from outside: our client added a liveness check — "is this thread still in the shell?" — and it passed, because the deleted thread was indistinguishable from a live one in the projection the client can see.

Reproduction

  1. Open a pull request in T3 so a thread is created and linked to it.
  2. Delete that thread.
  3. Open the same pull request again.

Expected: a new thread. Actual: resolution to the deleted thread, linked_at refreshed, no new thread, nothing openable.

Workaround

delete from projection_thread_pull_requests
 where repository='<org>/<repo>' and number=<N>;

Restart T3 afterwards. Identifiers for the affected private repository are redacted; thread UUIDs and timestamps are verbatim from the local state.sqlite on the affected machine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions