Skip to content

Stage 1: Front-end Admission and Lease

Course: Hermes Agent Source Code Class · Stage Exam Prerequisite: Stage 0 Corresponding Overview: turn-lifecycle.md Sections 1–4 Goal of This Stage: To clarify the dual boundary of “input first becomes a durable work item, then is executed by the holder of the unique lease.”

Pressing Enter does not mean the turn has already started. Hermes first turns the input into a durable admission, which is then taken in FIFO order by SessionAuthority, and finally guaranteed by the durable turn lease that only one writer exists per session.

Input
Authentication
→ payload normalization
→ session_admissions: queued
→ drain / claim: started
→ TurnRunner set up the stage
→ AIAgent take turn lease
Execute
→ final state + delivery

This design addresses four real-world problems: cross-process concurrency, long task restarts, duplicate submissions, and the split-brain scenario where an “old process is still alive but should not continue writing.”

Admission determines whether “this input has been received by the system,” and lease determines whether “this executor is currently qualified to continue writing.”

These two cannot be combined:

  • Admission is a fact in the persistent queue and is visible across processes;
  • Lease is a short-term single-writer authorization, which can expire, refresh, or be re-acquired by a successor.
  1. The classic CLI submits hermes_cli/gateway_agent.py prompt.submit and consumes events; it does not execute a turn.
  2. The Gateway first performs SessionAuthority.submit() session:submit authentication, then enters _submit() under a session mutation lock.
  3. _submit() validates the intent/payload, normalizes attachments, surface, display, finite, CLI turn and other fields, and calls admit_session_input() to commit the durable row.
  4. Submissions from the same session enter a durable FIFO; draining according to seq only takes the head of the queue, avoiding overlapping writes of the transcript by two turns.
  5. TurnRunner binds the delivery adapter, streaming callback, approval, and tool progress; the real AIAgent is still provided by the session runtime.
  6. TurnFacadeMixin.run_conversation() The first key step is admit_durable_turn_lease(); if it fails, an early result is returned.
  7. DurableTurnLease refreshes in the background approximately every 60s, with TTL of 300s; waiting limit 1800s.
  8. Ended executions finally sequentially stop refreshing, join, clear interrupts generated by their own lease, and release the lease.
Object/Field Meaning Invariant
Submission One logical submission Identity, session, request ID must remain stable
session_admissions Durable work item Clients cannot forge principal or producer marker
queued Submitted but not yet executed Can persist across restarts
started Claimed by an owner Only one valid claim
unknown Claim/submission boundary cannot be verified Must readback/reconcile; guessing changes is forbidden
Terminal State Completed, failed, or canceled Must be committed together with the transcript boundary
DurableTurnLease Current execution right Holder-qualified for renewal and release
generation Session execution generation Monotonic progression; cannot be reused by an old claim

holder is not just a simple session ID, but a persistent identity containing process, turn, platform, etc. This way, even if an old refresher arrives late, it cannot renew the successor’s lease.

prompt.submit
→ SessionAuthority.submit(actor, submission)
→ authorize(session:submit)
→ mutation_lock
→ _submit: payload normalization
→ admit_session_input: queued
→ drain claim: started + owner epoch
→ TurnRunner
→ AIAgent.run_conversation
→ admit_durable_turn_lease: holder + expires_at
→ loop/finalize
→ terminal transition
→ delivery

Key Observation Points:

  • No model call at queued;
  • The owner is already executed at started, but tool side effects may not occur;
  • unknown Not automatic failure, but a “must audit” paused state;
  • lease Normal release only releases its own holder and cannot clear successors.

Experiment A: Observe immediately after submission

Section titled “Experiment A: Observe immediately after submission”

Submit an input in the test library, but pause drain before the model is called. Check the admission line.

Experiment B: Simulating uncertain execution boundaries

Section titled “Experiment B: Simulating uncertain execution boundaries”

Create an exception between claim and stale recovery to enter unknown, then proceed to the official resolve/readback path.

The old holder directly calls release or refresh, then the new holder obtains the lease for the same session.

  1. Experiment A: queued should be visible, and there is no new assistant transcript; This proves that “received” does not equal “executed.”
  2. Experiment B: First, determine whether it is completed based on existing claims, generation, captured results, and transcripts; Do not directly change unknown back to queued, otherwise it may be repeated execution.
  3. Experiment C: The update of the old holder should fail due to holder mismatch; The new holder gets the new lease. Release only affects sessions/owners still recorded in the lease.
Failure Facts That Have Happened Restoration Principles
admission write failed Should not execute Returns an error, retains the release rule for captured media
Waiting for lease timeout Input has been received but not executed Returns early result; Do not treat input as consumed
Interrupted by new input while waiting The original input may only be in memory Passed to the next round of persistence
lease Refresh Lost Enforcement Rights May Have Been Transferred hard interrupt; The old owner stopped moving forward
SQLite lock lasts until expiration Next refresh won’t make up for expiration Proactively stop to protect transcript
admission unknown Completion status uncertain readback/reconcile, final state and transcript boundary submitted together
Process restart Memory callback loss Recovery from durable FIFO and lease row

Special reminder: If a LEASE is lost, the tool side effects that have already started will not automatically roll back. You must first check the tool results and external systems before deciding whether to retry.

  1. Why can’t you use only memory queues to manage input from the same session?
  2. What do queued, started, and unknown respectively mean?
  3. Why can’t admission and turn lease be combined into one lock?
  4. What are the approximate TTL, wait limit, and refresh interval for leases?
  5. After losing a lease, why can’t you quietly let the old process continue to wrap up?
  6. When encountering unknown, what is the correct first action?
  1. Because CLI, TUI, Gateway, and background tasks may operate the same session simultaneously across different processes. Memory queues cannot cross reboots, nor can they form stable cross-process sequences and idempotent judges.
  2. queued is persisted but not yet claimed; started is that a certain owner has taken over; unknown means the commit/execution boundary cannot be verified, requiring readback and reconciliation. The final state is completed, failed, or canceled.
  3. Admission manages persistent work items and FIFO ordering, with long lifecycles; lease manages short-term single-writer authorization, with TTL, refresh, and holder fences. Merging the two causes queuing facts and enforcement rights to contaminate each other.
  4. TTL 300s, wait limit 1800s, default refresh interval about 60s, constrained by source code constants/configurations.
  5. Because execution rights may have already been taken by the new owner; Continuing to write to the old process can cause double writes, duplicate tool side effects, or transcript forks. If the lease is lost, use a hard interrupt to end the old execution, and the new owner takes over according to its claim.
  6. First, readback admission, claim, generation, captured result, and transcript to check if it is completed; Then go through the resolve_unknown classpath to submit a definite terminal state; you cannot directly reset it to queued.
  • hermes_cli/gateway_agent.py
  • gateway/session_authority.py::SessionAuthority.submit
  • gateway/session_authority.py::SessionAuthority.resolve_unknown_as_owner
  • gateway/run_turn_runner.py::TurnRunner
  • agent/turn_facade.py::TurnFacadeMixin.run_conversation
  • agent/turn_facade_lease.py::admit_durable_turn_lease
  • agent/turn_facade_lease.py::DurableTurnLease.refresh_tick
  • hermes_state.py::SessionDB
  • tests/gateway/test_managed_turn_contract.py

Enter Stage 2: After obtaining the lease, how the AIAgent’s synchronization loop progresses round by round, and under what conditions it exits to finalize.