Skip to content

fix: refresh tool cache on MCP notifications/tools/list_changed - #10459

Closed
arohou wants to merge 9 commits into
aaif-goose:mainfrom
arohou:fix/tool-list-changed-refresh
Closed

arohou wants to merge 9 commits into
aaif-goose:mainfrom
arohou:fix/tool-list-changed-refresh

Conversation

@arohou

@arohou arohou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

My first contribution to goose — deliberately kept small (2 source + 2 test files,
one concern) and linked to #10433, per CONTRIBUTING's "start small" guidance. I'm very open to feedback & guidance!

Goose fetches each MCP extension's tools/list once at init and caches it for the
session. rmcp's ClientHandler::on_tool_list_changed defaults to a no-op and
GooseClient never overrode it, so notifications/tools/list_changed was silently
dropped, and the ExtensionManager tool cache (only invalidated on
add/remove-extension) went stale. Tools added or removed mid-session never appeared
until the app was restarted.

This wires the dropped notification into the cache-invalidation path that
ExtensionManager already has:

  • Add a small ToolCacheInvalidator trait in mcp_client.rs
    (async fn invalidate_tools).
  • GooseClient holds an Option<Weak<dyn ToolCacheInvalidator>> and overrides
    on_tool_list_changed to upgrade the Weak and call it. Weak, not Arc — a
    client must never keep its ExtensionManager (and thus the child MCP process)
    alive past the session. This is the retain-cycle P1 that the automated review of
    the earlier attempt (feat: implement dynamic tool discovery via MCP notifications/tools/li… #8276) flagged.
  • impl ToolCacheInvalidator for ExtensionManager delegates to the existing
    invalidate_tools_cache_and_bump_version(); the next agent turn re-fetches via
    get_prefixed_tools().
  • The invalidator is threaded through every client-construction path
    (stdio/child-process, streamable-http, OAuth connect_with_auth, unix-socket,
    builtin in-process). The in-process platform-extension factory does not take
    it
    — it is not an MCP transport and cannot emit tools/list_changed; it already
    gets a Weak extension-manager back-reference via context.

Scope: 2 source files + 2 new test files, one concern. No new dependencies, no
Cargo.lock change. Revives and hardens #8276 (auto-closed as stale, never
human-reviewed); credit to the original author below.

Testing

New end-to-end reproducer that drives a real MCP stdio server through goose's
own extension machinery and rmcp transport (no mocks, no internal-API pokes) and
observes the public tool list (get_prefixed_tools, i.e. what the agent sees):

crates/goose/tests/tool_list_changed_test.rs +
crates/goose/tests/dynamic_tools_server.py (stdlib-only; advertises
tools.listChanged=true, serves alpha, and on the first alpha call emits
notifications/tools/list_changed then starts serving beta).

With the fix (this branch) it passes:

cargo test -p goose --test tool_list_changed_test
test tool_list_changed_refreshes_public_tool_list ... ok
test result: ok. 1 passed; 0 failed; ... finished in 0.14s

To see it fail without the fix (i.e. confirm the test actually gates on the
change): the two test files exist only in this commit, so don't check out main
instead restore just the two source files to origin/main, keeping the test,
then run and restore:

git checkout origin/main -- crates/goose/src/agents/mcp_client.rs \
                            crates/goose/src/agents/extension_manager.rs
cargo test -p goose --test tool_list_changed_test          # FAILS (see below)
git checkout HEAD -- crates/goose/src/agents/mcp_client.rs \
                     crates/goose/src/agents/extension_manager.rs   # restore the fix
test tool_list_changed_refreshes_public_tool_list ... FAILED
dynamic__beta never appeared after notifications/tools/list_changed — the
notification was dropped (bug present)
test result: FAILED. 0 passed; 1 failed; ... finished in 10.20s

(git revert HEAD would remove the test too — the targeted source-file checkout is
what isolates "test present, fix absent".)

Unit tests added alongside:

  • agents::mcp_client::tests::on_tool_list_changed_invalidates_then_noops_after_drop
    — a live invalidator triggers one invalidation; after the owning manager is
    dropped, the Weak fails to upgrade and the handler is a safe no-op (guards the
    P1).
  • agents::extension_manager::tests::test_tools_cache_invalidated_on_tool_list_changed
    — a client whose tool set grows; the cache hides the new tool until
    invalidate_tools is called, then surfaces it.

Manual server-behavior check (no goose needed) — tools/list goes
[alpha][alpha, beta] with a notifications/tools/list_changed between:

