Skip to content

fix(sdk): resume a conversation whose custom tool module is gone - #5771

Open
enyst wants to merge 2 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-custom-tool-resume
Open

enyst wants to merge 2 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-custom-tool-resume

Conversation

@enyst

@enyst enyst commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes a fix for a bug uncovered during the first full agentic run of verify-agent-server.


AGENT:

Why

A persisted conversation became unreadable after restart if the module defining one of its custom tools was no longer registered.

Summary

 resume persisted conversation
   event with unknown kind → skip model event; follow its stored parent
   prompt with unknown tool → omit that tool definition
   missing tool registration → warn; initialize available tools
   readable history remains traversable

 fresh conversation with an unknown tool → existing strict failure

Tolerate unavailable custom-tool models only when recovering persisted history. Both full and incremental history traversal continue past an unknown event, and agent initialization uses the readable prefix to recognize a resumed conversation.

Source: event_store.py · base.py · system.py.

Issue Number

Fixes #4080

How to Test

Previously recorded verification
  • uv run pytest tests/sdk/conversation/test_resume_without_custom_tool.py: 5 passed (3 fail on main: the two branch-walk tests and the end-to-end resume; the corrupt-JSON and fresh-conversation tests pin unchanged behaviour). The end-to-end test persists a conversation with a registered custom tool, unregisters it and renames the stored kinds, resumes with agent=None as the server does, and sends a message.
  • uv run pytest -n 8 tests/sdk/agent tests/sdk/conversation tests/sdk/event tests/sdk/tool: all pass except 2 ACP timing tests that pass when re-run alone (flaky under parallel load).
  • Map proof with AGENT_SERVER_VERIFY_HOME set to a private directory:
    • control-agent-server map run --file F27 --only F27.custom-tool-missing-on-resume --fresh --repeat 2 --checkout <main worktree at dfd8fe8>: xfail twice.
    • The same command from this branch: xpass twice (message POST 200, events/search 200, server log shows Skipping unreadable event, Leaving out tool of unknown kind 'QaF27EchoTool' and Tool 'qa_f27_echo' is not registered; resuming conversation ...).
    • After dropping the marker: control-agent-server map check ok, and map run --file F27 --only F27.custom-tool-missing-on-resume --fresh: pass.
  • uv run pre-commit run --files <changed files>: all hooks pass.

Video/Screenshots

n/a

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

Skipped events are logged; this adds no public opaque-event placeholder or UI count. Malformed JSON and non-object records still fail. The alternative of requiring every historical tool to be re-registered remains stricter; broader tool-set compatibility is tracked in #2667, with verification under #5653.

Visual explanation by Codex using HumanLayer show-me.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:02d0a23-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-02d0a23-python \
  ghcr.io/openhands/agent-server:02d0a23-python

All tags pushed for this build

ghcr.io/openhands/agent-server:02d0a23-golang-amd64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-golang-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-golang-amd64
ghcr.io/openhands/agent-server:02d0a23-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:02d0a23-golang-arm64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-golang-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-golang-arm64
ghcr.io/openhands/agent-server:02d0a23-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:02d0a23-java-amd64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-java-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-java-amd64
ghcr.io/openhands/agent-server:02d0a23-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:02d0a23-java-arm64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-java-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-java-arm64
ghcr.io/openhands/agent-server:02d0a23-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:02d0a23-python-amd64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-amd64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-amd64
ghcr.io/openhands/agent-server:02d0a23-python-arm64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-arm64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-arm64
ghcr.io/openhands/agent-server:02d0a23-python-minimal-amd64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-minimal-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-minimal-amd64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:02d0a23-python-minimal-arm64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-minimal-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-minimal-arm64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:02d0a23-python-slim-amd64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-slim-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-slim-amd64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:02d0a23-python-slim-arm64
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-slim-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-slim-arm64
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:02d0a23-golang
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-golang
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-golang
ghcr.io/openhands/agent-server:02d0a23-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:02d0a23-java
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-java
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-java
ghcr.io/openhands/agent-server:02d0a23-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:02d0a23-python-minimal
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-minimal
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-minimal
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-minimal
ghcr.io/openhands/agent-server:02d0a23-python-slim
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python-slim
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python-slim
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime-slim
ghcr.io/openhands/agent-server:02d0a23-python
ghcr.io/openhands/agent-server:02d0a23d467ea62c708abdb0bfb08a3d40896fff-python
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-custom-tool-resume-python
ghcr.io/openhands/agent-server:02d0a23-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 02d0a23-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 02d0a23-python-amd64) are also available if needed

