Skip to content

fix(cli): honor --resume/--continue in oneshot (-z) — hydrate and chain the session - #57859

Closed
nathansmithopenclaw-alt wants to merge 1 commit into
NousResearch:mainfrom
nathansmithopenclaw-alt:fix/oneshot-resume-hydration
Closed

nathansmithopenclaw-alt wants to merge 1 commit into
NousResearch:mainfrom
nathansmithopenclaw-alt:fix/oneshot-resume-hydration

Conversation

@nathansmithopenclaw-alt

Copy link
Copy Markdown

What

hermes --resume <id> -z "<prompt>" (and --continue [name]) now actually chains: the session's prior transcript is loaded as conversation history and the turn is appended to the SAME session id. Ids that don't exist yet are created on first use, so scripted callers can mint a stable session key per conversation up front and pass it on every turn — no output parsing, no discovery step.

Why

The oneshot (-z) dispatch in hermes_cli/main.py called run_oneshot() and sys.exit()ed before the --resume/--continue → chat routing, and run_oneshot() never hydrated anything — the flag was accepted (exit 0, no warning) and silently dropped. Every -z turn was stateless and wrote a new throwaway session.

This is #49195. It bites any frontend driving oneshot with a stable per-conversation id — our concrete case is a gateway that runs a Hermes chat seat over -z one-shots; before this fix the agent was amnesiac on every turn.

Relationship to existing PRs

  • fix(oneshot): honor --resume/--continue so -z can be session-aware #40333 implemented essentially this hydration and was closed by its author without review — this PR carries that approach forward (same core mechanism, independently derived) and adds create-on-first-use ids plus best-effort degradation.

  • fix(cli): reject --resume/--continue in oneshot instead of dropping it #49204 takes the inverse, minimal path (reject the flags in oneshot) and explicitly defers "full resume hydration" to a later change, citing exact-output replay and compression-lineage concerns. This PR is that later change, and both concerns are addressed:

    • Compression lineage: the resume id is projected to the live tip via the existing SessionDB.resolve_resume_session_id before loading, exactly like interactive resume.
    • Replay duplication: history is seeded via run_conversation(conversation_history=...); _flush_messages_to_session_db skips seeded history dicts by identity, so resumed transcripts are never re-written to the store (same mechanism the gateway relies on).

    If maintainers prefer this hydration path, fix(cli): reject --resume/--continue in oneshot instead of dropping it #49204's guard becomes unnecessary; if you'd rather land the guard first, this rebases trivially on top of it (the guard's two dispatch sites are the ones wired here).

