Skip to content

fix(server): a draft can retry its first send after a failed bootstrap - #8226

Merged
shivamhwp merged 4 commits into
pingdotgg:mainfrom
shivamhwp:fix/draft-retry-after-bootstrap-failure
Aug 28, 2026
Merged

fix(server): a draft can retry its first send after a failed bootstrap#8226
shivamhwp merged 4 commits into
pingdotgg:mainfrom
shivamhwp:fix/draft-retry-after-bootstrap-failure

Conversation

@shivamhwp

@shivamhwp shivamhwp commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Sending the first message on a new thread bootstraps thread.create and thread.turn.start together. When the bootstrap fails partway (worktree prep on a repo with no commits, a setup script, a dropped connection), the server rolls back with thread.delete. That is a soft delete, and the draft keeps its client-minted thread id, so the retry's thread.create hit requireThreadAbsent and was refused with "Thread already exists and cannot be created twice", on every retry, until the draft was abandoned. #7664 rotates the id on the client when the server reports the deletion, which covers the common path but not an interrupted send or a failed cleanup.

requireThreadAbsent now only blocks on a live row. Each per-thread projector drops its rows for the old incarnation when it applies thread.created, so a re-created id starts with an empty timeline and per-projector replay stays deterministic. A replayed thread.deleted that a later thread.created supersedes no longer removes attachment files, since those already belong to the new incarnation. Both thread.create paths in ws.ts wait for the deletion reactor to drain first, so the old incarnation's session stop and terminal close always finish before the new thread can own those resources.

Closes #4647. Closes #5721.

Verification: server suites 452/452, typecheck, lint. Each new test fails with its fix reverted. Live app with client id rotation disabled: nine same-id retries against a repo with no commits, zero invariant errors, clean projection. Full report with screenshots: https://y5pnrxd87iej.postplan.dev

Out of scope, noted for follow-up: the provider resume cursor and checkpoint git refs are not cleaned on delete (pre-existing, unreachable from this path since the failed incarnation never starts a turn). The same stale-child-rows hole exists in orchestrator V2's ON CONFLICT DO UPDATE on thread.created.

Claude Fable 5 via Claude Code.

Note

Allow draft to retry first send after failed bootstrap by re-creating soft-deleted threads

  • requireThreadAbsent in commandInvariants.ts now treats soft-deleted threads as absent, so thread.create succeeds for an id whose previous incarnation was deleted.
  • WebSocket handling in ws.ts fences on thread.create: after dispatching a create, it calls threadDeletionReactor.drainThrough(created.sequence) and waits for the reactor to observe events up to that sequence and drain its worker before proceeding with bootstrap setup or returning.
  • ThreadDeletionReactor API changes from drain to drainThrough(sequence) in ThreadDeletionReactor.ts, using a SubscriptionRef watermark to track the highest observed event sequence.
  • Projections in ProjectionPipeline.ts now clear all per-thread rows on thread.created (messages, sessions, turns, activities, proposed plans, pending approvals) and only schedule attachment deletion for thread.deleted when hasEventAfter confirms no later thread.created exists for the same aggregate.
  • Event store in OrchestrationEventStore.ts gains hasEventAfter to check whether an event of a given type exists after a supplied sequence for an aggregate.
  • Risk: ThreadDeletionReactorShape and OrchestrationEventStoreShape interfaces changed — any out-of-tree implementations must now provide drainThrough(sequence) and hasEventAfter(input) respectively.

Macroscope summarized e6b5f34.


Note

Medium Risk
Changes thread lifecycle invariants, projection replay semantics, and deletion-cleanup ordering; behavior is well covered by new tests but affects core orchestration paths.

Overview
Fixes failed first-send bootstrap where thread.delete left a soft-deleted row and blocked retries with “Thread already exists.” requireThreadAbsent now only rejects when the thread is still live (deletedAt is null).

Projection replay treats a reused id as a new incarnation: on thread.created, per-thread projectors wipe messages, turns, sessions, activities, plans, and pending approvals for that id. On replayed thread.deleted, attachment cleanup runs only when eventStore.hasEventAfter finds no later thread.created for the same aggregate, so retried threads keep on-disk files.

ThreadDeletionReactor replaces drain with drainThrough(sequence), tracking the highest seen event sequence and waiting until the subscriber has caught up before draining the cleanup worker. ws.ts calls drainThrough after every successful thread.create (including bootstrap) so prior deletion cleanup (sessions/terminals) finishes before the new incarnation owns those resources.

