Skip to content

feat(v0.3.0): auto-snapshot, --resume, --clean-snapshots, pluto_health - #43

Merged
leondavi merged 5 commits into
masterfrom
v0.3.0
May 16, 2026
Merged

feat(v0.3.0): auto-snapshot, --resume, --clean-snapshots, pluto_health#43
leondavi merged 5 commits into
masterfrom
v0.3.0

Conversation

@leondavi

Copy link
Copy Markdown
Owner

Make recovery from a dead MCP transport one command. Both launchers now take a snapshot every 2h (configurable) and one final snapshot on graceful shutdown; --resume resolves /<agent_id>.plut so users no longer need to remember paths. New pluto_health MCP tool distinguishes adapter failures from server-unreachable cases.

  • src_py/utils/snapshot_helper.py: AutoSnapshotter, resolve_resume_path, clean_snapshots — shared by MCP friend and AgentFriend.
  • agent_mcp_friend: --resume / --snapshot-dir / --no-auto-snapshot / --auto-snapshot-interval / --clean-snapshots; auto-snapshot wired through FastMCP lifespan with final shutdown snapshot; pluto_health tool pings /health + reports last-snapshot metadata.
  • agent_friend: same flags; PlutoConnection.save_snapshot_files passthrough; AutoSnapshotter started after connect, stopped in finally.
  • launchers: forward new flags; --resume prints a role-reselection reminder (snapshots restore identity + locks but not the role prompt).
  • docs: refreshed pluto-mcp-friend.md and pluto-agent-friend.md with TL;DR recovery recipe, flag tables, and pluto_health entry.

leondavi and others added 5 commits May 16, 2026 14:55
Make recovery from a dead MCP transport one command. Both launchers now
take a snapshot every 2h (configurable) and one final snapshot on
graceful shutdown; --resume resolves <snapshot-dir>/<agent_id>.plut so
users no longer need to remember paths. New pluto_health MCP tool
distinguishes adapter failures from server-unreachable cases.

- src_py/utils/snapshot_helper.py: AutoSnapshotter, resolve_resume_path,
  clean_snapshots — shared by MCP friend and AgentFriend.
- agent_mcp_friend: --resume / --snapshot-dir / --no-auto-snapshot /
  --auto-snapshot-interval / --clean-snapshots; auto-snapshot wired
  through FastMCP lifespan with final shutdown snapshot; pluto_health
  tool pings /health + reports last-snapshot metadata.
- agent_friend: same flags; PlutoConnection.save_snapshot_files
  passthrough; AutoSnapshotter started after connect, stopped in finally.
- launchers: forward new flags; --resume prints a role-reselection
  reminder (snapshots restore identity + locks but not the role prompt).
- docs: refreshed pluto-mcp-friend.md and pluto-agent-friend.md with
  TL;DR recovery recipe, flag tables, and pluto_health entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A wiped/cleaned Pluto server has no memory of prior tokens, but agents
holding those tokens had no way to know until their next call returned
"session_not_found." This adds a boot-unique server_epoch (UUID minted
at app:start) that the client caches on register and compares against
/health on demand. A mismatch means the server was restarted and the
held token is dead — surfaced up-front via pluto_health so agents can
diagnose "session lost" without first failing a real call.

- pluto_app: generate server_epoch in persistent_term at boot; expose
  via server_epoch/0.
- pluto_http_listener: emit server_epoch on /health, /ping, and the
  three register response branches.
- PlutoHttpClient: cache resp["server_epoch"] on register; add
  fetch_server_epoch() helper.
- pluto_health tool: include live + cached epoch; set
  agent_registered=false and server_restarted=true with a recovery_hint
  when they differ.
- pluto_session: include cached server_epoch.
- docs: explain server_epoch semantics + the new health fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The polling loop now probes /health every ~30s and triggers a
re-register up front when the live server_epoch differs from the one
cached on register. Avoids the lag where the wrapper only notices a
server restart after the next message-bearing call returns 404.

Reuses the existing _reregister path used for session_not_found, so
lock semantics (lost_locks) and seq cursor reset are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlutoMCPFriend.sh prints a recovery note on launch and the docs explain
why the MCP friend can't self-heal (stdio child of Claude Code) versus
PlutoAgentFriend (owns its process, auto-reregisters on epoch change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HTTP listener forced Connection: close on every response and the
Python client opened a fresh socket per call via urllib.urlopen. Under
routine traffic (3 agents, 1s peek loop each, server-epoch probes), the
kernel filled with 16k TIME_WAIT entries on port 9202 in ~18 minutes,
blocking new connections with SYN_SENT timeouts.

Server side (pluto_http_listener.erl):
- handle_connection now loops via serve/2 with a 60s idle timeout
  between requests on the same socket.
- send_http_response/4 negotiates Connection: keep-alive vs close per
  RFC 7230 §6.3 (HTTP/1.1 default keep-alive; HTTP/1.0 default close).
- send_http_response/3 stays as a back-compat alias defaulting to
  close — used by do_long_poll's direct-write path.

Client side (pluto_client.py):
- New _HTTPConnectionPool: LifoQueue-backed bounded pool of
  http.client.HTTPConnection (size 4), context-managed acquire,
  per-call socket read timeout, stale-conn auto-retry once on
  RemoteDisconnected/ConnectionResetError/BrokenPipeError.
- PlutoHttpClient routes everything through _request, which sets
  Connection: keep-alive, drains the response body so the socket is
  reusable, and falls back to a fresh conn on stale-disconnect.
- long_poll passes its extended deadline via _request(timeout=...)
  instead of mutating self.timeout (race-prone under concurrent
  callers).
- unregister drains and closes the pool.

Verification: under a 500-call load against the rebuilt server,
netstat -an | grep 9202 | grep TIME_WAIT shows zero new entries —
the count is identical before and after. Pre-fix the same load would
have added ~500 TIME_WAITs.

tests/test_http_keepalive.py: new regression test covering socket
reuse under sequential load, concurrent long-poll vs short calls
sharing the pool, and the stale-connection retry branch. All three
pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@leondavi
leondavi merged commit 412bdaf into master May 16, 2026
2 checks passed
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