feat(runs): admission control — execution_mode + resource_keys on delegate_task - #112
Merged
Conversation
…egate_task The delegating model may declare each task `exclusive` (default) or `parallel_read_only` and name up to 8 normalized `resource_keys` it touches. Two live runs sharing a key never overlap unless BOTH are read-only; the check runs in `talk_runs.start_run` BEFORE a run id is minted, before the acceptance record is written, before the worker thread starts — a refused run burns nothing and can never surface as `lost`. Keys are reserved under `_RUN_LOCK` across the acceptance write so two tool-pool workers admitted in that gap cannot both start. The refusal is a spoken tool result naming the conflicting run and the shared key — never a hang, never a silent queue — on the api-server and detached lanes. The host-loop lane is checked against registry holders but cannot hold keys itself (Hermes's own delegation registry runs the child); its receipt says so instead of implying a fence. `TALK_TRUST_DECLARED_READ_ONLY` (default false) is the only knob that widens behavior: off, `parallel_read_only` is downgraded to `exclusive` and recorded that way; it is read at admission time, so turning it off closes overlaps admitted earlier. No keys = today's run byte-for-byte (no `admission` field on the record). Snapshots deep-copy the admission so a reader can't mutate a live holder's fence. 29 tests in tests/test_admission.py; each of five deliberate mutants (reservations invisible, knob ignored, no fence in start_run, tier-1 unchecked, check_work silent) is killed. Fixes #101 Signed-off-by: SmokeDev <degensmoke@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Admission control for concurrent delegation (#101).
delegate_taskgains two optional arguments —execution_mode∈ {exclusive(default),parallel_read_only} andresource_keys(≤8 short strings, whitespace-collapsed + case-folded, order kept, duplicates dropped) — threaded throughtalk_host.run_agentintotalk_runs.start_run. Two live runs that share any key never overlap unless both areparallel_read_only.Why
start_runspawned a worker per delegation with no overlap policy: delegate two tasks that touch the same checkout and two agents edit it at once; two deploys hit one target.How
start_runresolves the declaration, then under_RUN_LOCKlooks for a live holder (non-terminal registry entry, or a reservation mid-acceptance) sharing a key. A conflict raisesAdmissionRefusedBEFORE a run id is minted, before the acceptance row is written, before the worker thread starts — a refused run burns nothing and can never surface asloston reconnect._RUN_LOCK, so the check cannot hold the lock across it. Admitted keys are reserved under the same critical section as the check and handed to the registry entry in the same critical section as the insert; a failed acceptance releases them. Two tool-pool workers admitted in the gap cannot both start (tested with a blocked_accept_run).I can't start that yet — run 4 (audit the repo) is still running and touches the same resource ('/srv/app'); wait for it, stop it, or re-delegate without that key.Tier 1 (host agent loop inside/talk) is checked against registry holders — never started on top of one — but Hermes's own delegation registry runs the child, so this fence cannot hold it afterwards; theWORK_STARTEDreceipt says so rather than implying a fence.check_workreads out what each running job is holding.TALK_TRUST_DECLARED_READ_ONLY, default false. Off:parallel_read_onlyis downgraded toexclusiveand recorded that way (the declaration is the model's own claim, not a sandbox). The knob is read at admission time, so turning it off closes overlaps admitted earlier. Only1/true/yes/onturns it on; junk keeps the fence. It is the only thing that can widen behavior.admissionfield on the record, no fence in either direction (asserted on the JSONL row's exact key set).talk_tools._handle_delegate_taskbefore any lane is consulted.admissionso acheck_workreader cannot mutate a live holder's fence.Tests
tests/test_admission.py— 29 tests: the fence (disjoint / shared / plural refusal / read-only pairs with knob on and off / downgrade recorded / knob flipped off after admission), the declaration (normalization, cap, unknown mode), byte-for-byte compatibility (record key set, no burned id, no history row on refusal), the reservation (gap closed, failed acceptance releases), snapshot isolation +check_workreadout, and the tool → host → registry thread on all three lanes.Mutant spot-check (each mutant applied, suite run, file restored byte-identical): reservations invisible → 1 fail; knob ignored → 2 fails; no fence in
start_run→ 9 fails; tier-1 unchecked → 1 fail;check_worksilent → 1 fail.Full suite after rebase onto
ab0d302: 1590 passed, 40 skipped, 5 xfailed;ruff check .clean.git diff --statvs--ignore-all-space --statdiffer by exactly one line:run_id = _accept_run(entry)re-indented into atry:block — no line-ending changes.Docs
README: new "Two jobs, one checkout — admission control" subsection under Background work + knob row;
docs/OPERATING.mdagent-lanes knob row;CHANGELOG.md[Unreleased]→ Added.Ported idea from bielcarpi/hermes-live-voice (MIT) — idea only, no code.
Fixes #101
— SmokeDev