Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/daemon/internal/agent/codex/mcp_cancellation.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,5 @@ func (s *Session) cleanupCancelledMCP(ctx context.Context) error {
if json.Unmarshal(raw, &receipt) != nil || !slices.Equal(receipt.ServerNames, labels) {
return errors.New("codex: native MCP invalidation unconfirmed")
}
_, err = request("config/mcpServer/reload", nil)
return err
return nil
}
10 changes: 3 additions & 7 deletions apps/daemon/internal/agent/codex/mcp_cancellation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ func TestMCPChildFirstObservedTerminalUsesNativeRootAttribution(t *testing.T) {
}
}

func TestExecutorCancelledMCPRequiresScopeCloseThenInvalidationAndReload(t *testing.T) {
for _, mode := range []string{"mcp-confirmed", "mcp-caller-cancelled", "mcp-stop-failed", "mcp-invalidate-unconfirmed", "mcp-reload-failed"} {
func TestExecutorCancelledMCPRequiresScopeCloseThenInvalidation(t *testing.T) {
for _, mode := range []string{"mcp-confirmed", "mcp-caller-cancelled", "mcp-stop-failed", "mcp-invalidate-unconfirmed"} {
t.Run(mode, func(t *testing.T) {
e, root := executorFixture(t, mode)
e.base.cfg.view.MCP = cancellationMCPBindings("active", "late", "healthy")
Expand Down Expand Up @@ -227,11 +227,7 @@ func TestExecutorCancelledMCPRequiresScopeCloseThenInvalidationAndReload(t *test
if mode == "mcp-stop-failed" {
wantInvalidate = 0
}
wantReload := 0
if confirmed || mode == "mcp-reload-failed" {
wantReload = 1
}
if counts["mcpServer/invalidate"] != wantInvalidate || counts["config/mcpServer/reload"] != wantReload || counts["turn/interrupt"] != 1 || counts["thread/start"] != 1 || counts["initialize"] != 1 || counts["mcpServerStatus/list"] != 0 || len(pids) != 1 {
if counts["mcpServer/invalidate"] != wantInvalidate || counts["config/mcpServer/reload"] != 0 || counts["turn/interrupt"] != 1 || counts["thread/start"] != 1 || counts["initialize"] != 1 || counts["mcpServerStatus/list"] != 0 || len(pids) != 1 {
t.Fatal(counts, pids)
}
})
Expand Down
5 changes: 0 additions & 5 deletions apps/daemon/internal/agent/codex/preparation_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,6 @@ func TestPreparationFakeCodexProcess(t *testing.T) {
if executorMode == "mcp-invalidate-unconfirmed" {
result = McpServerInvalidateResponse{}
}
case "config/mcpServer/reload":
if executorMode == "mcp-reload-failed" {
_ = output.Encode(map[string]any{"id": frame.ID, "error": map[string]any{"code": -32603, "message": "reload rejected"}})
continue
}
case "environment/status":
if os.Getenv("OAC_TEST_PREPARATION_BLOCK") == "1" {
for {
Expand Down
2 changes: 1 addition & 1 deletion packages/codex-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ Every captured thread's current configuration is checked before invalidation sta

Success returns `{ "serverNames": ["<binding-label>"] }`, sorted and deduplicated, after selected client shutdown completes. Ready clients have entered the native `ClientState::Closed`; pending startup is cancelled. Existing prepared calls retain their original client and cannot redirect to a replacement. This receipt acknowledges local invalidation, not remote tool completion or replacement readiness.

After the host `StopMCP` receipt and native invalidation receipt, the adapter calls maintained `config/mcpServer/reload`. Its dirty state applies through the existing native refresh flow before a subsequent Turn uses MCP. Reconciliation replaces closed clients and reuses unchanged healthy connections. No eager `mcpServerStatus/list` discovery, synthetic model Turn, polling delay or whole app-server restart establishes this fence.
Invalidation marks each captured Session’s MCP runtime dirty while holding its existing refresh semaphore. Before a subsequent Turn uses MCP, the maintained native refresh flow reconciles its owned configuration, replaces closed clients and reuses unchanged healthy connections. The cancellation path does not reload configuration files or eagerly discover replacements; no synthetic model Turn, polling delay or whole app-server restart establishes this fence.

The patch's `targeted_invalidation_*` Rust tests exercise closed-client replacement and unaffected connection identity through native reconciliation using controlled in-process transports. The Go adapter tests exercise cancellation latching, late starts, native call identity, host closure ordering, explicit declaration, receipt failures and Executor reuse. Native compilation and focused Rust tests must run in the qualified source build; source applicability alone does not qualify the binary.
3 changes: 2 additions & 1 deletion packages/codex-runtime/invalidate-mcp.patch
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs
index 46af2d3..9753960 100644
--- a/codex-rs/core/src/session/mcp.rs
+++ b/codex-rs/core/src/session/mcp.rs
@@ -89,6 +89,29 @@ impl ElicitationReviewer for GuardianMcpElicitationReviewer {
@@ -89,6 +89,30 @@ impl ElicitationReviewer for GuardianMcpElicitationReviewer {
}

impl Session {
Expand All @@ -388,6 +388,7 @@ index 46af2d3..9753960 100644
+ .mcp_runtime
+ .invalidate_servers(&names)
+ .await;
+ session.mark_mcp_runtime_dirty();
+ Ok::<_, anyhow::Error>(())
+ })
+ .await?
Expand Down
Loading