Skip to content

Conversation

@hendem
Copy link

@hendem hendem commented Jan 6, 2026

Summary

This PR adds cleanup mechanisms to prevent memory leaks in several areas of the codebase:

  • ACPSessionManager: Add remove(), size(), and clear() methods to properly clean up session state when sessions end
  • Rpc: Add timeout mechanism (30s default) for pending RPC calls to prevent orphaned requests from accumulating
  • FileTime: Add clearSession() to clean up per-session file read timestamps when sessions end
  • MCP OAuth: Add TTL (10 minutes) for pending OAuth transports to clean up abandoned authentication flows

Changes

File Change
src/acp/session.ts Add remove(), size(), clear() methods
src/util/rpc.ts Add timeout, dispose(), pendingCount() methods
src/file/time.ts Add clearSession(), sessionCount() methods
src/mcp/index.ts Add TTL-based cleanup for pendingOAuthTransports
test/acp/session.test.ts Tests for session cleanup
test/util/rpc.test.ts Tests for RPC timeout and cleanup
test/file/time.test.ts Tests for FileTime session cleanup

Testing

  • 12 new tests added covering all cleanup mechanisms
  • All tests pass
  • Typecheck passes
  • Build passes

Related Issues

Note

This PR is complementary to #7032 which addresses Bus subscription cleanup. Together they should significantly reduce memory accumulation during long-running sessions.

… and OAuth

- ACPSessionManager: add remove(), size(), and clear() methods for session cleanup
- Rpc: add timeout mechanism (30s default) for pending requests to prevent orphaned calls
- Rpc: add dispose() and pendingCount() methods for cleanup and monitoring
- FileTime: add clearSession() and sessionCount() for per-session read time cleanup
- MCP: add TTL (10 minutes) for pending OAuth transports to clean up abandoned flows

Related to anomalyco#4315, anomalyco#5363
Copilot AI review requested due to automatic review settings January 6, 2026 05:06
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

The following comment was made by an LLM, it may be inaccurate:

Duplicate PR Search Results

✓ Found 1 Related PR:

PR #7032: fix(core): add dispose functions to prevent subscription memory leaks

Summary

No duplicates found. PR #7039 is a new PR addressing memory leaks through cleanup mechanisms in ACPSessionManager, RPC, FileTime, and MCP OAuth. The only related open PR is #7032, which the PR description already acknowledges as complementary work - they target different areas of memory accumulation and are meant to work together.

The PR is ready to proceed and does not appear to be a duplicate of any existing work.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds cleanup mechanisms to prevent memory leaks across multiple components of the OpenCode system. The changes introduce TTL-based cleanup for OAuth flows, timeout handling for RPC calls, and management methods for session state cleanup.

Key changes:

  • Added timeout mechanism (30s default) for RPC calls to prevent orphaned requests
  • Implemented TTL-based cleanup (10 minutes) for pending OAuth transports
  • Added session management methods (remove(), size(), clear()) to ACPSessionManager and FileTime

Reviewed changes

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

Show a summary per file
File Description
src/util/rpc.ts Adds timeout mechanism, dispose(), and pendingCount() methods to prevent pending request accumulation
src/mcp/index.ts Adds TTL-based cleanup for pendingOAuthTransports to handle abandoned authentication flows
src/file/time.ts Adds clearSession() and sessionCount() methods for per-session read timestamp cleanup
src/acp/session.ts Adds remove(), size(), and clear() methods for session state management
test/util/rpc.test.ts Comprehensive tests for RPC timeout, disposal, and pending request tracking
test/file/time.test.ts Tests for FileTime session cleanup functionality
test/acp/session.test.ts Tests for ACPSessionManager cleanup methods

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

