Skip to content

fix(cli): run /new session-boundary memory extraction off the command path (#16454 salvage) - #61139

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/16454-new-session-flush-offthread
Jul 8, 2026
Merged

fix(cli): run /new session-boundary memory extraction off the command path (#16454 salvage)#61139
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/16454-new-session-flush-offthread

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

/new no longer blocks for seconds on end-of-session memory extraction — the LLM-bound provider flush is queued on the memory manager's serialized background worker, with the end→switch ordering providers depend on preserved and exit-time durability guaranteed.

Salvages #16454 by @Tosko4 (cherry-picked, authorship preserved) onto current main, with a review-driven redesign of the offload mechanism on top.

Supersedes #16454 — can be closed in favor of this PR.

Root cause & why not a bare thread

The original PR's ad-hoc daemon thread raced new_session()'s inline on_session_switch(reset=True): MemoryManager.on_session_end takes no session id, providers key off internal _session_id state, and the switch (instant) beats the extraction (LLM-bound, seconds) essentially every time. A late on_session_end then ran against post-rotation bindings — old transcript misattributed to the NEW session id, double-ingest of the old turn buffer (supermemory), or double-commit (openviking already async-finalizes old sessions inside on_session_switch). Caught in deep review (3-angle, all converged on this).

Changes

  • run_agent.py/cli.py (contributor, cherry-picked): move /new's blocking commit_memory_session off the command path; commit_memory_session accepts an explicit session_id.
  • agent/memory_manager.py (follow-up): new commit_session_boundary_async() — queues on_session_end + on_session_switch(reset=True) as ONE task on the manager's existing single-worker executor (the same worker sync_all uses). Strict end→switch ordering, FIFO serialization against per-turn syncs, non-blocking caller, inline degradation when the executor is unavailable. No ad-hoc threads, no per-provider changes.
  • cli.py (follow-up): new_session() snapshots history and routes the boundary through commit_session_boundary_async (inline switch preserved for the no-history case); the context-engine on_session_end half stays synchronous (cheap, must land before reset_session_state() rebinds the engine); _run_cleanup drains pending boundary work via the manager's existing flush_pending(timeout=10) barrier so "/new then quit" can't drop extraction (shutdown_all's own drain is ~5s and cancels queued tasks; bounded well inside the 30s exit watchdog).

Validation

Check Result
Targeted tests (boundary commit, new_session, commit_memory_session, provider ABC, session switch, boundary hooks, shutdown, finalize) 153 passed, 0 failed
E2E (real MemoryManager + real executor + slow LLM-like provider): end(old-sid) → switch(new-sid) → next-turn sync, caller blocked 0ms; flush_pending drains queued boundary on exit path pass
ruff on touched files clean
ty (head vs merge-base, cli.py + run_agent.py + memory_manager.py) 503 == 503, 0 net-new

New regression tests: ordering invariant under slow extraction, FIFO vs sync_all, switch-fires-even-if-end-raises, no-provider no-op, CLI snapshot handoff + inline-switch fallback, synchronous engine boundary, cleanup flush_pending.

Credit: @Tosko4 for the diagnosis and the off-thread design direction (#16454).

Phase-2 review gates (follow-up commit 3)

Ran the structured hermes-pr-review Phase 2 (2a/2b/2c) + hermes-agent-dev gates against the branch; findings folded in as refactor: address Phase-2 review findings:

  • Snapshot handoff converted from instance attribute to return value (eliminates a leak/stale-mis-fire class when no memory manager is configured or an exception lands mid-new_session).
  • Dropped the production-dead session_id kwarg from commit_memory_session (no caller passed it after the redesign; speculative params rejected per AGENTS.md) and a dead local in commit_session_boundary_async.
  • Gate results: 2a tests/cli/ 1048 passed + 6 memory suites 136 passed, 0 failed; 2b programmatic live smoke — caller returns in <1ms, end→switch→sync ordering verified against a real executor; 2c 4-angle review — 0 Criticals (compressor durable-cooldown round-trip and end→switch vs DB-flush ordering both verified safe against source), warnings fixed. Known scoping note: TUI/gateway finalize paths still run extraction inline — pre-existing behavior, out of scope for this fix(cli) PR.

Tosko4 and others added 2 commits July 9, 2026 01:55
Deep review of the cherry-picked NousResearch#16454 found the ad-hoc flush thread
raced new_session()'s inline on_session_switch(reset=True): memory
providers key off internal _session_id state (MemoryManager.on_session_end
takes no session id), so a late off-thread extraction ran against
post-rotation bindings — misattributing the old transcript to the new
session id, double-ingesting the old turn buffer (supermemory), or
double-committing (openviking already async-finalizes in
on_session_switch).

Redesign: new MemoryManager.commit_session_boundary_async queues
on_session_end + on_session_switch as ONE task on the manager's existing
single-worker background executor (the same worker sync_all already
uses). This preserves the strict end→switch ordering providers depend on,
serializes against per-turn syncs FIFO, keeps /new non-blocking, and
degrades to inline (pre-NousResearch#16454 behavior) when the executor is
unavailable. No ad-hoc threads; no per-provider changes needed.

The context-engine on_session_end half stays synchronous in
_launch_session_boundary_memory_flush (cheap, must land before
reset_session_state rebinds the engine).

Exit durability: _run_cleanup calls the manager's existing
flush_pending(timeout=10) barrier before shutdown, so '/new then quit'
doesn't drop the queued extraction (shutdown_all's own drain is ~5s and
cancels queued tasks). Bounded well inside the 30s exit watchdog.

Tests: ordering invariant with slow (LLM-like) extraction, FIFO
serialization vs sync_all, switch-fires-even-if-end-raises, no-provider
no-op, CLI snapshot handoff + inline-switch fallback, sync engine
boundary, cleanup flush_pending.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard tool/memory Memory tool and memory providers sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P2 Medium — degraded but workaround exists labels Jul 8, 2026
- Return the boundary snapshot from
  _launch_session_boundary_memory_flush as a local value instead of
  staging it on self._session_boundary_snapshot. The instance-attr
  handoff could leak (no memory manager configured) or mis-fire a
  stale snapshot on a later /new if an exception hit between staging
  and consumption. A local variable eliminates the class; the helper
  also returns None when no memory manager is configured so
  new_session takes the inline-switch path.
- Drop the now-dead session_id kwarg from commit_memory_session:
  after the redesign no production caller passes it (gateway, TUI,
  compression all use the default), and speculative params are
  rejected per AGENTS.md. The explicit-old-session need is served by
  cli.py's direct engine call + commit_session_boundary_async.
- Drop the dead providers snapshot in commit_session_boundary_async
  (only the emptiness check used it).
- Tests updated accordingly (dead-kwarg test removed, snapshot
  assertion now covered by return-value contract).

Phase-2 gates: 2a tests/cli 1048 passed + 6 memory files 137 passed;
2b programmatic live smoke 0.38ms non-blocking caller, end→switch→sync
ordering verified; 2c structured 4-angle review — no Criticals, these
warnings fixed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/memory Memory tool and memory providers type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants