Commit Graph

6 Commits

Author SHA1 Message Date
Brooklyn Nicholson
ff27cc345f fix(cli): shut down the active agent's memory provider before lease release when cli.agent was retired 2026-09-30 14:23:52 -05:00
Zheqing Zeng
82c76be695 fix: run memory-provider session finalization inside ownership too
Review feedback (Enough1122): _run_cleanup -> _shutdown_agent_memory_provider
-> MemoryManager.on_session_end is session-keyed settlement (OpenViking commits
sessions/{sid} synchronously; Supermemory flushes pending turns stamped with the
session id), and at the previous head it ran AFTER the lease release — a
successor admitted during the linger could be mid-turn on the session while the
predecessor committed its remote memory session underneath.

Move the shutdown into phase 1 (after the durable flush — #88583's failure
ordering — before the finalize hook's slot in ownership). It is idempotent
(_memory_provider_shutdown), so _run_cleanup's post-release repeat is a no-op.

Also correct the docstring: phase-1 settlement is best-effort, not a proven
invariant — failures are logged and the release still happens, because an
exited process pinning the lease is worse than a dangling row (session_recovery
reaps stale rows).

Adds a discriminating test: real lease, recording agent stub, successor
acquires during the linger — memory_commit must precede release precedes
successor_acquired. Order tests now pin flush → finalize → memory → release →
linger → teardown.

(cherry picked from commit 06dc026126a473a08f182014d38f5213a842c8a3)
2026-09-30 09:57:20 -05:00
Zheqing Zeng
1d39a2d3d3 fix: settle session-owned state before releasing the lease
Review feedback (andrexibiza): the previous revision released the lease before
_flush_one_shot_session_store, whose unconditional end_session (WHERE id=? AND
ended_at IS NULL, no owner fence) could stamp a successor's open interval when
the successor acquired and reopened the session during the lingering process's
post-release flush.

New order: session settlement (flush + finalize hook) → lease release →
process-only linger + teardown. Early admission is preserved (release still
precedes the linger); the handoff race is closed because the predecessor has
no session-owned writes left once it releases.

Adds a two-owner regression test (real SessionDB + real lease): the linger is
patched to play a successor that acquires and reopens mid-linger; the
successor's open interval must survive the predecessor's finalize. Verified
discriminating against BOTH unsafe orders (previous PR head and origin/main).

(cherry picked from commit e0b4a34a981f6d2bbdcc97be977e64e867987b6a)
2026-09-30 09:57:16 -05:00
Zheqing Zeng
734ac474e5 fix(cli): release the one-shot session lease before the exit linger
A one-shot CLI run (hermes chat -Q, the Bot Mode delivery transport)
claimed the session lease at turn start and released it only via
atexit/finalize-tail, after the bounded exit linger for
notify_on_complete children. A run whose turn had ENDED but whose
process lingered alive-but-idle (observed: 434s drain wait, 0.016s CPU
over 20 min wall) kept the lease valid — its liveness check is
(pid, create_time) — so every new delivery into the session was refused
("Refused active session") for minutes after the turn finished, and
each refused delivery spawned its own lingering process tree
(#118826).

_finalize_single_query runs only after every turn of the run (main
turn, kanban goal loop, notify-completion follow-ups) is done, and none
of its remaining steps — the linger wait, the durable flush, cleanup —
runs a turn. The lease means "a turn may run on this session", so
release it FIRST: the boundary the one-shot path already declares ("the
exit linger is NOT part of the spawner's delivery", #113608). Cleanup
failure propagation is unchanged, and a cleanup failure can now never
skip the release.

(cherry picked from commit 438c68b80898efb9a287d39da32a94b772316d4e)
2026-09-30 09:57:11 -05:00
teknium1
17a137d8a3 fix(tui-gateway): a SIGTERM-ignoring command no longer survives the gateway's SIGTERM exit
The SIGTERM handler arms a 1s os._exit timer, then runs _shutdown_sessions: a flush of up to
5s, then _stop_turns_before_exit, whose kill was the graceful TERM, wait 1s, KILL. A command
that ignores SIGTERM was still alive when the timer fired, and os._exit left it reparented to
init (live: `trap '' TERM; sleep 3600` survived a SIGTERM to `python -m tui_gateway.entry`).

- kill_live_foreground_processes(now=True): SIGKILL each in-flight foreground tree at once,
  no TERM grace, no wait (BaseEnvironment._force_kill_process; LocalEnvironment kills the
  recorded process group, never our own).
- The grace timer's exit (entry._hard_exit) runs it before os._exit.
- _stop_turns_before_exit SIGKILLs whatever is still alive halfway through its settle budget
  (it ignored the interrupt's TERM), so the tool call still ends with a result the teardown
  persists instead of a dangling tool_call in state.db.
- The other hard exits that skip cleanup do the same before os._exit: the serve parent-death
  watchdog, the CLI exit watchdog, the kanban worker's SIGTERM path, and the messaging
  gateway's shutdown and loop-liveness watchdogs.
- Deflake test_shutdown_mid_tool_kills_the_command_and_keeps_its_result: the 0.5s settle
  budget was too tight under -n 40 (1 red in 9 runs); the join returns when the turn ends.
2026-09-23 17:09:54 -07:00
teknium1
2ca53fc386 refactor(cli): move cli.py module-level helper clusters into topical siblings; cli.py lands under 2,000 lines (#116911)
Mechanical, behaviour-neutral extraction along the existing hermes_cli/cli_*.py
pattern. Six clusters of module-level helpers leave cli.py:

- cli_config_load.py     prefill messages, reasoning/service-tier parsing, terminal
                         env mirroring, CLI defaults + YAML merge, logging bootstrap
- cli_render.py          reasoning-tag stripping, ANSI/skin colours, light-mode
                         detection, markdown/final rendering, output-history
                         recording, _cprint, ChatConsole, compact banner, panel wrap
- cli_terminal_input.py  file drops/attachments, bracketed-paste patch, extended
                         Enter keys, CPR guards, TUI input height, query images
- cli_shutdown.py        process session-id sync, exit watchdog, cleanup steps,
                         session-finalize notifications, one-shot finalize
- cli_single_query.py    kanban goal loops, exit-code mapping, quiet -q runner,
                         image routing, signal handlers, single-query mode
- cli_auto_maintenance.py state-db / checkpoint startup maintenance

Every moved body is AST-identical to the base copy modulo two mechanical
edits: cli-level names are late-bound with a call-time `from cli import ...`
(the cli_init_mixin pattern) and mutable cli state is read through `_cli().NAME`
(the gateway_service_unit `_gw()` pattern), so every monkeypatch seam on the
`cli` facade still intercepts moved code and no read snapshots stale state.
Mutable module state and every `global`-writing function stay in cli.py; the
facade re-exports the moved names in one import block.

test_bracketed_paste_timeout AST-loads the paste helper from its new home.
cli.py 3959 -> 1820 lines.
2026-09-20 20:42:17 -07:00