Comment on lines +52 to +58
const timeoutHandle = setTimeout(() => {
const request = pending.get(requestId)
if (request) {
pending.delete(requestId)
reject(new Error(`RPC call '${String(method)}' timed out after ${timeout}ms`))
}
}, timeout)
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition between the timeout callback (lines 52-58) and the response handler (lines 36-46). If a response arrives at exactly the same time as the timeout fires, both could execute simultaneously. While the current implementation uses Map.get() and Map.delete() which should handle this, consider checking if the request still exists after clearTimeout to make the race handling more explicit.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +129
/**
* Remove a session from the manager to free memory.
* Should be called when a session is terminated or the connection closes.
*/
remove(sessionId: string): boolean {
const existed = this.sessions.has(sessionId)
if (existed) {
this.sessions.delete(sessionId)
log.info("removed_session", { sessionId })
}
return existed
}

/**
* Get the count of active sessions (useful for monitoring/debugging).
*/
size(): number {
return this.sessions.size
}

/**
* Clear all sessions. Used during cleanup/dispose.
*/
clear(): void {
const count = this.sessions.size
this.sessions.clear()
log.info("cleared_all_sessions", { count })
}
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cleanup methods (remove, size, clear) are not being called anywhere in the production code. While they provide the infrastructure for memory management, the PR doesn't include integration points where these methods would actually be invoked (e.g., when an ACP connection closes or a session is terminated). Consider adding calls to sessionManager.remove() in the appropriate lifecycle hooks, such as the cancel method or connection cleanup handlers.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +46
/**
* Clear all read timestamps for a session to free memory.
* Should be called when a session ends.
*/
export function clearSession(sessionID: string): boolean {
const s = state()
if (sessionID in s.read) {
delete s.read[sessionID]
log.info("cleared session read times", { sessionID })
return true
}
return false
}
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FileTime.clearSession() method is not being called anywhere in the production code. While the method provides the infrastructure for memory management, there are no integration points where it would actually be invoked when a session ends. Consider adding calls to FileTime.clearSession() in session cleanup handlers or when sessions are removed.

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +164
/** Clean up expired OAuth transports */
function cleanupExpiredOAuthTransports() {
const now = Date.now()
for (const [key, entry] of pendingOAuthTransports) {
if (now - entry.createdAt > OAUTH_TRANSPORT_TTL) {
log.info("cleaning up expired oauth transport", { key, age: now - entry.createdAt })
pendingOAuthTransports.delete(key)
}
}
}
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout cleanup only runs when a timeout occurs, but expired transports can accumulate if OAuth flows are abandoned before timeout. Consider implementing periodic cleanup or cleanup on every insertion to handle abandoned flows more proactively. For example, running cleanup on a timer or on every state initialization would prevent accumulation of entries that never timeout.

Copilot uses AI. Check for mistakes.
Comment on lines 820 to +822
try {
// Call finishAuth on the transport
await transport.finishAuth(authorizationCode)
await entry.transport.finishAuth(authorizationCode)
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After calling finishAuth on the transport (line 822), the entry is deleted from pendingOAuthTransports at line 840 before the subsequent add() operation. If the add() call or any other operation fails after line 840, the transport is already removed and cannot be retried. Consider moving the deletion to after the entire operation succeeds to allow retry on partial failures.

Copilot uses AI. Check for mistakes.
},
/** Clear all pending requests (for cleanup) */
dispose(): void {
for (const [requestId, request] of pending) {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable requestId.

Suggested change
for (const [requestId, request] of pending) {
for (const request of pending.values()) {

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,89 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports afterEach, beforeEach.

Suggested change
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
import { describe, expect, test } from "bun:test"

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
const sdk2 = {
session: { create: async () => ({ data: { id: "session-2" } }) },
} as any
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable sdk2.

Suggested change
const sdk2 = {
session: { create: async () => ({ data: { id: "session-2" } }) },
} as any

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,126 @@
import { describe, expect, test, beforeEach, mock } from "bun:test"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports beforeEach, mock.

Suggested change
import { describe, expect, test, beforeEach, mock } from "bun:test"
import { describe, expect, test } from "bun:test"

Copilot uses AI. Check for mistakes.
@hendem
Copy link
Author

hendem commented Jan 6, 2026

Closing - FileTime cleanup is already addressed by #6682 which also wires up the call from Session.remove(). ACP agent cleanup is in #7043.

@hendem hendem closed this Jan 6, 2026
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.

1 participant