Reviewed by Cursor Bugbot for commit e6b5f34. Bugbot is set up for automated code reviews on this repo. Configure here.

A new thread's first send creates the thread and starts the turn in one
bootstrap. When the bootstrap fails partway (worktree prep, setup script,
a dropped connection), the server rolls back with thread.delete. That is
a soft delete, the draft keeps its client-minted thread id, and the
retry's thread.create hit requireThreadAbsent, which treated the
tombstone as a live thread: "Thread already exists and cannot be created
twice", on every retry, until the draft was abandoned.

requireThreadAbsent now only blocks on a live row. Each per-thread
projector drops its rows for the old incarnation when it applies
thread.created, so a re-created id starts with an empty timeline and
per-projector replay stays deterministic. A replayed thread.deleted that
a later thread.created supersedes no longer removes attachment files,
since those already belong to the new incarnation. Both thread.create
paths in ws.ts wait for the deletion reactor to drain first, so the old
incarnation's session stop and terminal close always finish before the
new thread can own those resources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 997b3e27-051c-4fa3-adbb-b20577141519

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 25, 2026
Comment thread apps/server/src/ws.ts Outdated
…criber

The drain that gates thread.create only waited for the reactor's queue to
empty, but thread.deleted reaches that queue through an asynchronous
subscriber. A retry arriving between publish and enqueue could re-create
the id and then have the old cleanup stop the new session. The reactor now
tracks the highest event sequence its subscriber has handed on, and drain
first waits for that to reach the engine's latestSequence before draining
the worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shivamhwp
shivamhwp marked this pull request as ready for review August 25, 2026 21:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1c06a88. Configure here.

Comment thread apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts
@macroscopeapp

macroscopeapp Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This fix changes core thread lifecycle and replay semantics across projections, persistence, cleanup workers, and WebSocket sequencing. The behavior is well tested, but the cross-cutting runtime coordination and new cleanup fencing are substantial enough to merit human review.

You can add or adjust custom eligibility rules. Learn more.

@shivamhwp
shivamhwp merged commit a40aef4 into pingdotgg:main Aug 28, 2026
26 checks passed
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 29, 2026
## What's Changed
* fix(grok): improve skills, plans, usage, and turn reliability by @t3dotgg in pingdotgg/t3code#8358
* fix(server): recover stale Codex approval callbacks by @luckyPipewrench in pingdotgg/t3code#5195
* test(server): remove duplicate missing worktree test by @t3-code[bot] in pingdotgg/t3code#8252
* fix(server): replay all un-applied events during projection bootstrap by @krutftw in pingdotgg/t3code#7538
* test: remove low-signal test files by @t3-code[bot] in pingdotgg/t3code#8397
* test: prune trivial error and layout tests by @t3-code[bot] in pingdotgg/t3code#8400
* Fix Android adaptive launcher icon by @colonelpanic8 in pingdotgg/t3code#4332
* feat(web): split provider settings into list and editor by @t3dotgg in pingdotgg/t3code#8380
* fix(codex): accept Codex 0.150 account plans by @gsimone in pingdotgg/t3code#8447
* fix(tooling): allow ignored-only staged changes by @juliusmarminge in pingdotgg/t3code#8468
* fix(mobile): keep iOS home header stable by @juliusmarminge in pingdotgg/t3code#8467
* fix(web): stop showing red x summaries for ordinary tool failures by @t3dotgg in pingdotgg/t3code#8395
* fix(mobile): refine Git action toast glass styling by @juliusmarminge in pingdotgg/t3code#8399
* fix(desktop): allow preview automation in agent-created threads by @t3dotgg in pingdotgg/t3code#8483
* test(web): remove redundant cache key test by @t3-code[bot] in pingdotgg/t3code#8484
* fix(release): move nightly schedule to minute 38 by @t3dotgg in pingdotgg/t3code#8509
* fix(web): stabilize the provider settings editor by @t3dotgg in pingdotgg/t3code#8472
* fix(web): open GitHub pull requests in browser when loading fails by @t3dotgg in pingdotgg/t3code#8507
* fix(codex): show sub-agent models by @t3dotgg in pingdotgg/t3code#8502
* feat(analytics): report connected client platforms by @t3dotgg in pingdotgg/t3code#8481
* feat(server): accept PDF, ZIP, and other file uploads up to 50MB by @t3dotgg in pingdotgg/t3code#8235
* feat(web): toggle a thread's pin from the keyboard by @ipanasenko in pingdotgg/t3code#8440
* fix(web): add back button to project settings by @StiensWout in pingdotgg/t3code#8168
* refactor(mobile): compile semantic themes for Uniwind by @juliusmarminge in pingdotgg/t3code#7327
* fix(desktop): Cache Runtime locally on WSL Filesystem, dramatically improving launch times  by @ikifar2012 in pingdotgg/t3code#5769
* fix(mobile): show OpenCode model sources in picker by @juliusmarminge in pingdotgg/t3code#8573
* fix(clients): honor project default models in new threads by @anirudhsama in pingdotgg/t3code#6011
* fix(mobile): show file actions on Android by @none23 in pingdotgg/t3code#8215
* fix(connect): explain DPoP connection failures by @extoci in pingdotgg/t3code#8351
* feat(web): make the sidebar project filter a searchable combobox by @SunkenInTime in pingdotgg/t3code#5931
* fix(server): a draft can retry its first send after a failed bootstrap by @shivamhwp in pingdotgg/t3code#8226
* fix(desktop): stop hidden previews draining battery by @Bil0000 in pingdotgg/t3code#8567
* fix(desktop): oauth popups open from the browser preview by @walid-baharwal in pingdotgg/t3code#8435
* fix(web): keep long task drawers usable on small screens by @shivamhwp in pingdotgg/t3code#8313
* fix(opencode): handle child approvals, stops, and model catalogs by @t3dotgg in pingdotgg/t3code#8480
* fix: make thread auto-settling opt-in by @shivamhwp in pingdotgg/t3code#8321
* fix(web): stop session activity timing test from blocking releases by @t3dotgg in pingdotgg/t3code#8585
* fix(mobile): show composer menus when starting a task by @juliusmarminge in pingdotgg/t3code#8587
* fix(web): show the configured stash shortcut by @UtkarshUsername in pingdotgg/t3code#8437
* feat(web): add toggleable confirmation before unpinning a thread by @UtkarshUsername in pingdotgg/t3code#7313
* fix: restore automatic thread settling defaults by @t3dotgg in pingdotgg/t3code#8596
* fix(mobile): restore composer glass and rounded shadows by @juliusmarminge in pingdotgg/t3code#8597

