Skip to content

fix(app-server): unsubscribe task threads after client disconnect - #707

Open
thossullivan wants to merge 2 commits into
openai:mainfrom
thossullivan:fix/unsubscribe-completed-task-threads
Open

fix(app-server): unsubscribe task threads after client disconnect#707
thossullivan wants to merge 2 commits into
openai:mainfrom
thossullivan:fix/unsubscribe-completed-task-threads

Conversation

@thossullivan

@thossullivan thossullivan commented Sep 1, 2026

Copy link
Copy Markdown

Summary

The app-server broker multiplexes plugin tasks through one shared upstream connection. Starting or resuming a thread subscribes that connection to thread events.

Before this change, closing a downstream task socket removed request and stream routing state but retained the thread subscription. Repeated tasks could therefore accumulate thread runtimes and MCP process trees inside one broker session.

This change tracks thread ownership for every downstream socket. The broker sends thread/unsubscribe only after the final downstream owner releases a thread.

Implementation

The broker maintains ownership in both directions. Downstream sockets reference thread IDs, and thread IDs reference their downstream owners. This structure preserves a shared subscription while another client still owns the thread.

Ownership covers started, resumed, forked, detached review, subagent, and automatically created child threads. Direct request results establish root ownership. Subagent notifications use parentThreadId or collaboration sender and receiver IDs to inherit ownership from the causal parent. They do not use whichever downstream client happens to be active when a delayed notification arrives.

Requests from each downstream socket run in order. This prevents overlapping resume or review requests from releasing another request's provisional claim. A response received after its downstream socket closes also triggers immediate cleanup.

Normal socket closure and abrupt disconnection use the same final-owner cleanup path. Automatic unsubscribe failures are logged and retried with bounded backoff. Explicit downstream unsubscribe requests continue to preserve subscriptions owned by other clients.

Scope

This change does not alter MCP configuration or depend on Basic Memory. Basic Memory only provided visible reproduction evidence for the host-level lifecycle defect.

Codex retains unsubscribed threads during its documented 30-minute inactivity period. This change starts that bounded cleanup period but does not promise immediate MCP process termination.

Testing

The integration suite covers normal completion, shared resumed threads, detached reviews, forks, active-turn disconnection, subagents, late child threads, collaboration-only child discovery, explicit unsubscribe, and upstream failures.

The adversarial regression cases also cover delayed child notifications during an unrelated task, overlapping same-socket resume requests, transient unsubscribe retries, and test resource cleanup.

  • node --test tests/broker-subscriptions.test.mjs: 14 passed
  • npm test: 105 passed
  • npx tsc -p tsconfig.app-server.json --noEmit: passed

Real-process verification used Codex CLI 0.150.1 and Basic Memory.

Case Probe after client closure Meaning Natural MCP exit
Control broker unsubscribed The broker retained the subscription until the probe removed it Approximately 30 minutes after the probe
Patched broker notSubscribed The broker had already released the subscription Approximately 30 minutes after client closure

Both cases started a real Basic Memory wrapper and Python server. Every isolated test descendant exited during cleanup.

After the adversarial hardening, patched UAT covered both downstream closure modes.

Patched case Probe after client closure Natural MCP exit
Graceful socket closure notSubscribed 1,804,992 ms after closure
Abrupt socket disconnection notSubscribed 1,809,728 ms after disconnection

Both brokers and app-server processes remained alive through natural MCP unload. Final cleanup left zero isolated descendants.

Related work

This change complements #543, #660, and #680. Those items address broker and app-server lifetime, while this change releases completed task threads inside a running broker.

The change does not depend on either lifecycle pull request.

Fixes #706

@thossullivan
thossullivan marked this pull request as ready for review September 1, 2026 19:01
@thossullivan
thossullivan requested a review from a team September 1, 2026 19:01
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T19:05:37.543970Z 1203f5d Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sylvesterkaczmarek sylvesterkaczmarek 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.

One race still looks possible here. addThreadOwner() cancels a retry timer, but it doesn't do anything about a thread/unsubscribe already in pendingUnsubscribes. If the last owner disconnects and that request is in flight while another client resumes the same thread, a late unsubscribe can leave the shared app-server connection unsubscribed even though threadSockets has a new owner.

I don't see a test for reacquiring a thread during an in-flight unsubscribe. Could we re-check ownership when the unsubscribe settles (and restore the subscription if needed), or otherwise serialize that transition?

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.

App-server broker retains thread subscriptions after task clients disconnect

2 participants