cd crates/goose/tests
printf '%s\n' \
  '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}' \
  '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"alpha","arguments":{}}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' | python3 dynamic_tools_server.py

Full local verification (hermit toolchain, rustc 1.96.1):

  • cargo check -p goose --tests — clean
  • cargo test -p goose --lib tools_cache / ... on_tool_list_changed — pass
  • cargo clippy -p goose --all-targets -- -D warnings — clean
  • cargo fmt — no changes outside the touched files

Related Issues

Fixes #10433
Prior art / supersedes: #8276 (auto-closed as stale, never human-reviewed)

Screenshots/Demos (for UX changes)

N/A — behavior fix, no UI change. See the before/after test output above.


Co-authored-by: makanilani 43491852+makanilani@users.noreply.github.com

@arohou

arohou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up on the Build and Test Rust Project failure — sorry about that! I've root-caused it and pushed a follow-up commit.

It's my fix working against a real server, not a regression. The failing test is the pre-existing mcp_integration_test replay for @modelcontextprotocol/server-everything (untouched by this PR, green on main). That server advertises tools.listChanged: true and emits notifications/tools/list_changed at startup. On main, goose dropped it, so the recorded transcript had a single tools/list. With this fix, goose now honors the notification and re-fetches, so the request sequence gains one tools/list before the next tool call — which is exactly the behavior this PR adds. So the "failure" is a nice real-world confirmation the fix fires end-to-end.

Fixture update (200dd04). I updated that replay fixture to expect the extra tools/list (insert the re-fetch, shift the following request ids/progressTokens by one). .results.json is unchanged since the tool-call results don't change.

For transparency: this is a minimal hand-edit, not a GOOSE_RECORD_MCP re-record.

Re-recording against the currently-published server-everything pulls in a lot of unrelated drift: a much larger tool set, a new roots/list exchange (which also embeds an absolute local path from the recording machine), and npm-notice noise. Rather than commit that, I applied the minimal edit that a clean re-record against the pinned server would have produced — just the one added re-fetch — and confirmed the fixture plays back cleanly across 20 repeated local runs. If you'd rather regenerate it in your own environment/canonical way, I'm happy to defer to that; just let me know.

Happy to adjust anything here.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 200dd04c14

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