## New Contributors
* @luckyPipewrench made their first contribution in pingdotgg/t3code#5195
* @krutftw made their first contribution in pingdotgg/t3code#7538
* @colonelpanic8 made their first contribution in pingdotgg/t3code#4332
* @ikifar2012 made their first contribution in pingdotgg/t3code#5769
* @walid-baharwal made their first contribution in pingdotgg/t3code#8435

**Full Changelog**: pingdotgg/t3code@v0.0.35...v0.0.36

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.36
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 29, 2026
Merges 58 upstream commits (`badae6a5c` → `6a9d9f988`, upstream v0.0.34
→ v0.0.36) through the `fork-upstream-merge` skill.

`merge-stats.mjs` reconciles exactly: **431 files landed** against **431
in the upstream range**, no gap to explain. Fork delta is 611 files.

## Conflicts

Three, plus one git resolved silently and wrongly.

- **`Sidebar.tsx`** — upstream replaced the project picker's
`Menu`/`MenuRadioGroup` with a searchable `Combobox` (pingdotgg#5931). Took
upstream's structure and re-applied only the
`FEATURES.projectManagement` gate on "New project", now carrying the `//
Fork:` marker it had been missing. Upstream has since grown the mobile
touch-target span itself, so the Mobile Touch Delta has nothing left to
re-apply here — one convergence, unprompted.
- **`ProviderSettingsPanel.tsx`** — upstream split provider settings
into list and editor (pingdotgg#8380, pingdotgg#8472), moving `ProviderLastChecked` and
the refresh button out of `headerAction` into the list footer. Took
upstream's and re-applied the gate as `!readOnly &&
FEATURES.serverAdministration`.
- **`pnpm-lock.yaml`** — took upstream's and re-ran `vp i`, which
restored the fork's `@t3tools/moatless-api` workspace edge.
- **`packages/contracts/src/orchestration.test.ts`** — auto-merged, no
marker, broken. Both sides appended `OrchestrationMessage` to the same
import list and the same `decodeOrchestrationMessage` const at different
offsets, so git took both. Surfaced as a parse error in lint, typecheck
and test at once. This is the case `merge-stats.mjs`'s
conflict-candidate list exists to catch.

`HostedBrowserFrame.tsx` (fork-only) also needed the `renderingActive`
prop pingdotgg#8567 made required. Upstream suspends a parked webview unless
background audio, PiP or a recording still needs it painted; a frame has
none of those to read and is the app's only copy of the preview page, so
it passes `true` and keeps today's behavior.

## Sweep

Five keyword hits, all false positives:

- `apps/web/src/connection/clientMetadata.ts` + test — reports the
client's OS/browser/device on connect (pingdotgg#8481). Auth-adjacent, but it
rides `ClientPresentation` on the relay and remote-bearer bootstraps;
the fork's primary environment sends none of it.
- `packages/client-runtime/src/relay/errorPresentation.ts` + test,
`connection/errors.test.ts` — explain DPoP failures (pingdotgg#8351). Relay only,
and T3 Connect is decided out.

## Feature classification

**Usable as-is**

- Searchable project-filter combobox in the sidebar (pingdotgg#5931).
- Long task drawers stay usable on small screens (pingdotgg#8313) — directly
relevant, the fork's phone story is `apps/web` in mobile Safari/Chrome.
- Toggleable confirmation before unpinning a thread (pingdotgg#7313); toggle
thread pin from the keyboard.
- Back button in project settings (pingdotgg#8168); the configured stash shortcut
is shown (pingdotgg#8437).
- No more red-x summaries for ordinary tool failures (pingdotgg#8395); PRs open
in the browser when loading fails (pingdotgg#8507).
- Project default models are honored in new threads (pingdotgg#6011).
- Provider settings split into list and editor (pingdotgg#8380, pingdotgg#8472) — landed,
though `/settings/providers` is itself gated behind
`serverAdministration`.

**Unsupported in Moatless / needs implementation**

- **Non-image file attachments** (pingdotgg#8235) — a turn may now carry any file
up to 50MB, advertised as `capabilities.fileAttachments.maxUploadBytes`
and sized by `PROVIDER_SEND_TURN_MAX_FILE_BYTES`. Moatless advertises
neither this nor `attachmentUploads`, so the composer's attach
affordance stays off. Costs nothing today (upstream's own web composer
offers images only; `ChatAttachment` widened just far enough to
typecheck), but it is the second capability key to report when uploads
land. Extends the existing **Attachment uploads** entry in
`docs/fork/gaps.md`.
- **Connected-client platform analytics** (pingdotgg#8481) and **DPoP failure
explanations** (pingdotgg#8351) — relay and T3 Connect surfaces, already decided
out of the fork.

No new WS methods entered the contract in this range, so no new
`UnsupportedMethodError` union entries. `unsupported-methods.mjs`
reports `ADD 0`, `DROP 1` (`scripts.run`), `KEEP 2`. The `scripts.run`
DROP is **not** actioned — it is the documented exception in the gaps
register: that union entry answers for `apps/server`, which still stubs
the method (verified surviving in `ws.ts`), not for Moatless.

**Backend behavior to consider reproducing in Moatless**

- **Replay all un-applied events during projection bootstrap** (pingdotgg#7538) —
a one-line pipeline fix upstream; Moatless runs its own projection and
the same class of bug applies.
- **A draft can retry its first send after a failed bootstrap** (pingdotgg#8226)
— Moatless owns turn start, and a draft wedged by a failed bootstrap is
the same dead end here.
- **Recover stale Codex approval callbacks** (pingdotgg#5195) — Moatless owns the
agent runtime in the sandbox.
- **Stop querying Claude context usage after turns** (pingdotgg#8610) — a cheap
correctness/cost fix in the same place Moatless polls.
- **Server-side accept and size-limit non-image uploads** (pingdotgg#8235), the
other half of the gap above.
- Auto-settling churn (pingdotgg#8321 made it opt-in, pingdotgg#8596 reverted) nets to no
change — noted so the next merge does not re-derive it.

## Verification

`verify.mjs`: `tripwires`, `fmt:check`, `lint`, `typecheck` pass. `test`
reports `@t3tools/web` **flaky, passed alone** — in the full run
`MessagesTimeline.test.tsx` skipped all 34 tests on a 30s module-import
timeout under `ChatMarkdown.tsx`; alone the package is 297 files / 3117
tests green. Load, not the merge. `inventory-check.mjs` clean.

Model: Claude Opus 5, via Claude Code in a Moatless sandbox.

---
Moatless task:
https://moatless.soaplabstest.com/tasks/a0c041bb-1426-4591-9296-6a4b0cfa2eff
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

1 participant