Skip to content

Add session shutdown method across all SDKs#682

Draft
stephentoub wants to merge 2 commits intomainfrom
stoub/shutdown
Draft

Add session shutdown method across all SDKs#682
stephentoub wants to merge 2 commits intomainfrom
stoub/shutdown

Conversation

@stephentoub
Copy link
Collaborator

@stephentoub stephentoub commented Mar 5, 2026

Problem

When a session is destroyed, the CLI sends a session.shutdown notification after responding to the session.destroy RPC request. However, all four SDKs clear every event handler immediately upon receiving the session.destroy response. By the time the session.shutdown notification arrives, there are no handlers left to receive it. The event is silently dropped.

This means session.shutdown — which is defined in the generated types and documented as part of the session lifecycle — is effectively unreceivable by any SDK consumer today.

Alternatives considered

Remove handler clearing from destroy entirely. After destroy, the CLI sends no further events (shutdown is the last one), so clearing is not strictly necessary. However, all four SDKs document that destroy() / DisposeAsync() clears handlers, and in .NET, DisposeAsync carries the expectation that all work has quiesced upon return. Removing the clearing would break that contract as the handler for shutdown could run at any point after DisposeAsync. DisposeAsync could stall waiting for it, but then that's introducing potentially long waits into an operation that's supposed to be relatively quick.

Add a timeout-based wait inside destroy. We could wait a short window (e.g. 500ms) for the shutdown event before clearing handlers. This is fragile — the delay is arbitrary, adds latency to every destroy call, and still provides no guarantee the event arrives in time.

Solution

Introduce a new shutdown method in each SDK:

Language Method
Node.js session.shutdown()
Python await session.shutdown()
Go session.Shutdown(ctx)
.NET await session.ShutdownAsync()

shutdown() sends the session.destroy RPC to the CLI but does not clear event handlers. This gives the caller an explicit window to observe the session.shutdown notification before they choose to finalize cleanup.

destroy() / DisposeAsync() now calls shutdown() internally, then clears handlers as before. Callers who never call shutdown() directly see no behavior change — full backward compatibility is preserved.

Both methods are idempotent: calling shutdown() twice is a no-op on the second call, and destroy() safely handles the case where shutdown() was already called.

Typical usage pattern

// Register handler before shutdown
session.on('session.shutdown', (event) => {
  console.log('Session shut down:', event);
});

// Send the destroy RPC, keeping handlers alive
await session.shutdown();

// ... wait for / process the shutdown event ...

// Clean up handlers and finalize
await session.destroy();

What's included

  • 4 SDK source files: New shutdown method + refactored destroy in Node.js, Python, Go, and .NET
  • 4 E2E test files: Each SDK's session event test now calls shutdown, asserts the session.shutdown event is received, then calls destroy
  • 5 doc files: API reference updates in all language READMEs, docs/compatibility.md, and the docs-maintenance agent

Add a new shutdown() method (ShutdownAsync in .NET, Shutdown in Go) that
sends the session.destroy RPC to the CLI without clearing event handlers.
This allows callers to observe the session.shutdown notification that the
CLI sends after responding to the destroy request.

The existing destroy() / DisposeAsync() method now calls shutdown()
internally before clearing handlers, preserving full backward
compatibility.

Updated E2E tests in all four SDKs to exercise the new method and assert
that the session.shutdown event is received. Updated API reference docs
in all language READMEs, docs/compatibility.md, and the docs-maintenance
agent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 5, 2026 03:53
@stephentoub stephentoub requested a review from a team as a code owner March 5, 2026 03:53
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 a shutdown() method (in language-appropriate naming) to all four SDKs — Node.js, Python, Go, and .NET — that sends the session.destroy JSON-RPC request to the server without clearing local event handlers. This enables callers to observe the server-sent session.shutdown notification before finalizing cleanup. destroy()/DisposeAsync() now delegates to shutdown() first, maintaining full backward compatibility for existing callers.

Changes:

  • New shutdown() / Shutdown() / ShutdownAsync() method in each SDK with idempotency guards.
  • Refactored destroy() to call shutdown() internally then clear handlers as before.
  • E2E tests updated to call shutdown(), await the session.shutdown event, then call destroy(). Documentation updated in all language READMEs, docs/compatibility.md, and the docs-maintenance agent.

Reviewed changes

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

