Summary
In browser mode with a persistent manual-login profile, an Oracle-owned ChatGPT tab can survive an error after its browser lease is released. If the tab is intentionally kept for recovery, a later successful session --harvest / reattach still appears to close only the CDP attachment, not retire the original Oracle-owned page target.
This means retained error/recovery tabs can accumulate independently of maxConcurrentTabs: the active lease count stays bounded, but old Oracle-owned targets can remain in the shared Chrome while later runs acquire freed leases and open new targets.
I am not suggesting that all failed/rate-limited tabs should be closed immediately. I have local cases where a run first failed on a visible ChatGPT rate-limit warning and a later harvest recovered useful output. The gap is the lack of a bounded retirement path after recovery is no longer needed or after recovery succeeds.
Environment
Oracle CLI: 0.18.0
Engine: browser
Profile: persistent manual-login profile
maxConcurrentTabs: 3
Platform: macOS
I also checked current main at bbc1b3b0261d (2026-08-28); the lifecycle described below is still present there.
What I observed
During repeated browser work across multiple independent work units, the shared Oracle Chrome accumulated dozens of old ChatGPT tabs while the configured active lease limit remained bounded.
The persistent Chrome profile also retained session state containing roughly 48-49 distinct ChatGPT conversation URLs in its recent Default/Sessions/Tabs_* files. I do not claim every retained conversation came from this exact path, but it is consistent with the visible tab accumulation.
For the rate-limit trigger specifically, I found eight local Oracle sessions that correctly recorded the ChatGPT modal as:
code: chatgpt-ui-warning
uiWarning.type: rate_limit
message: Too many requests ... You're making requests too quickly ...
At least three of those sessions were later harvested successfully (harvest.state=completed), which is why immediate close-on-error would be unsafe.
Source-level lifecycle
Current src/browser/index.ts has:
function shouldCloseOwnedRunTargetAfterRun(options: {
runStatus: "attempted" | "complete";
ownsTarget: boolean;
keepBrowser: boolean;
closeOwnedTabOnComplete?: boolean;
}): boolean {
return (
options.runStatus === "complete" &&
options.ownsTarget &&
(Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser)
);
}
The browser lease is still released from the run's finally path. So an errored/incomplete run can release its lease without closing its owned target.
With another active lease keeping the shared Chrome alive, this sequence is possible:
run A opens/owns target A
-> run A errors after submission
-> runStatus remains "attempted"
-> shouldCloseOwnedRunTargetAfterRun(...) == false
-> lease A is released
-> another lease keeps shared Chrome alive
-> target A remains open for possible recovery
-> a later run acquires the freed lease and opens target A2
Repeating this grows retained page targets without violating maxConcurrentTabs.
The recovery path also seems to lack a retirement step. In current src/browser/reattach.ts, successful reattach/harvest calls closeAttached(), which closes the CDP connection to the target, then returns the recovered answer. It does not appear to close the original Oracle-owned target after recovery succeeds.
So the lifecycle can become:
error/incomplete
-> keep target for recovery # desirable
-> release lease
-> later harvest/reattach succeeds
-> session can become completed
-> original target still remains # no explicit retirement path found
Rate-limit is a trigger, not the core bug
PR #395 correctly improved ChatGPT rate-limit detection. I am not reporting that rate limits are undetected.
Rate limiting just makes this lifecycle easy to exercise: it produces terminal-for-now browser errors, and retries after the lease is freed can create additional targets. The underlying issue is target lifetime versus lease/session lifetime.
There is also an adjacent classification edge in the generic UI-warning path: a warning discovered while constructing createAssistantTimeoutError() is emitted with stage="assistant-timeout", while browser preservation is stage-based. That can make a UI warning follow the same preservation lifecycle as a genuine incomplete assistant capture. This is secondary to the missing post-recovery retirement path above.
Expected behavior
I think recovery retention and tab cleanup need to be separated explicitly. Possible directions:
- Keep Oracle-owned targets for genuinely recoverable errors, but retire them after a successful harvest/reattach once the recovered output is persisted.
- Close owned targets immediately for errors known to be terminal/non-recoverable.
- Add a bounded retention/TTL or reconciliation cleanup for recoverable targets that are never harvested.
- Track target ownership/recoverability explicitly enough that cleanup does not risk closing a still-useful long-running response.
A regression test could hold one shared-profile lease open, make another owned run enter an error/recoverable state and release its lease, then verify that:
- the target is retained while recovery is still needed, and
- the target is retired after successful recovery (or another explicit terminal reconciliation event).
Related work
Those changes explain why retaining a target can be necessary. This report is about the opposite edge of the lifecycle: a retained target can outlive its useful recovery window while its lease is already reusable.
Summary
In browser mode with a persistent manual-login profile, an Oracle-owned ChatGPT tab can survive an error after its browser lease is released. If the tab is intentionally kept for recovery, a later successful
session --harvest/ reattach still appears to close only the CDP attachment, not retire the original Oracle-owned page target.This means retained error/recovery tabs can accumulate independently of
maxConcurrentTabs: the active lease count stays bounded, but old Oracle-owned targets can remain in the shared Chrome while later runs acquire freed leases and open new targets.I am not suggesting that all failed/rate-limited tabs should be closed immediately. I have local cases where a run first failed on a visible ChatGPT rate-limit warning and a later harvest recovered useful output. The gap is the lack of a bounded retirement path after recovery is no longer needed or after recovery succeeds.
Environment
I also checked current
mainatbbc1b3b0261d(2026-08-28); the lifecycle described below is still present there.What I observed
During repeated browser work across multiple independent work units, the shared Oracle Chrome accumulated dozens of old ChatGPT tabs while the configured active lease limit remained bounded.
The persistent Chrome profile also retained session state containing roughly 48-49 distinct ChatGPT conversation URLs in its recent
Default/Sessions/Tabs_*files. I do not claim every retained conversation came from this exact path, but it is consistent with the visible tab accumulation.For the rate-limit trigger specifically, I found eight local Oracle sessions that correctly recorded the ChatGPT modal as:
At least three of those sessions were later harvested successfully (
harvest.state=completed), which is why immediate close-on-error would be unsafe.Source-level lifecycle
Current
src/browser/index.tshas:The browser lease is still released from the run's
finallypath. So an errored/incomplete run can release its lease without closing its owned target.With another active lease keeping the shared Chrome alive, this sequence is possible:
Repeating this grows retained page targets without violating
maxConcurrentTabs.The recovery path also seems to lack a retirement step. In current
src/browser/reattach.ts, successful reattach/harvest callscloseAttached(), which closes the CDP connection to the target, then returns the recovered answer. It does not appear to close the original Oracle-owned target after recovery succeeds.So the lifecycle can become:
Rate-limit is a trigger, not the core bug
PR #395 correctly improved ChatGPT rate-limit detection. I am not reporting that rate limits are undetected.
Rate limiting just makes this lifecycle easy to exercise: it produces terminal-for-now browser errors, and retries after the lease is freed can create additional targets. The underlying issue is target lifetime versus lease/session lifetime.
There is also an adjacent classification edge in the generic UI-warning path: a warning discovered while constructing
createAssistantTimeoutError()is emitted withstage="assistant-timeout", while browser preservation is stage-based. That can make a UI warning follow the same preservation lifecycle as a genuine incomplete assistant capture. This is secondary to the missing post-recovery retirement path above.Expected behavior
I think recovery retention and tab cleanup need to be separated explicitly. Possible directions:
A regression test could hold one shared-profile lease open, make another owned run enter an error/recoverable state and release its lease, then verify that:
Related work
running.Too many requestsmodal as a rate limit.Those changes explain why retaining a target can be necessary. This report is about the opposite edge of the lifecycle: a retained target can outlive its useful recovery window while its lease is already reusable.