#12336·t3code

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

Author: chrisjfunkCreated Sep 17, 2026Updated Sep 18, 2026
Labelsbugacceptedvia-triage

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

sql
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.