# Proof audit, pass 1 — deletion fallback fixture matrix

PR [#29643](https://git.cafe/pingdotgg/t3code/pull/29643), mirror [#29643](https://github.com/pingdotgg/t3code/pull/29643). Branch `fix/issue-29609`. Reviewed head [ace518f2a7](https://github.com/pingdotgg/t3code/commit/ace518f2a78ebd640e1d5b06a652bd9d5c42b047), against merge-base [30084f0018](https://github.com/pingdotgg/t3code/commit/30084f0018c5751a07291222e8d04e16d513700a). Worktree `/home/ubuntu/wt/pr-29643`.

## Findings and evidence limits

1. **Proof is missing.** This is a pass-1 fixture derivation, not visual verification. No captures were supplied or created. The PR body reports three manual cases, but those claims are not recordings. Required recordings are R01–R07 below. R01–R03 need labelled Before/After runs on the supplied merge-base and head; R04–R07 need head recordings. A still of the final thread cannot show which thread was deleted, whether a parked thread briefly opened, or whether navigation happened at all.
2. **The changed production files render no JSX.** The helper adds two eligibility predicates and a required clock input; the hook supplies the clock. The visible difference is the selected route and selected sidebar row after deletion. The existing new-thread draft is also a distinct outcome. No new typography, theme, width, loading indicator, card, or error rendering is introduced.
3. **Do not equate a non-null `snoozedUntil` with being parked.** The helper calls `effectiveSnoozed`: expired and early-woken threads remain eligible. R04 and R05 protect that distinction. Both settings sort orders need exercising; R01 and R02 use deliberately conflicting creation/send order and two eligible survivors.
4. **Snapshot caveat, code-derived and not reproduced here.** `deleteThread` reads candidate shells at lines 455–458, before asynchronous worktree lookup/confirmation and provider stop, and filters that array at lines 552–558. Another client can change a survivor from active to settled during the worktree dialog; the old active shell can still be chosen, and the later `readThreadShell` checks existence only. The reverse move can miss an eligible survivor. R08a/R08b document this limitation and are recommended recordings if the lead intends to claim correctness across concurrent clients. They are separate from the minimum proof of the static-state issue fix; do not represent them as passed.
5. **Scope is parked shelves, not every sidebar visibility rule.** Side-chat, delegated-child, tag-hidden, pinned, Working, and scoped-project behavior is not newly filtered here. The existing bot discussion explicitly withdraws the side-chat/tag-hidden finding as pre-existing. Do not silently enlarge this PR into a general visibility rewrite, and do not claim that this helper now exactly mirrors all sidebar navigation rules.

## Source and caller inventory

All source links below are pinned to the reviewed head.

- [Changed helper](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/Sidebar.logic.ts#L1213): find deleted thread or return `null`; retain same-project candidates; exclude the deleted ID and IDs in `deletedThreadIds`; exclude explicit `settledOverride === "settled"`; exclude `effectiveSnoozed(thread, { now })`; sort and select first, else `null`. Its **only production caller** is `useThreadActions.deleteThread`; the other direct calls are in [the existing test file](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/Sidebar.logic.test.ts#L601). The permission test mocks the helper to `null`, so it does not prove fallback routing.
- [Changed hook](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/hooks/useThreadActions.ts#L433): permission failure; unresolved target uses direct mutation without fallback; resolved target snapshots one environment; optional prior-success IDs; optional shared-worktree occupancy lookup; worktree session-grant lookup and confirmation; optional provider stop; route-match gate by **both** environment and thread; mutation failure returns before navigation; successful deletion clears client state; matching open-thread route navigates to a still-existing fallback or `/`; optional worktree deletion/status failure has a separate toast. Only clock insertion and the helper predicates change in this PR.
- [Chat header caller](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/hooks/useThreadActionMenu.ts#L323): `ChatHeader` is the only production `useThreadActionMenu` caller. Header title/chevron and header right-click open the same menu. Draft headers have no server-thread Delete path. Confirm setting on/off, cancel, permission denial, and error toast are existing branches. Desktop title clicks have an existing delay/double-click rename branch; use the chevron for an unambiguous capture.
- [Sidebar callers](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/Sidebar.tsx#L2482): single-row context Delete at line 2845; multi-selection context Delete at line 2482. Single deletes call the same hook. Bulk deletes run sequentially through `deleteSelectedThreadEntries`; its set contains **earlier successful deletions**, not every future selected item. Do not assert that a batch can never briefly visit an as-yet-undeleted selected thread. R06 makes the open thread last in the recorded deletion order.
- [Settings caller](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/settings/ArchivedThreadsPanel.tsx#L156): `/settings/archived` calls `confirmAndDeleteThread`, which delegates to `deleteThread`. The settings route is not the deleted thread route, so there is no fallback navigation. Archived shell may be absent from the live shell index. Archive loading/empty/error and multi-environment group layouts are unchanged and do not reach the changed navigation branch.
- Other hook consumers: [chat lifecycle](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/chat/useThreadLifecycle.tsx#L177) uses settle/pin/unpin, not deletion. No thread-delete command-palette or keybinding path was found in the web command palette/keybinding definitions. Side-chat cleanup and failed-launch cleanup dispatch their own mutations and bypass this hook.
- [Snooze predicate](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/packages/client-runtime/src/state/threadSettled.ts#L79): absent/null/invalid deadline => eligible; deadline `<= now` => eligible; future deadline plus no hand raise => parked. Hand raise is approval, user input, pending runtime request, subagent request, pending guest message, or a fresh runtime failure. Failure selects `runtime ?? session`; runtime must be error/failed; failed-run completion is preferred over runtime timestamp; a stale failure does not wake; an absent snooze-start timestamp permits the legacy failure case. Running/completed background work alone does not wake. These inherited predicate branches are collapsed into R01, R04, R05, and C04 rather than requiring a visual permutation of every provider status.
- [Sort](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/packages/client-runtime/src/state/threadSort.ts#L105): `created_at` uses creation then update fallback; default `updated_at` uses latest valid user-message timestamp, then valid user messages, then update/create. Zero/one eligible candidates bypass comparison; multiple candidates sort descending timestamp, then descending ID on a tie. R01/R02 exercise both orders and multiple candidates; R03 exercises zero. Timestamp corruption and ties are unchanged, not separate visual layouts.
- [Sidebar partition](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/components/Sidebar.tsx#L638): Snoozed, Settled, Pinned, Active/Working; loaded-server-config gate; optimistic drag placement; collapsed shelf retains route-thread row; Settled tail pagination. None of their render branches changes. Expand parked shelves in R01–R03 so the viewer can identify the candidates and observe their counts after deletion; collapsed/expanded and paginated tails do not need independent proof clips for this predicate change.
- [Landing route](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/apps/web/src/routes/_chat.index.tsx#L37): `/` starts a draft in the most recent unarchived project, then routes to `/draft/$draftId`. It is **not** necessarily an enduring empty homepage. With no project it shows the existing no-project hero; bootstrapping/hosted-empty/draft-start error are unchanged. R03 must show the completed draft landing, not merely the transient slash route.

## Capture contract and common data

- Minimum format: **recording**, never still-only. Show the initial open-thread title, expanded shelf identities/counts, the actual Delete action and confirmation (or deliberately disabled confirmation), then the final stable route/title/sidebar selection. Keep enough frames to see unexpected intermediate selections. Include a redacted RPC/event transcript of the seed and relevant shell fields alongside time-sensitive or scripted-provider clips; this does not replace the recording.
- Required viewport/theme tuples: R01–R06 on **web, 1440 × 900 CSS px, dark**; R07 on **desktop, 1440 × 900 content CSS px, dark**, or the closest available content size documented exactly. One theme is sufficient: no colour/CSS branch changes. No minimum narrow-width or light-mode duplicate. Native mobile is excluded because it does not call this web helper/hook; web on a narrow screen still uses the same hook, with an unchanged drawer. Remote/relay/tunnel share this client-local selection code; no transport changes require a matrix multiplication.
- Required **Before/After pairs**: R01, R02, R03, identical synthetic data/clock relationship at merge-base and head. R04–R07 are head controls. Seven rows produce ten required recording runs. They may be chapters in fewer video files if each reset, row ID, revision, viewport, and result is unmistakable.
- Use an isolated dev home and synthetic workspace, never run against live `~/.t3/userdata`. Each capture/driver run goes to a new timestamped directory below `/tmp/lead-29643-r1/`. No server or browser was started for this audit. The capturer follows the authorized app-test setup; this document is not a startup instruction to use a second unchecked server.
- Common project P in environment E; all fixture threads are top-level, unarchived, undeleted, no worktree unless stated, no hidden tags, and settled override `null` unless stated. Disable auto-settle for the fixture threads so elapsed audit time does not change their class. No pending request/guest/subagent flags unless stated. Use distinct short titles and known IDs, and identify each in the seed transcript.
- Common ranking fixture: D = “Delete me”, Z = “Parked candidate”, A = “Older created, recent send”, B = “Newer created, older send”. Creation order A < B < Z < D; latest user-send order B < A < Z < D. Every time precedes the deletion clock. Z outranks A and B under either sort before applying the new filters. With Z excluded, `updated_at` picks A and `created_at` picks B. Record actual shell timestamps; metadata writes are not reliable proxies for latest user-send time.
- Real app seed mechanism: create a synthetic project with `POST /api/projects/mutate` (`project.create`) or the UI; use authenticated `orchestration.launchThread` without an initial message to create a real empty thread, or with an initial message and a scripted provider to establish send ordering; use `orchestration.dispatchCommand` with unique command IDs and `thread.snooze` / `thread.settle` / `thread.unsettle` / `thread.pin` / `thread.archive` as needed. Inspect `orchestration.getThreadProjection` and `/api/orchestration/shell`. The real protocol/auth/RPC pattern is in [the existing driver](https://github.com/pingdotgg/t3code/blob/ace518f2a78ebd640e1d5b06a652bd9d5c42b047/scripts/mobile-behavior/environment.ts#L190); do not copy its startup/removal helper wholesale, since this task has stricter scratch/deletion rules.
- Client settings: `/settings/threads` exposes “Confirm thread deletion”; `sidebarThreadSortOrder` remains a persisted client setting even though no corresponding current UI control was found. In a Vite dev driver, call exported `ensureClientSettingsHydrated()` then `updateClientSettings({sidebarThreadSortOrder: "created_at"})` from `/src/hooks/useSettings.ts`. Set back to `updated_at` for the other rows. Do not send this client setting as a server settings patch. Theme is set through app Appearance settings. Start each real-thread action at `/$environmentId/$threadId`; routes do not include the literal `_chat` prefix.

## Fixture matrix

“Direct” means removing the added predicates changes the destination for this fixture. “Boundary” means the new predicate must preserve eligibility. “Control” means an unchanged branch/caller. Every row with a proposed capture is labelled recording because it involves interaction or time. **No row has captured evidence yet.**

| ID / named fixture | Required data and distinct rendered state | Action and expected result at head | Diff effect | How to seed / route | Minimum capture tuple / format | Pass-1 status |
|---|---|---|---|---|---|---|
| R01 — Future snooze, two live survivors | Common D/Z/A/B ranking; Z future snooze e.g. now +4h; no hand raise; Active has D/A/B, Snoozed has Z. `updated_at`. | Header chevron → Delete → Confirm D. Open A, never Z; Z stays Snoozed and neither A/B is removed. Before opens Z. | Direct: snooze exclusion; many eligible candidates and default sort. | Real app: launch ranking fixture; `thread.snooze` Z; set client sort; expand Snoozed; open E/D. Include one running-but-snoozed Z if using a held provider; otherwise stopped Z suffices. | Web 1440×900 × dark; **recording Before + After**. | **MISSING — required** |
| R02 — Settled, conflicting creation/send order | Common ranking; Z `settledOverride="settled"`, settledAt valid; no snooze. Two eligible survivors. `created_at`. | Sidebar D right-click → Delete → Confirm. Open B; Z remains Settled. Before opens Z. | Direct: settled exclusion; alternate sort; sidebar single caller. | Real app: `thread.settle` Z; set client sort through dev settings export; expand Settled; E/D. Distinct send order must be recorded, not inferred from visible row order. | Web 1440×900 × dark; **recording Before + After**. | **MISSING — required** |
| R03 — All same-project survivors parked | D plus S settled and Z future-snoozed in P; no A/B; add a newer eligible Q in a different project. Both parked shelves nonempty, then Active for P empty. | Header Delete D. No S/Z/Q opens. Navigate through `/` to a new-thread draft. Parked counts/IDs remain. Draft may belong to most-recent project Q; this is existing landing behavior. Before opens S or Z according to seed order. | Direct: filtering turns a formerly nonempty candidate set into zero; null fallback. | Real app: launch D/S/Z in P and Q in second project; settle S, snooze Z; open E/D and show P scope. Document resulting draft project rather than requiring P. | Web 1440×900 × dark; **recording Before + After**. | **MISSING — required** |
| R04 — Timer wake is eligible again | D/Z/A; Z still has a valid deadline that passes during recording; Z highest rank, not settled, no hand raise. | Show Z in Snoozed, await its observable wake, then delete D. Open Z. Do not incorrectly keep excluding a raw snooze field after expiry. | Boundary: supplied current `now`; future → expired branch. | Real app: `thread.snooze` with short future deadline, await app shelf/label change, then header Delete D at E/D. Save shell deadline and deletion time. Exact equality is a harness-only subcase in C04. | Web 1440×900 × dark; **recording After** including time transition. | **MISSING — required** |
| R05 — Early wake with future deadline | D/Z/A; Z `snoozedUntil > now`, settled null, and an outstanding request wakes it; Z highest rank. Distinct “needs you” state. | While Z needs input, delete D through header. Open Z, not A; the added filter must use effective snooze, not simply field presence. | Boundary: hand-raise exception. | Real app via a scratch scripted provider: copy the existing fake Fx fixture under the run directory, gate its `elicitation/create` on a release file, start Z, snooze once its run is active, release to request user input, await the persisted runtime-request, open E/D and delete. Existing public RPC cannot directly set the server-only pending flags. If server wake cleanup clears the deadline before deletion, that run proves ordinary awake fallback only; a **labelled client harness recording** with the raw future deadline + pending input shell is needed for this exact row. No such harness/driver is claimed to exist yet. | Web 1440×900 × dark; **recording After**; label **REAL APP / SCRIPTED PROVIDER** or **HARNESS** honestly. | **MISSING — required; deterministic fixture needs a driver/harness** |
| R06 — Bulk deletion leaves a parked and pinned survivor | Same P: selected C and open D; unselected future-snoozed Z, settled S, and eligible pinned A; A older in sort. No worktrees. | Bulk context Delete with successful C deletion before D. After deleting D, open A; S/Z remain on shelves, C/D removed. Show selection count and actual command order. | Direct: parked exclusion composed with prior-success ID set; bulk caller and eligible Pinned branch. | Real app: launch fixture, `thread.pin` A; settle S, snooze Z. Select C then D so recorded dispatch order is C,D; current route E/D. If entry order differs, reset or report it, do not pretend all selected IDs are passed in advance. | Web 1440×900 × dark; **recording After**. Disable delete confirmation for this row to cover the no-confirm route; show the setting/reset in transcript. | **MISSING — required** |
| R07 — Native desktop menu reaches same fallback | Reuse R01 data on head in Electron; native context menu, web content, title chevron. | Chevron → native Delete → app Confirm; opens A and keeps Z Snoozed. | Direct, shared web logic; desktop entry plumbing differs. | Real desktop app connected to isolated fixture environment; same seed RPC; E/D. Use content-size capture; include native menu with screen-level recording if the app recorder omits it. | Desktop ≈1440×900 content × dark; **recording After**. | **MISSING — required desktop smoke** |
| R08a — Candidate parked during worktree confirmation | D has sole non-scratch worktree, cleanup not automatic, operation and source-control scopes granted; only eligible survivor A initially active. | Begin Delete D; while “Delete the worktree too?” is open, another client/RPC settles A; decline worktree removal and finish. Desired current-state result is draft; code predicts opening now-settled A from stale snapshot. | Conditional interaction with added filter; snapshot risk, not a captured defect. | Real app: create isolated worktree D; second authenticated client sends `thread.settle` A and await first client's shelf change before dismissing worktree dialog. Never remove shared/live worktrees. | Same web tuple; **recording**, recommended concurrency follow-up, outside static-state minimum. | **UNVERIFIED RISK — not captured** |
| R08b — Candidate wakes during worktree confirmation | R08a, but A initially settled and then explicitly unsettled. | Another client `thread.unsettle` A while worktree dialog open; decline removal. Desired result A; code predicts draft from old settled shell. | Conditional: added settled filter can discard an already-visible candidate. | Real app: same isolated worktree setup; observe A become Active on the first client before continuing D deletion. | Same web tuple; **recording**, recommended concurrency follow-up, outside static-state minimum. | **UNVERIFIED RISK — not captured** |
| C01 — Delete a non-open sidebar thread | Open A, delete different D in same E/P; include parked Z if desired. Chat remains A. | Sidebar Delete D, route and selection stay A. | Control: `shouldNavigateToFallback` false. | Real app E/A; context menu D. | **recording if checked**; no independent minimum clip required. | **CODE-TRACED — unchanged branch** |
| C02 — Archived settings delete | At `/settings/archived`, archived D listed; loading/empty/error states are unchanged. | Delete archived D; list refreshes, settings route remains. No fallback expected. | Control: settings wrapper, often unresolved live target. | Real app: `thread.archive` D; visit `/settings/archived`; menu Delete. | **recording if checked**; no independent minimum clip required. | **CODE-TRACED — unchanged caller** |
| C03 — Cancel / denied / failed delete | Confirmation cancel or a mutation failure; permission denial disables or refuses mutation. | No successful deletion and no fallback selection; existing error toast on real failures. | Control: added filter cannot change a successful destination here. | Cancel is real app. Deterministic revoked-scope/mutation/navigation failure is a **labelled harness** unless explicitly reproduced through an isolated second client. No raw mutation-field override through public RPC. | **recording if checked**; no independent minimum clip required. | **CODE-TRACED — unchanged error exits** |
| C04 — Predicate data equivalence classes | No/undefined/null or invalid snooze; deadline exactly `now`; future snooze plus approval/runtime/subagent/guest/fresh failure; future snooze plus stale failure/running/completed background run; settled plus expired/woken snooze. | Null/invalid/expired/raised-hand snooze eligible unless explicitly settled; future unraised snooze excluded; settled always excluded. | Boundary subvariants of R01/R04/R05/R02; no new layout introduced by these predicate internals. | Null/expired/running/explicit settlement reachable in real app. Invalid ISO is schema-rejected; exact fixed-clock equality and arbitrary combined flags require a **labelled predicate/client harness**. Existing predicate tests describe these branches, but were not executed in this audit. | Harness **recording** if visually demonstrated; deterministic assertion transcript preferable for non-rendering data edge cases. No extra minimum visual clips. | **CODE-TRACED — representative captures R01/R02/R04/R05 missing** |
| C05 — Empty/missing/one-candidate helper shapes | Deleted ID absent, no survivors, or one eligible survivor; equal timestamps and fallback timestamps are unchanged sorting details. | Missing ID or zero survivors => null; one survivor => it; multiple ties retain descending-ID rule. | Control: unchanged guards and sorter; zero and many covered by R03 and R01/R02. | Deleting sole real thread reaches no-survivor branch; impossible missing deleted shell or malformed timestamps are **labelled harness** cases. R03 is the changed visually distinct null-fallback representative. | **recording if checked**; no separate minimum clip. | **CODE-TRACED — unchanged shapes** |
| C06 — Layout/platform/transport equivalents | Light/dark; narrow drawer/wide sidebar; collapsed/expanded shelves; long names; older Settled pages; connected/connecting/error/offline; local/relay/tunnel; different providers. | Same candidate rules when a delete successfully reaches the hook; unchanged layouts and availability gating. | No layout/style/contract/transport/provider-adapter changes. | Existing app settings/resizing/connection setup. Native mobile uses its separate list action mutation and is **not an affected fixture**. | No Cartesian product. R01–R06 web and R07 desktop cover the changed client path; mobile capture not required. | **CODE-TRACED — excluded duplicate captures** |

## Coverage and handoff

Inspected the complete three-file diff, exact head/base metadata and PR description, comments/reviews, the helper's production/test callers, deletion/confirmation/navigation branches, snooze and sort logic, sidebar partition/shelves, archived settings caller, index/draft landing, client-settings persistence, RPC creation/placement contracts, scripted-provider reference, and the separate mobile deletion path. Forge snapshot reported 41 successful checks, no failing/pending checks; that is not capture proof. No tests were rerun, no app/browser/server started, no worktree files changed, and no forge mutation/comment/push performed. The only upload is this synthetic fixture document. Disk check at start: 15 GB available, above the 5 GB stop threshold.

FRICTION: none

VERDICT: INSUFFICIENT R01, R02, R03, R04, R05, R06, R07 recordings missing
