Skip to content

🐛 Destroy every stream an Agent holds when it closes - #751

Open
alecgibson wants to merge 1 commit into
mainfrom
destroy-every-agent-stream
Open

alecgibson wants to merge 1 commit into
mainfrom
destroy-every-agent-stream

Conversation

@alecgibson

@alecgibson alecgibson commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #740
Fixes #748

At the moment, Agent._cleanup() only destroys streams that are still in each subscription path's own map, so any bookkeeping slip in one of those paths leaves a stream subscribed after the client disconnects.

This change tracks every stream handed to the agent in one map, and destroys them all on cleanup. So that presence streams can be replaced like doc streams, the p: null "client left" broadcast moves off the presence stream's 'end' and into _unsubscribePresence() and _cleanup(). A broadcast still in middleware at that point goes out as p: null instead.

That also fixes presence outliving its client when it was never subscribed, or still in flight, which receivePresence middleware now sees as p: null:

  1. Client A submits presence on a channel it hasn't subscribed to
  2. Client B, subscribed to the channel, receives it
  3. Client A disconnects, and B keeps A's presence forever

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@coveralls

coveralls commented Oct 6, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 97.955% (+0.04%) from 97.916% — destroy-every-agent-stream into main

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.

🟡 Changes recommended

In-flight presence broadcasts can survive cleanup, and synthetic disconnect messages include an invalid sequence value.

2 open findings
What changed in this PR

Tracks Agent-owned streams for reliable cleanup and broadcasts null presence on unsubscribe or disconnect.

Changes:

  • Adds centralized stream tracking and cleanup.
  • Moves presence-disconnect broadcasts into explicit cleanup paths.
  • Adds tests and documents null presence behavior.
File Description
lib/​agent.js Implements stream tracking and presence cleanup.
test/​agent.js Tests cleanup of all stream types.
test/​client/​presence/​presence.js Tests null presence broadcasts.
docs/​api/​presence.md Documents unsubscribe behavior.
docs/​middleware/​actions.md Documents null middleware presence.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/agent.js Outdated
Comment thread lib/agent.js Outdated

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.

🟡 Changes recommended

Asynchronous ordering gaps can still publish non-null presence after unsubscribe or disconnect, and failed submissions retain empty channel maps.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread lib/agent.js Outdated
Comment thread docs/api/presence.md Outdated
Fixes #740
Fixes #748

At the moment, `Agent._cleanup()` only destroys streams that are still
in each subscription path's own map, so any bookkeeping slip in one of
those paths leaves a stream subscribed after the client disconnects.

This change tracks every stream handed to the agent in one map, and
destroys them all on cleanup. So that presence streams can be replaced
like doc streams, the `p: null` "client left" broadcast moves off the
presence stream's `'end'` and into `_unsubscribePresence()` and
`_cleanup()`. A broadcast still in middleware at that point goes out as
`p: null` instead.

That also fixes presence outliving its client when it was never
subscribed, or still in flight, which `receivePresence` middleware now
sees as `p: null`:

1. Client A submits presence on a channel it hasn't subscribed to
2. Client B, subscribed to the channel, receives it
3. Client A disconnects, and B keeps A's presence forever

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@alecgibson
alecgibson force-pushed the destroy-every-agent-stream branch from c5c9a12 to 7c1a081 Compare October 13, 2026 16:34
@alecgibson

Copy link
Copy Markdown
Collaborator Author

Migration note

Not breaking. This can ship as a 6.x 🐛 fix.

On main, receivePresence middleware already sees p: null leaves: when a client destroys its presence, and when a subscribed client unsubscribes or disconnects. This PR sends the same leave in two more cases:

  • Presence submitted without subscribing, when its client disconnects. Before, remote clients kept it forever.
  • Presence still in the doc-version lookup or middleware when its client unsubscribes or disconnects. It goes through receivePresence a second time, as the same context.presence object with p: null. Before, it was published after the leave and never cleared (A presence submitted just before disconnect can outlive its client #748).

On main, middleware that reads context.presence.p without a null check already throws when a client leaves, and it will now also throw in these two cases. Handle the leave explicitly:

backend.use('receivePresence', function(context, next) {
  if (context.presence.p === null) return next();
  // ...
});

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.

🟢 Approval recommended

The implementation addresses both reported lifecycle races with comprehensive regression coverage and consistent documentation.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

@alecgibson
alecgibson marked this pull request as ready for review October 13, 2026 16:46

This branch has not been deployed

No deployments
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.

A presence submitted just before disconnect can outlive its client Make Agent._cleanup() reach every stream the agent is given

3 participants