feat(agent): add live upgrade — checkpoint subagents before restart and adopt orphans on startup (#71023) - #71027
Conversation
Related: #71027 combines the live-upgrade checkpoint proposal for #71023 with the same vision capability guard as #71024. The process-checkpoint work in #69918 is adjacent but covers detached processes rather than in-process subagents; please choose the intended scope and split/consolidate as appropriate. |
7d9d7df to
ba078d8
Compare
…nd adopt orphans on startup (NousResearch#71023)
ba078d8 to
266e2b4
Compare
…eation on startup - Save full conversation messages (tool calls + results) from each subagent's _session_messages into the checkpoint (version 2) - Add _safe_serialize_messages() helper for safe JSON serialization - Store orphan data in _pending_orphans module-level variable - Add get_pending_orphans() to access stored orphan records - Add recreate_pending_subagents(parent_agent) to auto-re-delegate orphans with saved goal + context + previous conversation history - Integrate auto-recreation in cli.py first-turn agent init - Update tests to cover version 2 checkpoint, pending orphans, safe serialization, and auto-recreation
|
Rebased onto latest main, split the unrelated vision guard into #72917, and upgraded the checkpoint to preserve full subagent state (goal, context, conversation history) with auto-recreation via |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for splitting the vision change and extending the checkpoint data. The restart-recovery gap is real on current main, but this implementation needs lifecycle and delivery rework before it is safe.
Problems
tools/delegate_tool.py:4244callsdelegate_taskwithoutbackground=True. Its default is synchronous (tools/delegate_tool.py:2823,3390-3391), unlike the normal top-level dispatch path (run_agent.py:6900-6919), so the first recovered user turn waits for the recreated work.cli.py:1180-1187checkpoints every_run_cleanup; normal interactive exit reaches cleanup atcli.py:17318. Startup then adopts (cli.py:13994-14000) and first chat recreates (cli.py:12778-12790) against whichever new session starts next. This can replay work after a normal exit into an unrelated session.- The checkpoint is removed before recreation (
tools/delegate_tool.py:4134-4138), while failures are caught after pending state is cleared (4172-4175,4259-4266), so failed recovery is not retryable.
Suggested changes
- Gate recovery on a verified update handoff plus originating-session ownership.
- Use the existing asynchronous delegation delivery path and add a real lifecycle test for non-blocking recovery and failed-dispatch retention.
Automated hermes-sweeper review.
| # can be surfaced by adopt_orphaned_subagents() on next startup. | ||
| try: | ||
| from tools.delegate_tool import checkpoint_active_subagents | ||
| checkpoint_active_subagents() |
There was a problem hiding this comment.
_run_cleanup() also runs for ordinary CLI exit (cli.py:17318 on current main), not only /update. With startup adoption and first-chat recreation below, this checkpoints and replays work into the next unrelated CLI session. Please gate this on a verified update/restart handoff and preserve originating-session ownership.
| # Store the full orphan data for later recreation | ||
| _pending_orphans = list(subagents) | ||
|
|
||
| # Remove the checkpoint file — data is now in memory |
There was a problem hiding this comment.
This deletes the only durable recovery record before any recreation attempt. recreate_pending_subagents() clears pending state before dispatching and catches failures, so a failed dispatch cannot be retried after another restart. Retain or atomically update the record until each dispatch is accepted.
| # Call delegate_task to recreate the subagent | ||
| from tools.delegate_tool import delegate_task as _dt | ||
|
|
||
| _dt( |
There was a problem hiding this comment.
This omits background=True. Current delegate_task resolves an omitted background value to false and waits synchronously, whereas normal top-level model delegation explicitly dispatches in the background. Recovery will block the first user turn; route it through the existing async ownership/delivery path.
| prev_work_lines.append(f"[tool_call]: {tname}") | ||
|
|
||
| if enriched_context: | ||
| enriched_context += "\n\n" + "\n".join(prev_work_lines) |
There was a problem hiding this comment.
The saved transcript becomes context, and _build_child_system_prompt() inserts context verbatim into the new child's system prompt. That promotes previous tool output and assistant text to privileged instructions. Use a bounded, structured continuation representation that preserves the original trust boundary.
SummaryOne PR addresses Issue #71023. #71027 adds pre-restart checkpointing and startup re-delegation with saved goals, context, and conversation history, directly targeting lost subagent work, but the diff does not preserve live execution or safely recover results across restart. Related pull requests
Suggested consolidationkeep open with a salvage path: retain #71027 as the best available direction, but do not merge it until the blocking contributor review is addressed. Gate checkpointing and replay on a verified update/restart handoff with originating-session ownership, dispatch recovered work asynchronously through the existing delivery path, retain or atomically advance durable recovery state until dispatch succeeds, and represent saved progress with a bounded structured continuation that preserves the original trust boundary. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I71023(["issue #71023 (open)"])
P71027["PR #71027 (open)"]
P71027 -->|best fix| I71023
class I71023 open
class P71027 open
class P71027 best
class P71027 target
click I71023 "https://github.com/NousResearch/hermes-agent/issues/71023"
click P71027 "https://github.com/NousResearch/hermes-agent/pull/71027"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 37 kB of PR diffs, 6 kB of issue/PR text, 3 kB of discussion (7 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
What
Implements live upgrade support for Hermes Agent: when the process restarts (e.g. via
/update), running subagents are checkpointed with full state before shutdown and automatically recreated on startup, preserving their progress.Part 1 — Rich checkpoint before restart (version 2)
checkpoint_active_subagents()indelegate_tool.pynow serialises the full state of running subagents to$HERMES_HOME/state/subagent_checkpoints.json. Each record contains:subagent_id,parent_id,depth,goal,model,started_at,tool_count,status,last_toolsaved_messages: The full conversation history (tool calls + results) extracted from the child agent's_session_messagesand safely serialized (non-serializable fields stripped, long content truncated)resolved_context: The subagent's context from_subagent_goalCheckpoint format is now version 2 (detectable by the
"version": 2field). Version 1 checkpoints (metadata only) from older code are still accepted gracefully.Part 2 — Orphan adoption with progress preservation
adopt_orphaned_subagents()consumes the checkpoint on startup and stores the orphan data in a module-level variable_pending_orphans. Instead of just logging lost work and deleting the checkpoint:get_pending_orphans()provides access to the stored dataPart 3 — Auto-recreation with progress continuity
New
recreate_pending_subagents(parent_agent)function:adopt_orphaned_subagents()delegate_taskwith the savedgoalandcontextsaved_messagesare available (version 2+ checkpoint), formats the previous conversation as enriched context so the new subagent knows what was already done and picks up where it left offcli.py's first-turn agent initializationHooks
cli.py_run_cleanup()— callscheckpoint_active_subagents()before resource teardowncli.pyrun()— callsadopt_orphaned_subagents()after the welcome bannercli.pyfirst-turn init — callsrecreate_pending_subagents()when the agent is first initialized, auto-re-delegating orphansChanges separated
The unrelated "vision capability guard" change has been split into a separate PR: #26 (fix/vision-capability-guard-71027)
Testing
24 tests in
tests/tools/test_delegate_checkpoint.pycovering:_safe_serialize_messagesedge cases (bytes, callbacks, truncation)$HERMES_HOMEget_pending_orphans()returns correct datarecreate_pending_subagents()re-delegates with goalrecreate_pending_subagents()builds enriched context from messages_remove_checkpoint_safenever raisesCloses
Closes #71023
Breaking Changes
None. The
adopt_orphaned_subagents()return signature (int) is preserved. New functions (get_pending_orphans(),recreate_pending_subagents()) are additive.