Show a summary per file
File Description
nodejs/src/session.ts Adds shutdown() with _isShutdown bool guard; destroy() delegates to it
nodejs/test/e2e/session.test.ts E2E test awaits session.shutdown event before calling destroy()
python/copilot/session.py Adds shutdown() with _is_shutdown bool flag; destroy() delegates to it
python/e2e/test_session.py E2E test awaits shutdown_event before calling destroy()
go/session.go Adds Shutdown() using atomic.Bool; Destroy() delegates to it (error message changed)
go/internal/e2e/session_test.go E2E test awaits session.shutdown event via channel before calling Destroy()
dotnet/src/Session.cs Adds ShutdownAsync() with Interlocked.Exchange guard; DisposeAsync() delegates to it
dotnet/test/SessionTests.cs E2E test awaits shutdownReceived task before calling DisposeAsync()
nodejs/README.md Documents new shutdown() method and updated destroy() description
go/README.md Documents new Shutdown() method
dotnet/README.md Documents new ShutdownAsync() method and updated DisposeAsync() description
docs/compatibility.md Adds "Shutdown session" entry to the compatibility table
.github/agents/docs-maintenance.agent.md Updates all four SDK method lists to include the new shutdown method

@stephentoub stephentoub marked this pull request as draft March 5, 2026 04:12
@stephentoub
Copy link
Collaborator Author

Actually, I'm pretty sure the CLI isn't ever actually sending the shutdown event as part of session destroy. In which case the changes here are necessary but insufficient.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations, and I'm pleased to report excellent consistency throughout. The shutdown() method has been implemented with strong feature parity across all languages.

Summary of Changes

This PR adds a new shutdown() method to all four SDKs (Node.js, Python, Go, and .NET) to address the race condition where the session.shutdown notification is silently dropped because handlers are cleared before it arrives.

Consistency Analysis

Method Naming - Perfectly Consistent

All SDKs follow proper language conventions:

  • Node.js: session.shutdown() (camelCase)
  • Python: session.shutdown() (snake_case)
  • Go: session.Shutdown() (PascalCase for exported method)
  • .NET: session.ShutdownAsync() (PascalCase + Async suffix)

Core Behavior - Identical Across All SDKs

  1. Idempotency: All implementations use proper guards to make shutdown() a no-op on repeated calls:

    • Node.js: _isShutdown boolean flag
    • Python: _is_shutdown boolean flag
    • Go: isShutdown atomic.Bool
    • .NET: _isShutdown with Interlocked.Exchange
  2. RPC Call: All send the same session.destroy RPC request without clearing handlers

  3. Integration with destroy(): All SDKs now call shutdown() first in their destroy()/DisposeAsync() methods, maintaining backward compatibility

Documentation - Consistent Structure

All four implementations include:

  • Clear XML/docstring comments explaining the purpose
  • Usage examples showing the pattern: register handler → shutdown → wait → destroy
  • Warnings about the relationship with destroy()/DisposeAsync()
  • Error/exception documentation

README Updates - Complete Coverage

  • Node.js README: Added shutdown() method documentation ✅
  • Python README: No detailed method reference section (existing pattern) ⚠️
  • Go README: Added Shutdown() method documentation ✅
  • .NET README: Added ShutdownAsync() method documentation ✅

Note: Python README follows a different documentation pattern (minimal API reference) compared to other SDKs. This is pre-existing and not introduced by this PR.

E2E Tests - Parallel Implementation

All four E2E test suites now include the same pattern:

// Call shutdown explicitly before destroy
await session.shutdown();  // or Shutdown() or ShutdownAsync()
await session.destroy();    // or Destroy() or DisposeAsync()

Compatibility Documentation - Updated

The docs/compatibility.md table now includes the new shutdown method.

Docs-Maintenance Agent - Updated

The validation checklist in .github/agents/docs-maintenance.agent.md has been updated to include shutdown() in all four SDK method lists.

Recommendations

No changes needed - This PR demonstrates exemplary cross-SDK consistency. The implementation:

  • Maintains feature parity across all languages
  • Respects language-specific idioms and conventions
  • Provides consistent documentation and examples
  • Includes comprehensive test coverage
  • Updates all relevant documentation

The only minor observation is that Python's README doesn't have the same detailed method reference as the other SDKs, but this is a pre-existing pattern difference, not something introduced by this PR.

Great work maintaining consistency across this multi-language SDK! 🎉

Generated by SDK Consistency Review Agent for issue #682 ·

The CLI runtime does not currently emit session.shutdown notifications
during the session.destroy flow, so remove E2E assertions that wait for
the event. The shutdown() method and two-phase API remain in place for
when the runtime is updated.

Update the Node.js failed-cleanup test to reflect that shutdown()'s
idempotency guard causes the destroy retry to succeed with local-only
cleanup when the server process is dead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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