Change

  • hermes_cli/oneshot.pyrun_oneshot(resume=...)_run_agent(resume=...) → new _load_resume_history(session_db, resume): resolve the compression chain, load messages in conversation format (dropping session_meta rows), best-effort reopen_session, and pin AIAgent(session_id=...). Unknown ids return as-is with no history (create-on-first-use; the agent's existing create_session upsert makes the row on first persist). A broken session store degrades to a stateless turn — -z never gets less reliable than before.
  • hermes_cli/main.py — both oneshot dispatch sites pass resume=_resolve_oneshot_resume(args). --resume is passed through verbatim (no existence check — that's what enables caller-minted ids); --continue resolves by name / most-recent exactly like interactive chat, and an unresolvable --continue errors (nothing sensible to chain onto).
  • hermes_cli/_parser.py-z help documents the chaining semantics.
  • With no --resume/--continue, behavior is byte-for-byte unchanged (session id stays agent-generated).

Tests

  • New tests/hermes_cli/test_oneshot_resume.py (7 tests): history load + session_meta filtering + lineage resolution, create-on-first-use, broken-store degradation, and wiring assertions that AIAgent gets session_id and run_conversation gets conversation_history (and that the no-resume path is untouched).
  • tests/hermes_cli/test_tui_resume_flow.py — two kwarg-capture assertions extended with the new resume key.
  • tests/hermes_cli full run: failure set byte-identical to a clean-tree baseline run (pre-existing failures only; +7 passing).

Manual verification (live CLI):

SID="test_$(date +%Y%m%d_%H%M%S)_zeb"          # id Hermes has never seen
hermes --resume "$SID" -z "Remember the word ZEBRA. Reply with exactly OK."
# -> OK
hermes --resume "$SID" -z "What one word did I ask you to remember? Reply with only that word."
# -> ZEBRA

Closes #49195

🤖 Generated with Claude Code

hermes -z parsed --resume but silently dropped it: both dispatch sites
called run_oneshot() without the flag, and the agent was built with no
session id and no history — every one-shot turn was stateless.

Now:
- run_oneshot(resume=...) loads the session transcript as
  conversation_history (walking compression chains via
  resolve_resume_session_id, dropping session_meta rows) and pins the
  agent to the SAME session id, mirroring the interactive resume path.
- Create-on-first-use: an id Hermes hasn't seen is used as-is with no
  history, so scripted callers (gateways, cron workers) can mint stable
  session keys up front and pass them on every turn — no output parsing.
- --continue resolves by name / most-recent exactly like interactive
  chat (_resolve_oneshot_resume).
- Message flush dedup is inherited: history dicts seeded via
  conversation_history are skipped by identity, so resumed transcripts
  are never re-written to the store.
- Best-effort: a broken session store degrades to a stateless turn.

Verified live: ZEBRA planted and recalled across two -z invocations on a
caller-minted id. tests/hermes_cli/test_oneshot_resume.py (7 new);
tests/hermes_cli 7804 passed — failure set byte-identical to the
pre-change baseline (259 pre-existing, attributed by clean-tree diff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 3, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: fixes #49195. Carries forward the hydration approach of closed #40333 (independently derived) and is the deferred-hydration change #49204 explicitly punted on. Competing with open #49204, which takes the inverse minimal path (reject --resume/--continue in oneshot rather than honor them). Not a duplicate of either — flagging the cluster (#57859 hydrate vs #49204 reject vs closed #40333) so a maintainer can pick the direction.

@LavyaTandel

Copy link
Copy Markdown

Closing — created in error while comparing Option A/B for prompt-caching issue #57845. The actual Option A/B pair is #57876 + follow-up branch fix/prompt-caching-markers.

@teknium1 teknium1 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.

Thanks for carrying forward the session-hydration approach. The current-main premise is confirmed: both oneshot dispatches call run_oneshot() before resume routing and pass no resume value (hermes_cli/main.py:12670-12685, hermes_cli/main.py:14826-14843). The implementation direction matches the existing interactive lineage and history flow (hermes_cli/cli_agent_setup_mixin.py:272-292) and the flusher’s identity-based seeded-history deduplication (run_agent.py:1885-1911).

Problems

  • The new tests are mock-only (tests/hermes_cli/test_oneshot_resume.py:63-81), so they do not verify real SQLite create-on-first-use, a second-process reload, or no duplicate transcript rows. The contribution rubric requires an E2E path for session-resolution and file-I/O changes.

Suggested changes

  • Add a temp-HERMES_HOME integration regression covering two caller-minted resumed oneshot turns and durable transcript assertions.
  • Salvage onto current main while preserving usage_file, added by 7dfd5077ce to hermes_cli/oneshot.py:174 and both dispatch calls.

Automated hermes-sweeper review.

db.reopen_session.side_effect = RuntimeError("db locked")
sid, hist = _load_resume_history(db, "sid")
assert sid == "sid"
assert hist is None

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 harness mocks SessionDB and AIAgent, so it cannot prove create-on-first-use or a second independent -z --resume invocation reloads and appends the real SQLite transcript. Please add a temp-HERMES_HOME integration test for that two-invocation path.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 15, 2026
blairjordan added a commit to blairjordan/hermes-agent that referenced this pull request Aug 27, 2026
Rebases nathansmithopenclaw-alt's NousResearch#57859 onto current main (which had
since landed the MCP-discovery wait and requested_provider/usage_file
params in _run_agent — this carries both forward alongside the resume
hydration) and addresses the two points from @teknium1's review:

- The existing test_oneshot_resume.py coverage is entirely mock-based
  (SessionDB, AIAgent, and _load_resume_history's own dependencies are
  all MagicMocks) — real create-on-first-use behavior, a second-process
  reload, and duplicate-row risk on the resume-and-reflush cycle were
  never actually exercised against disk.
- Adds TestOneshotResumeIntegration: two _run_agent() calls against a
  real, temp-path-backed SessionDB (only AIAgent is replaced, with a fake
  that performs the SAME real create_session/append_messages_batch calls
  a genuine turn's flush does), then a THIRD read through a brand-new
  SessionDB instance — the same shape a second `hermes -z` process
  invocation would see. Asserts the full two-turn transcript loads in
  order with exactly 4 rows, not 8: turn 1's messages are seeded as
  conversation_history and never re-appended by the flush's identity
  dedup.

tests/hermes_cli/test_oneshot_resume.py: 9 passed (7 existing + 2 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nathansmithopenclaw-alt

Copy link
Copy Markdown
Author

Closing in favor of #80799, which carries this fix rebased onto current main with real-SQLite coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes -z (oneshot) silently ignores --resume / --continue (session never hydrated)

4 participants