Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus
, '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

Preserve provider bindings when stopping sessions - #2125

Merged
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap
Apr 17, 2026
Merged

Preserve provider bindings when stopping sessions#2125
juliusmarminge merged 3 commits into
pingdotgg:mainfrom
HaukeSchnau:codex/preserve-codex-binding-on-reap

Conversation

@HaukeSchnau

@HaukeSchnauHaukeSchnau commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Closes#2121

Summary

  • preserve provider bindings when the idle session reaper stops a session
  • keep explicit stop-session behavior destructive so manual stop still detaches the provider thread
  • add coverage for preserved-binding recovery and the reaper stop payload

Why

Idle reaping currently uses the same destructive stop path as an explicit manual stop. For the Codex provider, that drops the resume cursor and forces the next turn onto a fresh Codex thread instead of resuming the previous provider thread.

This change keeps the resume binding for idle cleanup only, so a later send can recover the same provider thread while leaving explicit stop semantics unchanged.

Verification

  • bun fmt
  • bun x vitest run apps/server/src/provider/Layers/ProviderService.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
  • bun lint
  • bun typecheck
  • manual end-to-end validation against a real T3 + Codex flow, verifying that a thread still resumes after the idle reaper stops the provider session

Notes

  • callback state for old approvals or user-input requests is still not resumable; this only preserves the provider resume handle across idle cleanup
  • bun lint passes with existing repo warnings outside this patch

Note

Medium Risk
Changes provider session lifecycle/persistence semantics (stop no longer unbinds), which can affect routing and recovery paths across providers and restarts; coverage is improved but behavior changes are system-wide.

Overview
Provider session stopping is changed from a destructive unbind to a soft stop: ProviderServiceLive.stopSession now upserts the thread binding with status: "stopped" (and clears activeTurnId) instead of deleting it, keeping the persisted provider + resume cursor so a later sendTurn can recover/resume the same provider thread.

ProviderSessionDirectory removes the remove(threadId) API entirely, and tests are updated/added to verify stopped bindings remain persisted and that routing can resume after a stop. ProviderCommandReactor session restart logic is tightened to always prefer the thread-bound provider and to only drop resumeCursor on model-change restarts, with test reordering/adjustments around restart-failure and provider-switch rejection.

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

Note

Preserve provider bindings when stopping sessions instead of deleting them

  • ProviderSessionDirectory removes the remove method; stopped sessions are now persisted with status: 'stopped' and activeTurnId: null rather than deleted from the directory.
  • ProviderService.stopSession upserts the binding on stop, so a subsequent sendTurn can resume from the correct resumeCursor and provider without re-binding from scratch.
  • ProviderCommandReactor no longer restarts a session solely due to a provider change; it always uses the thread's bound provider and preserves resumeCursor unless a runtime mode or model-change condition requires a restart.
  • Behavioral Change: Sessions that were previously cleaned up on stop are now retained in persistence; any code path that relied on a missing binding after stop will behave differently.

Macroscope summarized b33fd1f.

@coderabbitai

coderabbitaiBot commented Apr 17, 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

Run ID: 94b6f852-fad1-407c-bfd7-4748337e9e61

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@github-actionsgithub-actionsBot added size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 17, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:560da15384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reaped = yield* providerService
.stopSession({
threadId: binding.threadId,
preserveBinding: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear preserved binding when explicit stop is requested

Calling providerService.stopSession with preserveBinding: true during idle reaping leaves a persisted resume binding even after the runtime session is closed, but processSessionStopRequested only performs a destructive provider stop when thread.session.status !== "stopped" (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:714-716). In the common reaped state (session already marked stopped), an explicit thread.session.stop command no longer removes that binding, so a later turn can still resume via the persisted cursor (ProviderService.startSession reuses persisted binding state). This makes explicit stop non-destructive for already-reaped threads and breaks the stated manual-stop semantics for that scenario.

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeappBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by removing the ability to delete provider bindings on stop. Two unresolved review comments identify that the implementation contradicts the stated intent - manual stops now preserve bindings when they should be destructive, allowing unintended session resumption.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member

reading this now i don't remember why we ever remove the provider binding 🤔

- Keep persisted provider bindings on stop/reap
- Remove obsolete `preserveBinding` stop flag
Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Apr 17, 2026

@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 and found 2 potential issues.

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 938a082. Configure here.

runtimePayload: {
activeTurnId: null,
},
});

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.

Manual stop preserves binding, contradicting stated destructive intent

High Severity

