Skip to content

feat(conch): add awaiting_human_response priority-override state - #501

Open
GrowDev1 wants to merge 3 commits into
mbailey:masterfrom
GrowDev1:conch-awaiting-human-response
Open

feat(conch): add awaiting_human_response priority-override state#501
GrowDev1 wants to merge 3 commits into
mbailey:masterfrom
GrowDev1:conch-awaiting-human-response

Conversation

@GrowDev1

Copy link
Copy Markdown

Summary

Adds a new Conch lock-file state, awaiting_human_response, for the case where a voice-conversation turn is blocked on a pending human confirmation (e.g. an agent asking the user to approve a risky action) rather than an ordinary between-turns hold.

Unlike a normal hold, this state:

  • Is a priority override in _held_by_other(): it blocks all other processes' floor-acquisition attempts unconditionally, even if the original holder's pid has died — cleanup on drop/crash is left to the caller (e.g. an approval-queue's own expiry), not a pid-liveness check here.
  • Is exempt from the normal CONCH_HOLD_EXPIRY idle-refresh window, since a real human decision can take much longer than a between-turns refresh; it carries its own explicit response_deadline instead.
  • Exempts the original holder's own process (verified via pid + process-start-time, not a bare pid comparison, since pid alone is not a reliable identity across process death/reuse) so it can resume the instant the human responds.

New Conch classmethods: mark_awaiting_human_response() (enter the state), awaiting_human_response_active() (the pid-liveness-independent way to check whether the state is currently blocking), and resolve_awaiting_human_response() (explicit resume path for an early "yes" rather than waiting out the full deadline).

_check_and_clear_stale_lock() gets a matching guard so its existing dead-pid fast-fail path can't silently clear a live awaiting_human_response marker out from under this new mechanism. In converse(), skip_conch=True is now a hard no-op while awaiting_human_response_active() is set — it must never be possible to talk over a pending confirmation, no matter what the caller passed.

A payload with no awaiting_human_response key at all (every existing lock file today) hits none of the new branches — fully backward compatible with the current two-state (held True/False) shape.

Note on current scope: mark_awaiting_human_response()/resolve_awaiting_human_response() have no caller in this repo yet — this PR adds the primitive and its guard-side enforcement (the converse() skip_conch check), for a caller (e.g. an external approval/confirmation flow) to invoke going forward. Happy to adjust the framing or narrow this PR if you'd rather see it land alongside an actual call site.

Cross-platform fix included: the process-identity check backing the "original holder can resume" exemption was drafted against /proc/<pid>/stat (Linux-only) and silently failed closed on every other platform — including Windows, which the rest of this file already supports via psutil/voice_mode.file_lock. Switched to psutil.Process(pid).create_time(), which is portable and matches this module's existing liveness probes.

Verified no overlap with VM-1967 (this branch is on top of it) — the new code touches a different guard than the ConchQueue deadlock fix.

Test plan

  • New tests/test_conch_awaiting_human_response.py (35 tests) covering: normal hold/acquire regression, the new state's block/exempt/expire semantics, the converse.py skip_conch guard (extracted from real source and exercised directly), pid-reuse protection, and the resolve-guard's precondition.
  • All existing conch/converse suites still pass alongside it: test_conch.py, test_conch_cli.py, test_conch_mcp.py, test_conch_notify.py, test_conch_queue.py, test_converse_conch_queue.py, test_converse_skip_conch.py, test_vm1967_conch_deadlock_repro.py — 259/259 total, run locally on native Windows (not just WSL, confirming the cross-platform fix).

GrowDev1 added 3 commits July 16, 2026 11:47
Adds a new conch lock state, `awaiting_human_response`, for the case
where a voice-conversation turn is blocked on a pending human
confirmation (e.g. an agent asking the user to approve a risky action)
rather than an ordinary between-turns hold.

Unlike a normal hold, this state:
- Is a PRIORITY OVERRIDE in `_held_by_other()`: it blocks all other
  processes' floor-acquisition attempts unconditionally, even if the
  original holder's pid has died -- cleanup on drop/crash is the
  caller's job (e.g. an approval-queue's own expiry), not a pid-liveness
  check here.