STDIN: {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"_meta":{"agent-session-id":"test-session-id","agent-tool-call-request-id":"test-id","progressToken":4},"name":"get-structured-content","arguments":{"location":"New York"}}}
STDOUT: {"result":{"content":[{"type":"text","text":"{\"temperature\":33,\"conditions\":\"Cloudy\",\"humidity\":82}"}],"structuredContent":{"temperature":33,"conditions":"Cloudy","humidity":82}},"jsonrpc":"2.0","id":5}
STDIN: {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"_meta":{"agent-session-id":"test-session-id","agent-tool-call-request-id":"test-id","progressToken":5},"name":"trigger-sampling-request","arguments":{"maxTokens":100,"prompt":"Please provide a quote from The Great Gatsby"}}}
STDIN: {"jsonrpc":"2.0","id":3,"method":"tools/list","params":{"_meta":{"agent-session-id":"test-session-id","progressToken":2}}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stabilize the replay around startup list_changed notifications

This replay now assumes the startup notifications/tools/list_changed messages on lines 5-6 are handled after the first tool list has been cached, so it expects this extra tools/list before get-sum. Those notifications are emitted before the first tools/list, though; if Tokio schedules the rmcp notification handler before get_all_tools_cached records/caches the initial list, the second dispatch reuses the cache and sends tools/call with id 3 instead. Since playback waits for an exact next STDIN match (crates/goose-test/src/mcp/stdio/playback.rs:72-80), this makes the replay timing-dependent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed, this is timing-dependent. But goose ends up with the correct tool set under either schedule; the two only differ in the wire sequence (whether the post-list_changed re-fetch lands before get-sum or is folded into the initial fetch). So the fix is correct either way — it's the exact-match replay that's sensitive, and it would be for any server that emits tools/list_changed at startup.

Rather than rely on the observed ordering (consistent across CI-Linux and ~20 local runs, but not guaranteed), we could make the replay itself order-tolerant: treat a tools/list as a "floating" request in playback.rs (replay its recorded response wherever goose issues it, and don't fail if it's absent). That would remove the timing-dependency generically.

I could work on this here in this PR, but I would have a (slight?) concern with scope creep. This is my first PR and I'm aiming to keep it very focused.

Could someone provide guidance on whether I should try to implement this in this PR? I'm open to doing it here if the team thinks it makes sense.

@arohou

arohou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

I had my agent investigate why CI is still failing, and it seems it's due to a change committed to main earlier today. Here's my agents' summary:

The two Build and Test TLS Backend failures are the pre-existing breakage from #10148 — main is red on the same job; unrelated to this PR.

I guess I can wait until #10148 is fixed

EDIT - this was fixed in main, CI pipeline now passes; just awaiting review now.

@arohou
arohou force-pushed the fix/tool-list-changed-refresh branch from 200dd04 to d88d8ea Compare July 14, 2026 20:01
@arohou

arohou commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Hope it's OK to tag @DOsinga - I'm a first time contributor just hoping this PR doesn't get lost in the sea of PRs coming your way. I'm tagging you because it seems you do a lot of the MCP-related merging. Would appreciate any feedback on this first PR. Thanks in advance!

@aeoess

aeoess commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tested commit d88d8ea with a separate local Node stdio fixture driven through Goose's ExtensionManager (add_extension, get_prefixed_tools, and dispatch_tool_call), without an LLM key.

The PR's end-to-end test passed 1/1, the cache-related unit tests passed 4/4, and a separate 7-case rig passed:

  • adding and removing tools during a session
  • replacing a same-name tool's schema
  • 10 notifications in one burst
  • a notification during an in-flight call
  • a notification from a server that did not advertise listChanged
  • refreshing a list from 150 to 175 tools

I also checked the counterfactual. Restoring mcp_client.rs and extension_manager.rs to the merge base while keeping the PR's end-to-end test reproduced the original failure: dynamic__beta never appeared because the notification was dropped. Restoring the fix made the test pass again.

One behavior worth recording: invalidation is manager-wide, so the next lookup re-lists every extension. The handler also honors the notification when the server did not advertise listChanged. Neither caused a correctness failure in this run, but together they allow a server to trigger a global re-list.

I found no blocker in the paths I exercised. Scope: I did not run the full Goose suite, and I did not exercise the mcp_replays fixture change.

I can share the fixture and 7-case rig if useful.

Alexis Rohou and others added 2 commits July 21, 2026 17:46
Goose fetches each MCP extension's tools/list once at init and caches it
for the session. rmcp's ClientHandler::on_tool_list_changed defaults to a
no-op and GooseClient never overrode it, so notifications/tools/list_changed
was silently dropped, and the ExtensionManager tool cache (only invalidated
on add/remove-extension) went stale. Tools added or removed mid-session
never appeared until the app was restarted. Real-world trigger: an stdio
proxy that serves an "offline" tool set while its upstream is unreachable,
then sends tools/list_changed on recovery -- goose keeps showing the offline
tools and the agent wrongly concludes the backend is down.

Add a small ToolCacheInvalidator trait in mcp_client.rs. GooseClient holds
an Option<Weak<dyn ToolCacheInvalidator>> and overrides on_tool_list_changed
to upgrade the Weak and drop the cache via the existing
invalidate_tools_cache_and_bump_version(); the next agent turn re-fetches
tools/list. The back-reference is Weak, not Arc, so a client never keeps its
ExtensionManager (and thus the child MCP process) alive past the session --
this resolves the retain-cycle P1 that the review of the earlier attempt
(aaif-goose#8276) flagged. The invalidator is threaded through every client
construction path (stdio/child-process, streamable-http, OAuth, unix-socket,
builtin in-process). The in-process platform-extension factory does not take
it: it is not an MCP transport and cannot emit tools/list_changed.

Revives and hardens aaif-goose#8276 (auto-closed as stale, never human-reviewed).

Tests:
- crates/goose/tests/tool_list_changed_test.rs: end-to-end reproducer that
  drives a real MCP stdio server (tests/dynamic_tools_server.py) through the
  extension machinery and rmcp transport, observing the public tool list.
  Fails on unpatched main (notification dropped, times out waiting for the
  new tool), passes with the fix.
- unit tests for the Weak drop no-op (the P1) and cache hiding/invalidation.

Refs aaif-goose#8276

Co-authored-by: makanilani <43491852+makanilani@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mcp_integration_test replay for @modelcontextprotocol/server-everything
records that server sending notifications/tools/list_changed at startup. With
the tool-cache-refresh fix, goose now honors that notification and re-fetches
tools/list, so the recorded request sequence gains one tools/list call before
the next tool call. Update the fixture to expect it: insert the re-fetch and
shift the following request ids / progressTokens by one.

This is a minimal hand-edit rather than a GOOSE_RECORD_MCP re-record on purpose:
re-recording against the currently-published server pulls in unrelated drift
(a larger tool set, a new roots/list exchange, npm notices). The edit is
semantically identical to what a clean re-record against the pinned server
would produce, and playback is deterministic across repeated runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arohou
arohou force-pushed the fix/tool-list-changed-refresh branch from d88d8ea to 05cec9e Compare July 21, 2026 17:05

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05cec9e06e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/mcp_client.rs
@arohou

arohou commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @aeoess for your review! I appreciate you exercising paths well beyond the PR's own test, plus confirming the counterfactual.

On the two behaviors you flagged:

  • Manager-wide invalidation (re-list on any notification). That's deliberate — it reuses the existing invalidate_tools_cache_and_bump_version() path, which already invalidates the whole-manager tool cache (the same path add/remove-extension uses). A full re-list is cheap relative to the correctness guarantee, and staying on the existing mechanism kept the change minimal. Per-extension invalidation would be a reasonable follow-up optimization if it ever matters.
  • Honoring a notification when the server didn't advertise listChanged. rmcp delivers on_tool_list_changed regardless of the advertised capability, and honoring it is harmless in the worst case (a redundant re-list) while being robust to servers that under-declare. Happy to add a capability guard (ignore unless tools.listChanged was advertised) if maintainers prefer the stricter behavior — it's a small change.

Neither is a correctness issue (as you found), but good to have on record — thanks for calling them out.

Note: I've since rebased onto current main (now 05cec9e). The code is identical to the d88d8ea you tested — the source files were untouched upstream, so the patches are byte-for-byte the same, just replayed on top of latest main.

Thanks again for the careful review.

Invalidating the ExtensionManager cache makes the *next* reply see updated
tools, but a reply already in flight keeps passing its up-front `tools`
snapshot to the provider on subsequent iterations. So a tool call that unlocks
new tools mid-reply wasn't offered to the model until a later reply.

Poll the (existing) `tools_cache_version` at each reply-loop iteration and, when
it changes, re-run `prepare_tools_and_prompt` via the existing `tools_updated`
refresh path, so the updated tool set reaches the model within the same reply.

- extension_manager: expose `tools_cache_version()` (already bumped on every
  cache invalidation).
- agent reply loop: snapshot the version next to `tools`; flag a refresh when it
  moves.
- test: `tool_list_changed_reply_loop_test` drives a real reply loop with a stdio
  server that emits `tools/list_changed` mid-reply and asserts the new tool is
  offered within the same reply. Fails without the loop change, passes with it.

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc6df5ca77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/agent.rs Outdated
`prepare_tools_and_prompt` rebuilds `system_prompt` without the session's
project addendum, so every reply-loop refresh path — runtime extension
enable, directory hints, and the tools/list_changed version check added
earlier in this PR — dropped project instructions for the rest of the reply.
The initial prompt appended the addendum, but the next provider turn after a
refresh ran without it.

Re-append the project addendum after each refresh via a shared
`with_project_addendum` helper (also used for the initial prompt).

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aeoess

aeoess commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Reviewed cc6df5c. The change looks good. It uses the existing tools_cache_version and refresh path to pick up tools/list_changed during an active reply, so newly available tools reach the model on the next loop iteration.

The new test covers the case we were concerned about: beta becomes available mid-reply and is included without waiting for a second reply.

I reviewed the diff against the earlier issue but did not rerun the suite. I don't see any blockers.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d9d9f7f9f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/agent.rs Outdated
The tools/list_changed version poll ran at the end of each reply-loop
iteration, so a notification whose handler landed after that poll but before
the next provider call was only noticed after a stale model turn — if that
turn returned a final answer, a newly published tool was never offered within
the reply. Move the version recheck to immediately before the provider call
(top of the loop), which also covers iterations reached via an earlier
`continue` that bypass the end-of-loop refresh block.

This is best-effort by nature: rmcp dispatches the notification handler on a
detached task with no completion signal, so a change whose handler has not run
yet is caught on a later iteration. The cache invalidation guarantees the
notification is never lost — only, in a narrow race, deferred by a turn.

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffa95f9810

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/agent.rs Outdated
`invalidate_tools_cache_and_bump_version` bumped `tools_cache_version` before
clearing `tools_cache`, while `get_all_tools_cached` early-returns on a cached
`Some` without a version check. On a multi-threaded runtime the reply loop's
version poll could observe the new version, lock the cache before the clear,
take the stale early-return, advance its local version snapshot, and then never
re-list for the rest of the reply.

Clear the cache first, then bump the version. Via the cache-mutex unlock
(release) sequenced-before the SeqCst bump, which synchronizes-with the poller's
SeqCst version load, an observer of the new version is guaranteed to see the
cleared cache and fall through to a refetch. The `version_after == version_before
&& cache.is_none()` write-back guard is order-independent and unchanged.

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aacd89c09

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/agent.rs Outdated
The reply loop kept a tool-cache version baseline separately from its `tools`
vector and read the two at different points, so they could drift: a
`notifications/tools/list_changed` handled while `prepare_reply_context` was
still building `tools` was recorded as "already consumed" by a later baseline
read, and the top-of-loop check then never re-listed for the rest of the reply.

Make the version travel with the tools instead of being read separately:
- `tools_cache` now stores `(version, tools)`, and `get_all_tools_cached`
  returns the version the returned tools were built from (the stored version on
  a cache hit, or `version_before` on a refetch).
- surface it via `get_prefixed_tools_with_version` / `list_tools_with_version`
  (existing `get_prefixed_tools` / `list_tools` stay as version-dropping
  wrappers, so other callers are untouched), and add it to the tuple returned by
  `prepare_tools_and_prompt` and to `ReplyContext`.
- the reply loop takes its baseline from the version returned alongside the
  tools at every build/rebuild site, and no longer reads the counter separately.

Now baseline == version-of-tools by construction, so the baseline can never
drift ahead of the tools it is paired with. The only residual is the inherent
async-delivery race (a notification whose handler has not run yet), which is
caught on a later turn and never loses a tool.

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84da77bc51

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/agents/extension_manager.rs Outdated
Alexis Rohou and others added 2 commits July 26, 2026 09:32
The previous invalidation cleared the cache and released the lock, then bumped
the version in a separate step. A get_all_tools_cached write-back finishing a
fetch could acquire the cache lock in that gap, observe the not-yet-bumped
version (so its `version_after == version_before` guard passed) and an empty
cache, and repopulate `Some((old_version, stale_tools))`. The bump then left a
stale entry tagged with the old version that the version poll can't evict, so
refreshes kept returning the stale tools until the next invalidation.

Hold the cache lock across both the version bump and the clear, so a write-back
runs either fully before invalidation (its entry is then cleared) or fully
after (its guard fails and it doesn't cache). This preserves the property that
an observer of the new version sees the cleared cache.

Addresses review feedback on the tools/list_changed PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arohou

arohou commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@alexhancock @DOsinga @jamadeo — small first contribution (fixes #10433): wires MCP tools/list_changed into the ExtensionManager tool cache you all maintain. 2 source + 2 test files, one concern. All codex review comments are resolved, and at least one user has reviewed & tested (see comments from @aeoess). Would any of you have a few minutes for a human look? Happy to adjust anything.

@DOsinga

DOsinga commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Thank you for the contribution and for continuing to work through the review findings. The evolution of this PR has been useful in showing that the problem is broader than simply wiring on_tool_list_changed to cache invalidation: it now touches cache-generation consistency, in-flight fetches, the active reply's local tool snapshot, prompt rebuilding, detached notification scheduling, and replay determinism.

Given everything uncovered so far, we don't think we have a sufficiently clear design boundary yet. We have moved #10433 to Accepted / design so we can first agree on the intended semantics and a design that handles these interactions together, rather than continuing to add point fixes here. In particular, we need to decide whether a changed tool must be available during the next provider iteration of the same reply or only on the next user turn; if same-reply visibility is required, we need an explicit update/coordination mechanism rather than relying on loosely timed version polling around rmcp's detached notification handler.

We are not closing this PR yet. Please join the design discussion on #10433; your findings and experience implementing this version are valuable input. We'll pause and revisit this PR in four days to give everyone time to respond.

DOsinga commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Thank you, Alexis. Your careful review, independent tracing of the next-turn behavior, and the strict replay-fixture update in #10928 were key to getting this over the line. We merged that fixture into #10925, the full CI suite passed, and the fix has now landed. This was very much something we landed together. I’m closing this PR as superseded by #10925, with real appreciation for the work and collaboration here.

@DOsinga DOsinga closed this Aug 4, 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.

MCP notifications/tools/list_changed is dropped — a server's tool list never refreshes mid-session

3 participants