stopSession now always upserts the binding with status: "stopped", but the PR intent is to only preserve bindings for idle-reaper stops while keeping manual/explicit stops destructive. The preserveBinding conditional mentioned in the PR description was never implemented — ProviderStopSessionInput still only has threadId, and the remove method was completely deleted from ProviderSessionDirectory. After a manual stop, the next sendTurn will recover the same provider thread via the persisted resumeCursor instead of starting fresh.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 938a082. Configure here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
@juliusmarmingejuliusmarminge changed the title Preserve provider bindings when idle-reaping sessionsPreserve provider bindings when stopping sessionsApr 17, 2026
- Keep an existing thread bound to its provider after reap
- Add regression coverage for first-turn and post-bind provider changes
@juliusmarminge
juliusmarminge merged commit 721b6b4 into pingdotgg:mainApr 17, 2026
12 checks passed
juliusmarminge added a commit to nexxeln/t3code that referenced this pull request Apr 17, 2026
The `remove` method was removed from `ProviderSessionDirectoryShape` in
pingdotgg#2125 (Preserve provider bindings when stopping sessions). The test mock
still referenced it, causing a typecheck failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roni-estein added a commit to roni-estein/t3code that referenced this pull request Apr 17, 2026
alecramos-sudo added a commit to alecramos-sudo/t3code that referenced this pull request Apr 20, 2026
Brings in upstream changes since d22c6f5:
- Claude Opus 4.5 built-in model (pingdotgg#2143)
- Claude Opus 4.7 built-in model (pingdotgg#2072)
- opencode provider support (pingdotgg#1758)
- Claude process leak fix & session reaper (pingdotgg#2042)
- Configurable project grouping (pingdotgg#2055)
- Node-native TypeScript for desktop/server (pingdotgg#2098)
- Clean up invalid pending approval projections (pingdotgg#2106)
- Thread status in command palette (pingdotgg#2107)
- Backend startup readiness coordination (pingdotgg#2133)
- Preserve provider bindings on session stop (pingdotgg#2125)
- Allow deleting non-empty projects (pingdotgg#1264)
- Various Windows fixes and CI improvements
Conflict resolutions:
- model.ts: kept claudeAgent opus-4-7 default, added opencode entry
- settings.ts: adopted upstream sidebarProjectGroupingMode/Overrides
- ClaudeProvider.ts: adopted upstream Opus 4.7 effort config
- Sidebar.tsx: adopted upstream project grouping approach
- package.json: kept custom desktop-only build filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lastobelus

Copy link
Copy Markdown

ouch, this caused a lot of pain in the last few days. I have to go apologize to several codex threads.

tyulyukov added a commit to tyulyukov/marcode that referenced this pull request Apr 23, 2026
…orphaned-session stop
**1. One card per file for multi-file patches**
Multi-file tool calls (apply_patch with N file markers, Claude MultiEdit,
etc.) previously rendered as a single collapsed "N files changed — Show
full diff" card. That UX forced the user to expand the aggregate just to
see any one file's diff.
`MessagesTimeline.logic.ts` now splits a file-change entry with N
`diffPreviews` hunks into N individual file-change rows. Each row reuses
the existing FileChangeCard single-hunk render path, so users see inline
diff previews for every file without expanding. Row ids are suffixed with
`#<index>` to keep each row's identity stable across updates.
**2. Stop button no longer deadlocks on orphaned provider sessions**
User hit this repro: thread shows "Working" for hours, clicking the red
stop button emits
`ProviderValidationError: Cannot recover thread '<id>' because no
provider resume state is persisted`
into the logs, and the UI stays stuck. Root cause:
- MarCode's `stopSession` deletes the provider binding (commit 8940163,
diverges from upstream pingdotgg#2125) — so after a crash/restart the binding
can exist with status="stopped" or with resumeCursor cleared.
- `ProviderService.interruptTurn` used `allowRecovery: true`, which
tried to RESURRECT the dead session via `recoverSessionForThread`
just to kill it. If resume state is missing → validation error
bubbles out → interrupt fails → thread.session.status stays
"running" → UI stays "Working".
Fixes in two layers:
- `ProviderService.interruptTurn` now uses `allowRecovery: false` and
skips the adapter call when `isActive === false`. You can't interrupt
a session that doesn't exist; there's nothing to kill.
- `ProviderCommandReactor.processTurnInterruptRequested` wraps the
provider interrupt in `Effect.catchCause`. If the provider call fails
(or, now more rarely, the adapter is no-op because recovery is off),
the reactor dispatches a `thread.session.set` with status="interrupted"
and `activeTurnId: null`, so the UI unblocks from "Working" regardless
of what the provider does. Also appends a visible failure activity
explaining "session was orphaned; force-stopped. Send a new message to
resume."
**Verification**
- Typecheck: all 9 packages clean.
- @marcode/web: 1074 tests pass (incl. 14 MessagesTimeline.logic).
- @marcode/server: 995 pass, 5 skipped (incl. 24 ProviderCommandReactor,
26 ProviderService).
- Browser: 161 pass, 6 skipped.
MuneerAhmed03 pushed a commit to MuneerAhmed03/t3code that referenced this pull request Apr 26, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
youpele52 referenced this pull request in youpele52/bigbud Jun 17, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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.

[Bug]: Claude losing earlier messages / not compacting correctly

3 participants

@HaukeSchnau@juliusmarminge@lastobelus