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:
- Deleting a thread clears (or tombstones) its rows in
projection_thread_pull_requests.
- 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.
/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
- Open a pull request in T3 so a thread is created and linked to it.
- Delete that thread.
- 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.
Summary
A thread's pull-request link outlives the thread. Once the thread is deleted, the row in
projection_thread_pull_requestsremains and continues to be the resolution target for that PR, so the PR becomes permanently unopenable: every attempt resolves to the deleted thread, refreshes itslinked_at, and never creates a replacement.Recovered by deleting the orphaned row by hand. Nothing in the UI offered a way out.
Environment
serverVersion0.0.42, darwin/arm64/api/orchestration/shelland/api/orchestration/dispatchWhat 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:It is the only row for that PR, and no replacement thread was ever created —
projection_threadshas 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, andresolveCommitall exitingSuccess, finishing 82ms beforelinked_atwas 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:
projection_thread_pull_requests.linked_at./api/orchestration/shellexposes deletion, so a client can tell. The thread shape currently carriessettledAt,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
Expected: a new thread. Actual: resolution to the deleted thread,
linked_atrefreshed, no new thread, nothing openable.Workaround
Restart T3 afterwards. Identifiers for the affected private repository are redacted; thread UUIDs and timestamps are verbatim from the local
state.sqliteon the affected machine.