Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/test_proposer_yield.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,3 +758,27 @@ def test_auto_shows_a_changed_check_as_a_diff(tmp_path):
assert "health check CHANGED" in passes[1], result.stderr
assert "+# weakened" in passes[1] and "-ping -c1" in passes[1]
assert [e for e in events if e["event"] == "health_check_changed"]


def test_a_yield_whose_completion_marker_already_exists_is_refused(tmp_path):
"""Measured live 2026-10-14: an adopted chain's exit_code_file named a .rc an
earlier job had left behind; the wait ended after 0 s while the chain ran."""
stale = tmp_path / "chain.rc"
stale.write_text("0\n")
document = _yield_json(job={"mode": "launch", "argv": ["/bin/sh", "-c", "sleep 1"]},
resume_when={"kind": "exit_code_file", "path": str(stale)})
result, events = _run(tmp_path, _agent(tmp_path, _writes_yield(tmp_path, document)),
max_iter=1)
invalid = [e for e in events if e["event"] == "yield_invalid"]
assert invalid and "already exists" in invalid[0]["reason"], result.stderr
assert not [e for e in events if e["event"] == "yield_job_start"]


def test_a_fresh_marker_path_is_still_accepted(tmp_path):
marker = tmp_path / "fresh.rc"
document = _yield_json(job={"mode": "launch",
"argv": ["/bin/sh", "-c", f"sleep 1; echo 0 > {marker}"]},
resume_when={"kind": "exit_code_file", "path": str(marker)})
result, events = _run(tmp_path, _agent(tmp_path, _writes_yield(tmp_path, document)),
max_iter=1)
assert [e for e in events if e["event"] == "yield_resume"], result.stderr
22 changes: 17 additions & 5 deletions proposer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1336,17 +1336,17 @@ HEALTH_ACTIVE=""; HEALTH_PROBLEM=""

YIELD_REASON=""; YIELD_MODE=""; YIELD_JOB_CWD=""; YIELD_JOB_LOG=""
YIELD_DEADLINE=""; YIELD_PREDICATE=""; YIELD_ON_RESUME=""; YIELD_ADOPT_PID=""
YIELD_NEEDS_HEALTHY="true"
YIELD_NEEDS_HEALTHY="true"; YIELD_MARKER_PATH=""
YIELD_JOB_PID=""; YIELD_JOB_RC=""; YIELD_STARTED_AT=""; YIELD_INDEX=0
YIELD_RESUME_BLOCK=""
YIELD_ARGV=()

# The schema owner and the predicate evaluator, in one place. Bash cannot parse
# JSON and must not try; this is the same shim shape specstride_spec_* uses.
# _yield_py validate <artifact> <workdir> <allow-command>
# -> 9 lines (reason, mode, cwd, log, deadline_sec, predicate kind,
# on_resume, adopt pid, requires_healthy_infra), or exit 2 with one line
# saying what is wrong.
# -> 10 lines (reason, mode, cwd, log, deadline_sec, predicate kind,
# on_resume, adopt pid, requires_healthy_infra, explicit marker path for
# exit_code_file/file_exists), or exit 2 with one line saying what is wrong.
# _yield_py argv <artifact> <workdir> <allow-command> -> one argv entry/line
# _yield_py check <artifact> <workdir> <allow-command> <launched-pid> <rc-file>
# -> exit 0 satisfied, 1 not yet, 2 unsatisfiable
Expand Down Expand Up @@ -1462,7 +1462,8 @@ if not isinstance(needs_healthy, bool):

if cmd == "validate":
for field in (one_line(reason), mode, cwd, log, str(deadline), kind,
one_line(on_resume), adopt_pid, "true" if needs_healthy else "false"):
one_line(on_resume), adopt_pid, "true" if needs_healthy else "false",
resolve(predicate_path) if kind in ("exit_code_file", "file_exists") else ""):
print(field)
sys.exit(0)

Expand Down Expand Up @@ -1561,6 +1562,7 @@ read_yield() {
YIELD_DEADLINE="${fields[4]:-}"; YIELD_PREDICATE="${fields[5]:-}"
YIELD_ON_RESUME="${fields[6]:-}"; YIELD_ADOPT_PID="${fields[7]:-}"
YIELD_NEEDS_HEALTHY="${fields[8]:-true}"
YIELD_MARKER_PATH="${fields[9]:-}"
return 0
}

Expand Down Expand Up @@ -2401,6 +2403,16 @@ for (( i=1; i<=MAX_ITER; i++ )); do
artifact "$YIELD_ARTIFACT"
rm -f "$YIELD_ARTIFACT"
YIELD_INDEX=$(( YIELD_INDEX - 1 ))
elif [[ -n "$YIELD_MARKER_PATH" && -e "$YIELD_MARKER_PATH" ]]; then
# A completion marker that exists BEFORE the wait starts is stale: the wait
# would end at once and resume the agent on a job still running. Measured
# live 2026-10-14: an adopted chain's exit_code_file named a .rc left by an
# earlier job; "yield satisfied after 0s" while the chain took the lab down.
echo "proposer.sh: refusing the yield — its completion marker $YIELD_MARKER_PATH already exists before the job has ended (stale from an earlier job). Remove it before starting the job, name a fresh path, or use resume_when.kind pid." >&2
specstride_emit yield_invalid iter "$i" reason "completion marker already exists: $YIELD_MARKER_PATH" \
artifact "$YIELD_ARTIFACT"
rm -f "$YIELD_ARTIFACT"
YIELD_INDEX=$(( YIELD_INDEX - 1 ))
elif yield_take_job; then
YIELD_STARTED_AT="$(date +%s)"
specstride_emit pass_yield iter "$i" reason "$YIELD_REASON" predicate_kind "$YIELD_PREDICATE" \
Expand Down
Loading