Stage 1: Front-end Admission and Lease
Course: Hermes Agent Source Code Class · Stage Exam Prerequisite: Stage 0 Corresponding Overview:
turn-lifecycle.mdSections 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.”
1. What this Phase Solves
Section titled “1. What this Phase Solves”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 + deliveryThis 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.”
2. First, remember a decision
Section titled “2. First, remember a decision”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.
3. Source Code Main Flow
Section titled “3. Source Code Main Flow”- The classic CLI submits
hermes_cli/gateway_agent.pyprompt.submitand consumes events; it does not execute a turn. - The Gateway first performs
SessionAuthority.submit()session:submitauthentication, then enters_submit()under a session mutation lock. _submit()validates the intent/payload, normalizes attachments, surface, display, finite, CLI turn and other fields, and callsadmit_session_input()to commit the durable row.- Submissions from the same session enter a durable FIFO; draining according to
seqonly takes the head of the queue, avoiding overlapping writes of the transcript by two turns. TurnRunnerbinds the delivery adapter, streaming callback, approval, and tool progress; the realAIAgentis still provided by the session runtime.TurnFacadeMixin.run_conversation()The first key step isadmit_durable_turn_lease(); if it fails, an early result is returned.DurableTurnLeaserefreshes in the background approximately every60s, with TTL of300s; waiting limit1800s.- Ended executions
finallysequentially stop refreshing, join, clear interrupts generated by their own lease, and release the lease.
4. Key Objects and Contracts
Section titled “4. Key Objects and Contracts”| 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.
5. An Observable Chain
Section titled “5. An Observable Chain”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 → deliveryKey Observation Points:
- No model call at
queued; - The owner is already executed at
started, but tool side effects may not occur; unknownNot automatic failure, but a “must audit” paused state;- lease Normal release only releases its own holder and cannot clear successors.
6. Experiment
Section titled “6. Experiment”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.
Experiment C: Verifying the holder fence
Section titled “Experiment C: Verifying the holder fence”The old holder directly calls release or refresh, then the new holder obtains the lease for the same session.
Experimental Reference Answers
Section titled “Experimental Reference Answers”- Experiment A:
queuedshould be visible, and there is no new assistant transcript; This proves that “received” does not equal “executed.” - Experiment B: First, determine whether it is completed based on existing claims, generation, captured results, and transcripts; Do not directly change
unknownback toqueued, otherwise it may be repeated execution. - 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.
7. Failure and recovery
Section titled “7. Failure and recovery”| 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.
8. Pass through customs and self-test
Section titled “8. Pass through customs and self-test”- Why can’t you use only memory queues to manage input from the same session?
- What do
queued,started, andunknownrespectively mean? - Why can’t admission and turn lease be combined into one lock?
- What are the approximate TTL, wait limit, and refresh interval for leases?
- After losing a lease, why can’t you quietly let the old process continue to wrap up?
- When encountering
unknown, what is the correct first action?
Reference answers
Section titled “Reference answers”- 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.
queuedis persisted but not yet claimed;startedis that a certain owner has taken over;unknownmeans the commit/execution boundary cannot be verified, requiring readback and reconciliation. The final state is completed, failed, or canceled.- 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.
- TTL
300s, wait limit1800s, default refresh interval about60s, constrained by source code constants/configurations. - 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.
- First, readback admission, claim, generation, captured result, and transcript to check if it is completed; Then go through the
resolve_unknownclasspath to submit a definite terminal state; you cannot directly reset it to queued.
9. Source code coordinates
Section titled “9. Source code coordinates”hermes_cli/gateway_agent.pygateway/session_authority.py::SessionAuthority.submitgateway/session_authority.py::SessionAuthority.resolve_unknown_as_ownergateway/run_turn_runner.py::TurnRunneragent/turn_facade.py::TurnFacadeMixin.run_conversationagent/turn_facade_lease.py::admit_durable_turn_leaseagent/turn_facade_lease.py::DurableTurnLease.refresh_tickhermes_state.py::SessionDBtests/gateway/test_managed_turn_contract.py
10. Next Steps
Section titled “10. Next Steps”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.