After a restart without the module that defines a custom tool, the
conversation's stored events name classes that are no longer loaded, and
one unknown kind failed the whole load (500 on every message and event
search on the agent server).

- EventLog.path_to_root (and the incremental view walk) skip an event
  whose JSON no longer validates, logging it and following its stored
  parent_id; unparseable JSON still raises.
- SystemPromptEvent leaves out stored tools of an unknown kind.
- On resume (a system prompt already stored), the agent skips tool specs
  that are not registered, with a warning; a fresh conversation still
  raises.
- Drop the known-bug marker from F27.custom-tool-missing-on-resume.

Fixes #4080

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/agent
   agent.py4903693%112, 327, 483, 487, 790–791, 798–799, 922, 926–927, 932–934, 936, 955–956, 962, 967, 1002–1003, 1010–1011, 1056, 1063, 1077–1078, 1110–1111, 1393, 1478–1479, 1481, 1511, 1560, 1567
   base.py3944090%125–126, 277, 352, 358, 560, 659, 669, 720, 722–723, 827–828, 838–839, 864–870, 879, 883, 886, 961, 964, 976–978, 983, 985, 988, 995, 999, 1008, 1022, 1025, 1041, 1087
openhands-sdk/openhands/sdk/conversation
   event_store.py2342291%132, 198, 201, 214, 218–219, 275, 279, 292–294, 305–306, 312, 314–317, 338, 340, 388, 402
   state.py280598%372–373, 382–384
openhands-sdk/openhands/sdk/event/llm_convertible
   system.py661085%55, 97–98, 118–119, 124–127, 130
TOTAL503141341273% 

@enyst
enyst marked this pull request as ready for review October 13, 2026 22:30
@enyst
enyst requested a review from all-hands-bot October 13, 2026 22:31

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This review was posted by an AI agent (OpenHands).

I re-fetched the current head (4a364c72), the PR body, the discussion, the linked issues, and the head's check runs. The head still matches, there are no existing reviews, and every check run on this head passes. I ran the new focused test file (tests/sdk/conversation/test_resume_without_custom_tool.py, 5 passed) and the surrounding suites (tests/sdk/conversation, tests/sdk/event, tests/sdk/agent, tests/sdk/tool — all pass).

The core change is sound. EventLog._read_branch_node/path_to_root and the incremental walk in ConversationState.view now skip an event whose JSON parses but whose model no longer validates; SystemPromptEvent._drop_unknown_tools tolerates a vanished tool class; and resume drops an unregistered tool with a warning. Unparseable JSON still raises, and a fresh conversation still raises KeyError, both preserved as intended.

One remaining load path is still strict, so the linked issue's degrade-per-event goal is not fully met for legacy conversations:

ConversationState._resolve_active_leaf still reads the leaf event strictly (openhands-sdk/openhands/sdk/conversation/state.py:291, not a changed line). When a conversation has no persisted leaf_event_id (pre-tree/legacy logs — leaf_event_id was added in #3922, while custom tool modules shipped earlier in #1383), the active leaf is resolved by scanning backwards with self._events[i]. If the last real event is one whose kind is no longer loaded (exactly the custom-tool case this PR targets), that read raises ValidationError and the whole conversation still fails to load — the same symptom as #4080. path_to_root was made tolerant, but this pre-pass runs before it and was not, so the tolerant walk is never reached.

Reproduction on the current head (persist a pre-tree log by appending events directly to EventLog with leaf_event_id=None, then rename the last event's kind to an unknown one and resume with agent=None):

ConversationState.create -> state.rebuild_view() -> _resolve_active_leaf()
  -> EventLog.__getitem__ -> Event.model_validate_json
pydantic_core.ValidationError: Value error, Unknown kind 'GoneEvent' for openhands.sdk.event.base.Event

The post-tree case (persisted leaf_event_id) is handled correctly: _resolve_active_leaf returns the stored id without reading, and the tolerant path_to_root skips the bad event. Only the legacy shape still strands the conversation.

This is narrower than the primary scenario, but it is a supported path (the tree feature explicitly keeps legacy history loadable) and it reproduces the linked issue's "entire conversation fails to load" outcome. Suggested scope: resolve the legacy leaf tolerantly as well (e.g. skip an unreadable tail event the way _read_branch_node does, or have the reverse scan use the tolerant read), or state explicitly that pre-tree conversations are out of scope for #4080.

🔄 CHANGES REQUESTED

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

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.

One unregistered event kind fails the entire conversation load (should degrade per-event)

3 participants