fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(server): retrying a new thread's first send no longer fails with "already exists" - #7608

Closed
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete
Closed

fix(server): retrying a new thread's first send no longer fails with "already exists"#7608
pc-style wants to merge 4 commits into
pingdotgg:mainfrom
pc-style:fix-thread-recreate-after-delete

Conversation

@pc-style

@pc-stylepc-style commented Aug 19, 2026

Copy link
Copy Markdown

Sending the first message on a new thread could fail during worktree prep. After that, every retry from the same draft errored with "Thread ... already exists and cannot be created twice", forever. The cleanup's thread.delete is a soft delete, and requireThreadAbsent counted the soft-deleted row as still occupying the id, which draft threads keep across retries.

The decider now treats a soft-deleted thread as absent, so the retry re-creates the id. The projection pipeline also purges the old incarnation's dependent rows on re-create, so the thread starts clean. Tests cover the invariant, a create/delete/re-create decider round-trip, and an engine-dispatch check that the purge runs.

Proof of work

Backend-only change (apps/server), no UI surface, so no screenshots.

  • vp test run on the three touched test files: 3 files, 30 tests, all pass
  • tsgo --noEmit in apps/server: clean
  • vp lint on the five touched files: clean

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes orchestration invariants and multi-projector thread lifecycle behavior; scoped to soft-delete/re-create paths with solid test coverage.

Overview
Fixes first-send retries on new drafts that reuse the same client-generated thread id after a failed attempt triggers cleanup via soft thread.delete.

Command invariants:requireThreadAbsent now treats threads with deletedAt set as absent, so thread.create with the same id is allowed instead of failing with "already exists".

Projections: On thread.created, dependent projectors purge prior rows for that thread id (messages, turns, activities, proposed plans, sessions, pending approvals). The threads projector still upserts a fresh shell with deletedAt: null. That keeps live dispatch and per-projector replay (threads projector runs last) aligned with the new incarnation.

Tests cover the invariant, decider delete→recreate, engine dispatch purge, and bootstrap replay.

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

Note

Fix retry of new thread's first send failing with "already exists" after deletion

  • requireThreadAbsent in commandInvariants.ts now treats a thread as absent if it is soft-deleted (deletedAt !== null), allowing re-creation with the same id
  • Every projector in ProjectionPipeline.ts (messages, turns, sessions, activities, proposed plans, pending approvals) purges all existing rows for the thread on thread.created, so stale projections from a prior incarnation don't conflict
  • ThreadDeletionReactor in ThreadDeletionReactor.ts skips provider session stop and terminal close when the thread id has already been re-created and is live
  • Behavioral Change: a soft-deleted thread id can now be re-used immediately; the thread.created event now performs destructive deletes across all projection tables for that threadId before re-inserting

Macroscope summarized a27f2b0.

@coderabbitai

coderabbitaiBot commented Aug 19, 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: af55f9d4-9aaf-4606-8be3-9f5d6b3631af

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
Comment threadapps/server/src/orchestration/commandInvariants.ts
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Aug 19, 2026
@SunkenInTime

Copy link
Copy Markdown
Contributor

Taking a look at this

@Osman-bin-nasir

Copy link
Copy Markdown

LGTM!

@macroscopeapp

macroscopeappBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix enables thread ID reuse after deletion by purging old projection data on thread recreation. While the changes are well-scoped and heavily tested, an unresolved Medium severity finding about error handling in the deletion reactor warrants human review.

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

@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from d67bd7e to b602fb0CompareAugust 20, 2026 00:05
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ProjectionPipeline.ts Outdated
pc-styleand others added 2 commits August 20, 2026 11:26
…"already exists"
Thread deletion is a soft delete, but requireThreadAbsent treated any
row with the thread id as occupying it. When a first-turn bootstrap
failed (e.g. worktree prep from origin), its cleanup thread.delete only
marked deletedAt, so every retry from the same draft, which keeps its
client-generated thread id, was rejected with "Thread already exists
and cannot be created twice", permanently.
A soft-deleted thread now counts as absent for thread.create. Both the
in-memory projector and the SQLite projection pipeline already upsert
thread.created wholesale (including deletedAt: null), so re-creation
resets the row cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A re-created thread id (draft retry after a failed first-turn bootstrap)
kept the old incarnation's message, activity, plan, session, turn, and
pending-approval rows, so detail hydration could expose stale history.
Each projector now clears its own rows when it applies thread.created,
so per-projector replay from any cursor rebuilds the new incarnation
correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pc-style
pc-styleforce-pushed the fix-thread-recreate-after-delete branch from f62a9ce to 2be91ffCompareAugust 20, 2026 11:26
Comment threadapps/server/src/orchestration/commandInvariants.ts
The deletion reactor stops sessions and closes terminals keyed only by
threadId, asynchronously after thread.deleted. Now that a draft retry
can re-create a deleted thread id, queued cleanup could race the new
incarnation and stop its fresh session. Cleanup now skips when the
thread row is live again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadapps/server/src/orchestration/Layers/ThreadDeletionReactor.ts Outdated
A failed projection read now propagates to the worker's logging catch
instead of defaulting to the destructive path. The idle-session reaper
backstops any cleanup this skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursorcursorBot 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.

Reviewed by Cursor Bugbot for commit a27f2b0. Configure here.

@shivamhwp

Copy link
Copy Markdown
Collaborator

sol on behalf of shivamhwp

Thanks for the work here. We reproduced the exact failure this PR targets. After a failed first-send bootstrap, retrying reused a deleted thread ID and hit thread.create ... already exists and cannot be created twice.

#7664 fixes that same path and has now merged. We verified it through the real web app. After the initial bootstrap failure, the retry minted a fresh thread ID and reached the original Git failure again instead of the invariant error.

We are closing this as superseded because #7608 fixes the symptom by allowing a soft-deleted ThreadId to be created again. That changes the event identity rule, adds projection cleanup work to every thread.created, and leaves a race where old deletion cleanup can target resources from the recreated thread lifetime. #7664 avoids those risks by keeping the draft and assigning each bootstrap attempt a new ThreadId.

Thank you for digging into this and for the test coverage.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pc-style@SunkenInTime@Osman-bin-nasir@shivamhwp