Skip to content

fix: tear down brokers by sessionId on SessionEnd to avoid orphaned worktree brokers (#380)#381

Open
hongsu wants to merge 7 commits into
openai:mainfrom
hongsu:fix/broker-session-cleanup
Open

fix: tear down brokers by sessionId on SessionEnd to avoid orphaned worktree brokers (#380)#381
hongsu wants to merge 7 commits into
openai:mainfrom
hongsu:fix/broker-session-cleanup

Conversation

@hongsu

@hongsu hongsu commented Jun 18, 2026

Copy link
Copy Markdown

Summary

Fixes #380.

handleSessionEnd tears down the broker via loadBrokerSession(input.cwd), and broker.json is stored under a cwd-derived hash (resolveStateDir). When a broker was spawned for a different cwd than the session's cwd, the lookup misses, no broker/shutdown is sent, and the broker is orphaned — even on graceful exit (/quit, SIGTERM).

This is structural in a common workflow: a session runs in the main tree (session cwd = repo root) but runs reviews against a git worktree (broker cwd = worktree). Reviewing a worktree's uncommitted working tree requires broker cwd = worktree, so the mismatch is unavoidable, and graceful exit still leaks the broker.

Fix

Decouple broker cleanup from cwd by keying it on the owning sessionId:

  • Record sessionId in broker.json on broker creation (resolveSessionId reads CODEX_COMPANION_SESSION_ID, which the SessionStart hook already sets). The reuse path is untouched, so ownership stays with the creator.
  • teardownBrokersForSession(sessionId) scans the shared state root (resolveStateRoot()) and tears down every broker.json whose sessionId matches — independent of cwd.
  • handleSessionEnd calls it with input.session_id in addition to the existing cwd-based path (kept as a fast path / fallback for legacy brokers without sessionId).

Scope

In scope: orphaning on graceful exit caused by cwd mismatch (the dominant path).

Out of scope (separate concerns): abnormal exit where SessionEnd never fires (SIGKILL/OOM, see #108), an idle-timeout fallback, and a parent-liveness watchdog. Those are complementary and tracked separately.

Changes

  • scripts/lib/state.mjs — extract resolveStateRoot() (used to scan all workspaces' state without a cwd); resolveStateDir now reuses it (behavior unchanged).
  • scripts/lib/broker-lifecycle.mjsresolveSessionId(); record sessionId in the created broker session; teardownBrokersForSession(sessionId, {killProcess}).
  • scripts/session-lifecycle-hook.mjs — export handleSessionEnd; call teardownBrokersForSession(input.session_id) after the cwd-based path. A small isMain guard wraps the top-level main() so importing the module for tests does not trigger the blocking stdin read (CLI hook execution is preserved).

Backward compatibility

  • STATE_VERSION unchanged; the sessionId field in broker.json is additive.
  • Existing loadBrokerSession / teardownBrokerSession / clearBrokerSession signatures and the cwd-based cleanup path are retained. Legacy broker.json files without sessionId are simply skipped by the session scan and still handled by the existing cwd path.

Tests

New tests/broker-lifecycle.test.mjs (node:test), 12 cases covering resolveStateRoot, resolveSessionId precedence, teardownBrokersForSession (cross-cwd teardown, non-matching sessionId left intact, legacy/no-sessionId ignored, empty-sessionId no-op), and a regression lock: handleSessionEnd({cwd: <different>, session_id: S}) tears down a broker registered for another cwd.

node --test tests/*.test.mjs: the new suite is 12/12; no new failures introduced.

🤖 Generated with Claude Code

hongsu and others added 4 commits June 18, 2026 16:07
브로커 생성 시 CODEX_COMPANION_SESSION_ID를 broker.json에 sessionId 필드로 기록.
resolveSessionId(options)는 options.sessionId → options.env → process.env → null 순서로 해소.
기존 세션 재사용 경로(early-return)는 변경 없음.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@hongsu hongsu requested a review from a team June 18, 2026 07:31

@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: 8aa56c3f48

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/codex/scripts/session-lifecycle-hook.mjs Outdated
Compare the hook entrypoint and argv path after resolving realpaths so Node's import.meta.url canonicalization does not bypass direct execution when CLAUDE_PLUGIN_ROOT is a symlink. Add a runtime regression test that invokes SessionStart through a symlinked plugin root and verifies the exported session environment.

Constraint: Node resolves imported module URLs to real paths while argv preserves the invoked symlink path.
Rejected: Raw URL pathname comparison | fails for symlinked plugin installations.
Confidence: high
Scope-risk: narrow
Directive: Keep hook direct-execution checks symlink-aware; SessionEnd cleanup depends on main() running from installed hook commands.
Tested: node --test tests/runtime.test.mjs tests/broker-lifecycle.test.mjs; npm test
Not-tested: npm run build fails before this change because generated app-server types do not match current source types.
@hongsu hongsu force-pushed the fix/broker-session-cleanup branch from 817197c to 82a372f Compare June 18, 2026 09:23

@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: 82a372fc1f

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/codex/scripts/lib/broker-lifecycle.mjs

@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: 55b5892f52

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +244 to +248
const remainingOwners = owners.filter((owner) => owner !== sessionId);
if (remainingOwners.length > 0) {
fs.writeFileSync(
stateFile,
`${JSON.stringify({ ...session, sessionId: remainingOwners[0], sessionIds: remainingOwners }, null, 2)}\n`,

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 Serialize shared broker owner updates

When two owners of the same broker exit at nearly the same time, both SessionEnd hooks can read the same sessionIds before either write happens. Each hook then enters this remaining-owner branch, writes a state file that keeps only the other session, and skips shutdown; whichever write wins leaves broker.json pointing at an already-ended owner, so no later hook will tear the broker down. This affects users with multiple Claude sessions sharing one broker who close them concurrently; the owner removal needs a lock or compare/reload before deciding not to shut down.

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.

SessionEnd cleanup fails when review cwd ≠ session cwd — broker.json looked up by cwd-hash, leaving orphan brokers even on graceful /quit

1 participant