fix(issue-50): review findings of PR #56 on macOS

- admission: a `submit --dry-run` record is marked dry_run and is not an
  open job of its session (review 1e25cf2f F1)
- create: the monitor-row adoption requires the row's server to be the one
  this create spawned on (review 1e25cf2f F2)
- pane lock: retry only on contention (BlockingIOError); the docs say the
  flock lasts while any fd-9 holder (the claim shell and the shim) lives
  (review 66680d9d)
- tests on macOS: real-herdr box HOMEs under /tmp (sun_path is 104 bytes),
  resolved paths, the fake claude reads non-canonical input (MAX_CANON 1024)
  and the pane is read unwrapped; lock holders exec their sleep so killing
  them frees the lock; the adoption tests name the server

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-12 09:03:08 +09:00
1 parent 320aa9f7a0
commit a9ffedac36
10 files changed
+75 -19

No files matched your search

@@ -90,6 +90,7 @@ def register_job(
transport: str = "herdr",
supersede_older_attempt: bool = False,
parent_job: Optional[str] = None,
dry_run: bool = False,
) -> str:
"""Create a new ``pending`` job record and return its id.
@@ -106,6 +107,10 @@ def register_job(
Issue #34: ``parent_job`` names the job this one was delegated from, so a
loop waiting on the parent can find and wait for it. None is a root job.
Issue #50 (review 1e25cf2f F1): ``dry_run`` marks the record of a
``submit --dry-run``, which is never dispatched, so the registry admission
does not count it as an open job of its session.
"""
job_id = job_id or generate_job_id(bits)
heartbeat_sec = max(1, int(heartbeat_sec))
@@ -152,6 +157,8 @@ def register_job(
# Issue #34: the job this one was delegated from (None = a root job)
"parent_job": parent_job,
}
if dry_run:
record["dry_run"] = True
with registry_lock(registry_dir):
if mqtt_common._job_path(job_id, registry_dir).exists():
if not supersede_older_attempt:
@@ -393,6 +400,8 @@ def _build_parser() -> argparse.ArgumentParser:
p_reg.add_argument("--reviewer", default=None)
p_reg.add_argument("--reviewer-session", default=None)
p_reg.add_argument("--max-iterations", type=int, default=5)
p_reg.add_argument("--dry-run", action="store_true",
help="the record of a submit --dry-run: never dispatched, not an open job for admission")
p_reg.add_argument("--parent-job", default=None,
help="the job this one is delegated from (Issue #34); must exist in this registry")
@@ -569,6 +578,7 @@ def main(argv: Optional[List[str]] = None) -> int:
attempt_id=args.attempt_id,
job_id=args.job_id,
parent_job=args.parent_job,
dry_run=args.dry_run,
)
print(job_id)
return 0
@@ -297,6 +297,8 @@ def open_jobs(registry_dir: str, session: str, job_id: str, now: Optional[int] =
rec = _load_record(registry_dir, other)
if rec is None or _bare(str(rec.get("agent_session") or "")) != sess:
continue
if rec.get("dry_run"): # a submit --dry-run is never dispatched (review 1e25cf2f F1)
continue
status = rec.get("status")
if status == "running":
idle = _int_field(rec, "idle_timeout_sec", DEFAULT_IDLE_TIMEOUT_SEC)
+9 -5
View File
@@ -19,9 +19,12 @@ one lock per server from the choice to the claim.
`locked`, or `timeout` once MAM_PANE_CLAIM_WAIT_S
(default 120, minimum 1) has passed
The lock is a BSD flock, which belongs to the open file the shim holds on fd 9:
it outlives this helper and ends with the shim's process, however that ends
(exit, kill -9), so a dead holder leaves nothing to steal. flock(1) is not on
The lock is a BSD flock, which belongs to the open file on fd 9: it outlives
this helper and ends when the last process holding that fd ends, however it
ends (exit, kill -9), so a dead holder leaves nothing to steal. The holders are
the shell running the claim block and the command it runs (the shim); a child
that outlived them would keep it too, so nothing long-lived may inherit fd 9 -
the herdr server is spawned with close_fds (review 66680d9d). flock(1) is not on
macOS; fcntl.flock is. Stdlib only, Python 3.9.
"""
from __future__ import annotations
@@ -67,13 +70,14 @@ def wait_s(env: Mapping[str, str]) -> int:
def acquire(fd: int, wait: float) -> str:
"""`locked`, or `timeout` when the lock was not free within `wait` s. The
deadline is on the monotonic clock; a step of the wall clock does not
stretch or cut it."""
stretch or cut it. Only contention is waited out: any other error (a bad
fd) raises, which the shim reports as an undecided lock."""
deadline = time.monotonic() + wait
while True:
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return "locked"
except OSError:
except BlockingIOError:
if time.monotonic() >= deadline:
return "timeout"
time.sleep(0.2)