Skip to content

fix(broker): self-terminate on idle to reap orphaned shared brokers (#450)#457

Open
HoneyTyagii wants to merge 1 commit into
openai:mainfrom
HoneyTyagii:fix/broker-idle-timeout
Open

fix(broker): self-terminate on idle to reap orphaned shared brokers (#450)#457
HoneyTyagii wants to merge 1 commit into
openai:mainfrom
HoneyTyagii:fix/broker-idle-timeout

Conversation

@HoneyTyagii

Copy link
Copy Markdown

Summary

Fixes the shared/co-owned broker orphan described in #450 by adding a broker-side idle timeout, the resolution proposed in that issue (and the umbrella #108).

PR #381 keys broker teardown on the owning session(s): broker.json records sessionIds, a reused broker adds the reusing session as a co-owner, and teardownBrokersForSession shuts a broker down only once no owner remains. That correctly stops one session from tearing down a broker its siblings still use, but it leaves a gap:

  • Abnormal exit — a co-owning session that disappears without running its SessionEnd hook (SIGKILL, OOM, crash, host reboot) leaves its sessionId in broker.json forever. Later owners exit gracefully, still see the dead owner as "remaining", and preserve the broker — but no future hook ever fires for the dead owner, so the broker is orphaned indefinitely.
  • Lock-contention skip — if an ending session cannot acquire the short-timeout broker.json lock within the SessionEnd budget, it skips the entry and never retries, leaving its sessionId behind with the same result.

Both share the same unresolvable core: a departed session's sessionId lingers and later teardown has no reliable, cross-platform way to tell it is dead. A hook-side liveness signal is not a sound fix (see the reverted process.ppid/kill(pid, 0) approach discussed in #381).

Fix

Solve it broker-side: app-server-broker.mjs now self-terminates after an idle period with no connected client. This is platform-independent, needs no PID/liveness signal, and covers the abnormal-exit orphan, the dead-co-owner orphan, and the lock-contention skip in one mechanism. The session-side teardown from #381 remains a best-effort eager optimization; the idle timeout is the correctness backstop.

Behavior

  • The idle timer arms when the server starts listening and whenever the last client disconnects, and disarms as soon as a client connects. So the broker only counts down while genuinely idle and never terminates out from under an active client.
  • Configurable via --idle-timeout <ms> or the CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MS env var. Default is 30 minutes. A value <= 0 (or non-finite/negative) disables self-termination, preserving the previous behavior for callers that manage lifecycle themselves.
  • On timeout the broker performs the same graceful shutdown() used by broker/shutdown/SIGTERM (closes client sockets, closes the app-server client, removes the socket and pid file), then exits 0. A reuse check by later commands simply sees the endpoint gone and spawns a fresh broker.

Tests

Added tests/broker-idle-timeout.test.mjs (spawns the real broker against the fake codex fixture):

  • self-terminates after the idle timeout when no client is connected
  • stays alive while a client is connected, then exits after it disconnects
  • keeps running while idle when the timeout is disabled (--idle-timeout 0)

All three pass locally. (Two unrelated suite failures are pre-existing Windows-only artifacts: the Unix-socket path assertion in broker-endpoint.test.mjs and the symlink test in the review-context suite — neither touches this change.)

Relates to #108, #380, #381.

…penai#450)

A shared/worktree broker records its owning sessionIds in broker.json and is torn down only once no owner remains. If a co-owning session disappears without running its SessionEnd hook (SIGKILL, OOM, crash, host reboot) or its teardown skips the entry on lock contention, its sessionId lingers forever and no future hook fires for it, orphaning the broker indefinitely.

Fix it broker-side: app-server-broker.mjs now self-terminates after an idle timeout with no connected client. This is platform-independent, needs no PID/liveness signal, and covers the abnormal-exit orphan, the dead-co-owner orphan, and the lock-contention skip in one mechanism (see openai#108, openai#380, openai#450).

The timeout is configurable via --idle-timeout <ms> or CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MS (default 30m); a value <= 0 disables it. The timer arms on listen and whenever the last client disconnects, and disarms while a client is connected.

Copilot AI 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.

Pull request overview

Adds a broker-side idle timeout to prevent orphaned shared/co-owned broker processes when session-side teardown is skipped or never runs (e.g., SIGKILL/OOM/crash), aligning with the remediation proposed in issues #108 / #450.

Changes:

  • Implement broker self-termination after a configurable idle period with no connected clients (--idle-timeout / CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MS, default 30m).
  • Arm/disarm the idle timer based on client connect/disconnect events and on initial listen.
  • Add integration-style tests that spawn the real broker against the fake codex fixture to validate timeout, “stay alive while connected”, and “disabled timeout” behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
plugins/codex/scripts/app-server-broker.mjs Adds idle-timeout flag/env parsing and lifecycle logic to self-terminate when idle.
tests/broker-idle-timeout.test.mjs New tests that spawn the broker and validate idle-timeout behavior end-to-end.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +40
function resolveIdleTimeoutMs(optionValue, env = process.env) {
const raw = optionValue ?? env[IDLE_TIMEOUT_ENV];
if (raw === undefined || raw === null || raw === "") {
return DEFAULT_IDLE_TIMEOUT_MS;
}
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) {
return DEFAULT_IDLE_TIMEOUT_MS;
}
return parsed;
}
Comment on lines +1 to +2
import fs from "node:fs";
import net from "node:net";

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 00d3f931c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +36 to +37
if (!Number.isFinite(parsed) || parsed < 0) {
return DEFAULT_IDLE_TIMEOUT_MS;

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 Honor negative timeout values as disabled

When callers set --idle-timeout -1 or CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MS=-1 to follow the new <= 0 disable contract, this branch replaces the value with the 30-minute default. Since armIdleTimer() only disables when idleTimeoutMs <= 0, those callers still get an idle shutdown instead of preserving externally managed lifecycle; return a non-positive value for parsed negatives instead of the default.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants