Files
hermes-agent/gateway/session_worker_spawn.py
David Dudok de Wit e32a886e1d fix(worker): retain persistence and ownership through physical shutdown
Keep reserved workers able to persist during drain, join actual in-process writers before releasing a profile, and reap children created after cancelled process acquisition. Reopening a worker outbox reconciles its pending receipt; managed results retain per-turn usage and bounded live tool details.

This commit contains the physical-worker foundation only. Mutation-task and Bot mailbox lifetime tracking are separate follow-ups. The existing zero-second restart drain default is unchanged.

Validation: scripts/run_tests.sh -j 4 across 10 worker/runtime files: 26 passed, 1 POSIX-only skip on native Windows. Includes a real managed child completing its durable receipt during a configured restart drain. The unchanged upstream one-second retirement test missed its deadline once under parallel load and passed the runner retry; an isolated rerun passed both cases without retries. Logs: split-W-tests.log, split-W-retirement-rerun.log.

Review-source: helix4u, https://github.com/NousResearch/hermes-agent/pull/106742#issuecomment-6024687448
Review-source: greptile-apps, https://github.com/NousResearch/hermes-agent/pull/106742#discussion_r4211047305
Review-source: greptile-apps, https://github.com/NousResearch/hermes-agent/pull/106742#discussion_r4211047387
Review-source: greptile-apps, https://github.com/NousResearch/hermes-agent/pull/106742#discussion_r4213427126
Review-source: dokterdok, https://github.com/NousResearch/hermes-agent/pull/106742#discussion_r4211144039
Source-base: NousResearch/hermes-agent#106742 at 0b815190b2
(cherry picked from commit 6156b21c5e3fa5e9295a13d5e02caa9b959ada82)
2026-10-08 09:10:26 -07:00

37 lines
1.2 KiB
Python

"""Cancellation-safe handoff of a managed subprocess acquired off the owner loop."""
import asyncio
import threading
async def acquire_process(spawn, *args, **kwargs):
from gateway.session_managed_worker import ManagedWorker
lock = threading.Lock()
state = {'cancelled': False, 'process': None}
def create():
process = spawn(*args, **kwargs)
with lock:
cancelled = state['cancelled']
if not cancelled:
state['process'] = process
if cancelled:
ManagedWorker(process).close()
return process
pending = asyncio.create_task(asyncio.to_thread(create))
try:
process = await asyncio.shield(pending)
with lock:
state['process'] = None
return process
except asyncio.CancelledError:
with lock:
state['cancelled'] = True
process = state['process']
state['process'] = None
if process is not None:
await asyncio.to_thread(ManagedWorker(process).close)
# If creation still runs, its thread owns cleanup; consume an eventual spawn exception.
pending.add_done_callback(lambda task: task.exception() if not task.cancelled() else None)
raise