Summary
In the persistent local Webcmd daemon, releasing or closing the final leased page can cause Chromium to close its browser context. A command arriving immediately afterward may reuse the runtime before the asynchronous context close event invalidates it, then fail on its first navigation with:
Target page, context or browser has been closed
This is a Webcmd runtime lifecycle race, independent of benchmark isolation, network quality, model behavior, the serialization issue, CloakBrowser session limits in #225, and the profile-prefix teardown bug in #242.
Current behavior
CloakSessionManager.getPage() adopts the first existing Chromium page when the profile has no tracked pages:
const existingPages = candidate.context.pages();
return !freshPage && existingPages[0] && candidate.pages.size === 0
? existingPages[0]
: this.createPage(candidate.context, input.windowMode);
That initial about:blank page therefore becomes an ordinary leased user page.
When the session is released, Webcmd removes and closes every matching non-persistent page:
if (entry.siteSession !== "persistent" && !pageIsClosed(entry.page)) {
await entry.page.close().catch(() => {});
}
If this is the final Chromium page, page.close() can begin browser/context shutdown. The call can resolve before Chromium shutdown propagation completes. During that interval:
- The old
ProfileRuntime is still present in profiles.
- A new command obtains that runtime.
- It creates or adopts a page.
- The old context finishes closing.
- The new command fails during
goto() with the generic target-closed error.
Waiting briefly between release and the next command avoids the failure because the context close event has time to run invalidateProfileRuntime(). Immediate reuse reproduces it.
The same invariant can be violated through release(), explicit closePage(), freshPage, and idle-timeout cleanup.
Expected behavior
Ending a task/session should release its user pages without implicitly shutting down the profile runtime owned by the persistent daemon. A subsequent command should be able to create and navigate a page immediately.
Separate profiles must remain independent and usable concurrently. For example, these should work without restarting the daemon:
webcmd --profile peer twitter login
webcmd --profile actor twitter login
webcmd --profile peer twitter whoami
webcmd --profile actor twitter whoami
Suggested fix: runtime-owned anchor page
Give each ProfileRuntime one unleased about:blank anchor page:
- Reserve the initial Chromium page as
runtime.anchorPage when launching the profile runtime.
- Never lease it to a task or site session.
- Exclude it from
listPages, tab selection, page IDs, snapshots, and network capture.
- Always create a separate page for the first user lease.
- Preserve the anchor through
release(), closePage(), freshPage, and idle cleanup.
- If the anchor is unexpectedly closed while the context is healthy, recreate it under the existing per-profile page-creation lock before closing the final leased page.
- Close the anchor only when intentionally shutting down or evicting that profile runtime, shutting down the daemon, or after an unrecoverable browser disconnect.
- For
freshPage, create and register the replacement before closing the previous leased page.
The anchor must be per profile, not global. The manager already maintains a Map<string, ProfileRuntime>, and each profile has a separate user-data directory, so peer and actor would each have their own anchor and login state.
Acceptance criteria
- Repeated
release final session -> immediately create page -> navigate cycles do not produce target-closed errors.
- Closing the final visible/leased page leaves the profile context connected.
- The anchor never appears in user-facing tab/page APIs.
freshPage cannot transiently close the final runtime-owned page.
- Two named profiles can remain active and switch back and forth without a daemon restart.
- Shutting down the daemon closes every profile context and anchor.
- Intentional per-profile idle eviction closes only that profile runtime; persisted cookies remain available after relaunch.
- Tests cover explicit close, release, fresh-page replacement, idle cleanup, immediate reuse, and multiple profiles.
Additional note: window mode
Keeping an anchor prevents unnecessary background/foreground relaunches during normal daemon operation, but it does not by itself define behavior after a real daemon/browser restart or when an existing background profile receives a foreground login request. Canonical per-profile window-mode selection/promotion should be handled separately rather than coupling it to this lifecycle fix.
Summary
In the persistent local Webcmd daemon, releasing or closing the final leased page can cause Chromium to close its browser context. A command arriving immediately afterward may reuse the runtime before the asynchronous context close event invalidates it, then fail on its first navigation with:
This is a Webcmd runtime lifecycle race, independent of benchmark isolation, network quality, model behavior, the serialization issue, CloakBrowser session limits in #225, and the profile-prefix teardown bug in #242.
Current behavior
CloakSessionManager.getPage()adopts the first existing Chromium page when the profile has no tracked pages:That initial
about:blankpage therefore becomes an ordinary leased user page.When the session is released, Webcmd removes and closes every matching non-persistent page:
If this is the final Chromium page,
page.close()can begin browser/context shutdown. The call can resolve before Chromium shutdown propagation completes. During that interval:ProfileRuntimeis still present inprofiles.goto()with the generic target-closed error.Waiting briefly between release and the next command avoids the failure because the context
closeevent has time to runinvalidateProfileRuntime(). Immediate reuse reproduces it.The same invariant can be violated through
release(), explicitclosePage(),freshPage, and idle-timeout cleanup.Expected behavior
Ending a task/session should release its user pages without implicitly shutting down the profile runtime owned by the persistent daemon. A subsequent command should be able to create and navigate a page immediately.
Separate profiles must remain independent and usable concurrently. For example, these should work without restarting the daemon:
Suggested fix: runtime-owned anchor page
Give each
ProfileRuntimeone unleasedabout:blankanchor page:runtime.anchorPagewhen launching the profile runtime.listPages, tab selection, page IDs, snapshots, and network capture.release(),closePage(),freshPage, and idle cleanup.freshPage, create and register the replacement before closing the previous leased page.The anchor must be per profile, not global. The manager already maintains a
Map<string, ProfileRuntime>, and each profile has a separate user-data directory, sopeerandactorwould each have their own anchor and login state.Acceptance criteria
release final session -> immediately create page -> navigatecycles do not produce target-closed errors.freshPagecannot transiently close the final runtime-owned page.Additional note: window mode
Keeping an anchor prevents unnecessary background/foreground relaunches during normal daemon operation, but it does not by itself define behavior after a real daemon/browser restart or when an existing background profile receives a foreground login request. Canonical per-profile window-mode selection/promotion should be handled separately rather than coupling it to this lifecycle fix.