openclaw/openclaw · critical · CodexAppServerUnsafeSubscriptionError
Codex retired session subscription could not be released: ${
Error message
Codex retired session subscription could not be released: ${binding.threadId} What it means
Thrown as CodexAppServerUnsafeSubscriptionError during incognito-session retirement when neither releaseCodexAppServerLiveThread nor unsubscribeCodexThreadBestEffort could release the native Codex thread subscription. Incognito/ephemeral threads bypass idle eviction, so they must be explicitly unsubscribed at session end; failing both release paths means a native subscription could outlive its owning OpenClaw session, which the code refuses to allow silently.
Source
Thrown at extensions/codex/src/app-server/session-retirement.ts:63
const clientLease = retainSharedCodexAppServerClientByInstanceId(binding.clientId);
if (!clientLease) {
return result;
}
try {
// Reset retires native-child ownership before unsubscribing its parent;
// late child completions must never reach a replacement session generation.
codexNativeSubagentMonitorRuntime.retireParent(clientLease.client, binding.threadId);
const released = await releaseCodexAppServerLiveThread(clientLease.client, binding.threadId);
if (!released && isIncognitoSessionKey(params.identity.sessionKey)) {
// Ephemeral threads have no rollout to resume, so they intentionally
// bypass idle eviction but still end with their owning OpenClaw session.
const unsubscribed = await unsubscribeCodexThreadBestEffort(clientLease.client, {
threadId: binding.threadId,
timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS,
});
if (!unsubscribed) {
await closeCodexStartupClientBestEffort(clientLease.client);
throw new CodexAppServerUnsafeSubscriptionError(
`Codex retired session subscription could not be released: ${binding.threadId}`,
);
}
}
} finally {
clientLease.release();
}
return result;
});
}
View on GitHub (pinned to 01804a7531)
Solutions
- Check whether the Codex app-server process backing binding.clientId is alive and reachable before retirement.
- Retry retirement after re-establishing the shared client (retainSharedCodexAppServerClientByInstanceId returns null once the physical client is gone, which short-circuits safely).
- Increase CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS if the app-server is healthy but slow to acknowledge unsubscribe.
- If the client is irrecoverable, close it via closeCodexStartupClientBestEffort (already attempted before the throw) and let the next session start fresh.
Example fix
// before: retire while app-server is down
await retireCodexAppServerSessionGeneration({ bindingStore, identity, mode: "retire" }); // throws CodexAppServerUnsafeSubscriptionError
// after: confirm client health, then retire
const lease = retainSharedCodexAppServerClientByInstanceId(binding.clientId);
if (!lease) return; // physical client already gone, nothing to release
try { await retireCodexAppServerSessionGeneration({ bindingStore, identity, mode: "retire" }); }
finally { lease.release(); } Defensive patterns
Strategy: try-catch
Validate before calling
import { retainSharedCodexAppServerClientByInstanceId } from "./shared-client.js";
async function canReleaseIncognitoThread(clientId: string | undefined, threadId: string): Promise<boolean> {
if (!clientId) return true; // nothing to release
const lease = retainSharedCodexAppServerClientByInstanceId(clientId);
if (!lease) return true; // physical client gone, retirement will short-circuit
try {
return await isCodexAppServerThreadLive(lease.client, threadId);
} finally {
lease.release();
}
} Type guard
import { isIncognitoSessionKey } from "../incognito-session.js";
function isIncognitoRetirementAtRisk(identity: { sessionKey?: string }, binding?: { threadId?: string; clientId?: string }): boolean {
return Boolean(identity.sessionKey && isIncognitoSessionKey(identity.sessionKey) && binding?.threadId && binding?.clientId);
} Try / catch
import { CodexAppServerUnsafeSubscriptionError } from "./attempt-client-cleanup.js";
try {
await retireCodexAppServerSessionGeneration({ bindingStore, identity, mode: "retire" });
} catch (err) {
if (err instanceof CodexAppServerUnsafeSubscriptionError) {
// Log threadId, surface to operator; the client was already closed best-effort.
// Do not retry unchanged; re-establish the physical client first.
logger.error({ threadId: err.message }, "incognito subscription release failed");
throw err;
}
throw err;
} Prevention
- Confirm the Codex app-server process is reachable before retiring incognito sessions.
- Keep CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS sized to the slowest healthy app-server ack.
- Treat CodexAppServerUnsafeSubscriptionError as fatal-for-the-session, not as a transient retry; re-establish the client first.
When it happens
Trigger: Retiring or resetting an incognito session whose binding holds a live threadId; the Codex app-server is unreachable, the client lease's physical client is closed, or the unsubscribe RPC times out (CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS) and releaseCodexAppServerLiveThread also reports not released.
Common situations: Codex app-server process crashed or was restarted between turn and retirement; network proxy interruption to the app-server socket; aggressive session teardown while a native turn is still settling; corrupted client lease whose underlying socket is dead but the lease object is still retained.
Related errors
- Codex could not confirm admitted apps for thread ${params.th
- Codex remote workspace file transfer timed out.
- Codex retained thread subscription could not be released: ${
- Invalid URL supplied to sandbox http/request
- Blocked non-HTTP(S) protocol in sandbox http/request: ${pars
AI-assisted analysis of openclaw/openclaw@01804a7531 (2026-08-12).
Data as JSON: /api/errors/2c9d7b5f19cb57c1.
Report an issue: GitHub.