Skip to content

feat(runs): admission control — execution_mode + resource_keys on delegate_task - #112

Merged
TheSmokeDev merged 1 commit into
mainfrom
feat/run-admission-control
Sep 3, 2026
Merged

feat(runs): admission control — execution_mode + resource_keys on delegate_task#112
TheSmokeDev merged 1 commit into
mainfrom
feat/run-admission-control

Conversation

@TheSmokeDev

Copy link
Copy Markdown
Owner

What

Admission control for concurrent delegation (#101). delegate_task gains two optional arguments — execution_mode ∈ {exclusive (default), parallel_read_only} and resource_keys (≤8 short strings, whitespace-collapsed + case-folded, order kept, duplicates dropped) — threaded through talk_host.run_agent into talk_runs.start_run. Two live runs that share any key never overlap unless both are parallel_read_only.

Why

start_run spawned 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

  • Check before acceptance. start_run resolves the declaration, then under _RUN_LOCK looks for a live holder (non-terminal registry entry, or a reservation mid-acceptance) sharing a key. A conflict raises AdmissionRefused BEFORE 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 as lost on reconnect.
  • Reservation closes the check-then-accept gap. The acceptance write takes the history file lock, which must not nest inside _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).
  • Refusal is a spoken result, never a hang. Tiers 2/3 (api-server, detached) return 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; the WORK_STARTED receipt says so rather than implying a fence. check_work reads out what each running job is holding.
  • TALK_TRUST_DECLARED_READ_ONLY, default false. Off: parallel_read_only is downgraded to exclusive and 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. Only 1/true/yes/on turns it on; junk keeps the fence. It is the only thing that can widen behavior.
  • Args absent = today's run byte-for-byte. No admission field on the record, no fence in either direction (asserted on the JSONL row's exact key set).
  • Malformed declarations (unknown mode, >8 keys, non-string keys, >200 chars) are refused in talk_tools._handle_delegate_task before any lane is consulted.
  • Snapshots deep-copy admission so a check_work reader 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_work readout, 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_work silent → 1 fail.

Full suite after rebase onto ab0d302: 1590 passed, 40 skipped, 5 xfailed; ruff check . clean. git diff --stat vs --ignore-all-space --stat differ by exactly one line: run_id = _accept_run(entry) re-indented into a try: block — no line-ending changes.

Docs

README: new "Two jobs, one checkout — admission control" subsection under Background work + knob row; docs/OPERATING.md agent-lanes knob row; CHANGELOG.md [Unreleased] → Added.

Ported idea from bielcarpi/hermes-live-voice (MIT) — idea only, no code.

Fixes #101

— SmokeDev

…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>
@TheSmokeDev
TheSmokeDev merged commit a270e80 into main Sep 3, 2026
11 checks passed
@TheSmokeDev
TheSmokeDev deleted the feat/run-admission-control branch September 3, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(runs): execution_mode + resource_keys admission control for concurrent delegation

1 participant