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

  1. Check whether the Codex app-server process backing binding.clientId is alive and reachable before retirement.
  2. Retry retirement after re-establishing the shared client (retainSharedCodexAppServerClientByInstanceId returns null once the physical client is gone, which short-circuits safely).
  3. Increase CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS if the app-server is healthy but slow to acknowledge unsubscribe.
  4. 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

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


AI-assisted analysis of openclaw/openclaw@01804a7531 (2026-08-12). Data as JSON: /api/errors/2c9d7b5f19cb57c1. Report an issue: GitHub.