pingdotgg / pingdotgg/t3code

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

Open
#12,336 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accepted bug via-triage
Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing PR-thread preparation and deletion handling around projection_thread_pull_requests, then inspect the /api/orchestration/shell and /api/orchestration/dispatch entry points described in the report. Reproduce the sequence in the issue and inspect state.sqlite if needed. Done means reopening a PR after its thread is deleted no longer resolves to that deleted thread, and the shell behavior matches the chosen fix.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.