- Is exempt from the normal `CONCH_HOLD_EXPIRY` idle-refresh window,
  since a real human decision can take much longer than a between-turns
  refresh; it carries its own explicit `response_deadline` instead.
- Exempts the ORIGINAL holder's own process (verified via pid +
  process-start-time, not a bare pid comparison, since pid alone is not
  a reliable process identity across process death/reuse) so it can
  resume the instant the human responds.

New `Conch` classmethods: `mark_awaiting_human_response()` (enter the
state), `awaiting_human_response_active()` (the correct, unconditional
way for a caller to check whether the state is currently blocking --
deliberately not built on `get_holder()`/`is_active()`, which depend on
pid-liveness and the unrelated `CONCH_LOCK_EXPIRY` staleness window),
and `resolve_awaiting_human_response()` (explicit resume path once the
human has actually answered).

`_check_and_clear_stale_lock()` gets a matching guard so its existing
dead-pid fast-fail path can't silently clear a live
`awaiting_human_response` marker out from under this new mechanism.

In `converse()` (`_converse_core`), `skip_conch=True` is now a hard
no-op while `awaiting_human_response_active()` is set -- it must never
be possible to talk over a pending safety confirmation, no matter what
the caller passed.

A payload with no `awaiting_human_response` key at all (every existing
lock file today) hits none of the new branches -- fully backward
compatible with the current two-state (`held` True/False) shape.

Note: `_process_start_time()` reads `/proc/<pid>/stat` for process
identity and is therefore Unix-only; on Windows it returns None, which
`_is_same_process()` correctly treats as "not confirmed self" (fails
closed) -- so the same-process resume exemption doesn't fire on
Windows today, though the new state's core blocking behavior is
unaffected there. Flagging for discussion given this file's own recent
Windows-portability work (fcntl -> file_lock, os.kill ->
psutil.pid_exists) -- a psutil.Process(pid).create_time()-based
identity check would close that gap and is worth a follow-up if
wanted.
_process_start_time() previously read Linux-only /proc/<pid>/stat,
returning None (fail-closed) on every other platform -- including native
Windows, which the rest of this file was already made portable for
(psutil.pid_exists, voice_mode.file_lock instead of fcntl/os.kill). That
meant the same-process resume exemption in _is_same_process() silently
never fired outside Linux: the very process that called
mark_awaiting_human_response() could never resume its own turn, exactly
the HIGH-severity bug the round-2 audit fix was built to prevent,
reintroduced on non-Linux platforms by the round-3 pid-reuse-protection
fix. Caught by running the ported test suite below natively on Windows,
not assumed from the prior commit's own documented "flagging for
discussion" note.

Switches to psutil.Process(pid).create_time() -- an epoch-seconds float,
stable and portable, matching this module's existing psutil-based
liveness probes elsewhere in the file.

Adds tests/test_conch_awaiting_human_response.py (34 tests) ported from
the original design/validation suite, adapted to this repo's current
conch.py/converse.py structure: real subprocess pids instead of a
hardcoded pid=1 (not guaranteed to exist on Windows), imports the
installed voice_mode.conch package directly, and drops the
Thessary-internal approval_queue composition checks and the
original-vs-patched bug-repro comparison, neither of which apply to this
standalone repo. Confirmed zero regressions: all 224 existing
conch/converse-related tests plus these 34 pass together (258/258).
An independent review found the comments/docstrings from the prior two
commits leaked references that only make sense in a private downstream
project: an internal design-doc path, internal theme labels, a named
downstream project and a file that doesn't exist in this repo
(approval_queue.py), and an "independent audit round N" review-process
narrative. Rewrote every affected comment/docstring as plain, neutral API
documentation describing the WHY of each decision without any of that
context -- no behavior change.

Also adds a test for calling mark_awaiting_human_response() a second time
while already in the awaiting state (extends the deadline cleanly,
preserves identity fields, no corruption) -- a real gap the same review
pointed out, and removes an unused `import pytest` from the test file.
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.

1 participant