Skip to content

Commit 74f20d7

Browse files
committed
replay: semantic sessions never self-Suspend; serialize Suspend with knocker
Leave slots for outside eviction to archive once, and take knockMu on client Suspend/Pause so voluntary checkpoint cannot race Resume eviction.
1 parent 1313669 commit 74f20d7

2 files changed

Lines changed: 37 additions & 13 deletions

File tree

hack/replay-agent-semantic.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
1111
semantic-score (+ l1 ablation):
1212
Create (suspended) → llm_wait locally (heartbeat, no slot) →
13-
tool_loop: Resume (knock/queue) → run → keep slot (no proactive Suspend);
14-
unlocked llm_wait may be stolen by outside knockers via CP scores → …
13+
tool_loop: Resume (knock/queue) → run → keep slot (never self-Suspend);
14+
unlocked llm_wait may be stolen by outside knockers via CP scores →
15+
only eviction archives; final cleanup deletes leftovers
16+
1517
1618
1719
Compares policies via /metrics deltas (mid_tool_suspend, starvation wait, resume_wait, …)
@@ -770,9 +772,9 @@ def drive_phases_semantic(
770772
"""semantic-score path: llm_wait needs no slot until a Worker is held;
771773
tool_loop knocks via Resume.
772774
773-
After tool we do **not** Suspend. We stay on the Worker into later spans
774-
(typically unlocked llm_wait) so outside knockers + CP keepScore can steal.
775-
Session-end Suspend is only cleanup if still holding.
775+
Never self-Suspend: after tool we keep the Worker; unlocked llm_wait is
776+
how outside knockers steal (single archive on eviction). Leftovers are
777+
removed by policy-end cleanup_sandboxes.
776778
"""
777779
if not spans:
778780
spans = [{"phase": "idle", "lock": False, "t_start_ms": 0, "t_end_ms": 500}]
@@ -801,10 +803,9 @@ def drive_phases_semantic(
801803
)
802804
if dur > 0:
803805
time.sleep(dur)
804-
# Keep the slot; no proactive Suspend — waiters may steal on llm_wait.
805806
else:
806-
# llm_wait / idle: if never took a slot, stay off-Worker.
807-
# If still holding after tool, unlock + heartbeat; knockers may steal.
807+
# llm_wait / idle: heartbeat only. If holding a slot, stay unlocked
808+
# so knockers can preempt — do not Suspend ourselves.
808809
client.post_semantic(
809810
sandbox_id,
810811
{
@@ -820,8 +821,18 @@ def drive_phases_semantic(
820821
st = _sandbox_state(client, sandbox_id)
821822
if st != "running":
822823
on_worker = False
824+
# End of spans: still no Suspend. Leave unlocked so others can steal, or
825+
# stay running until run_policy cleanup deletes the sandbox.
823826
if on_worker:
824-
_try_suspend(client, sandbox_id, sandbox_locks, locks_mu)
827+
client.post_semantic(
828+
sandbox_id,
829+
{
830+
"version": "v1",
831+
"phase": "idle",
832+
"lock": False,
833+
"workflowID": workflow_id,
834+
},
835+
)
825836
return resume_sec
826837

827838

@@ -900,6 +911,8 @@ def run_one_session(
900911
l3,
901912
)
902913
job.resumed = True
914+
# Eviction may have Suspended us; never self-Suspend.
915+
job.suspended = _sandbox_state(client, job.sandbox_id) == "suspended"
903916
else:
904917
log(f"[session {index+1}/{total}] resume {job.sandbox_id[:8]}…")
905918
job.resume_sec = l3.resume_and_observe(client, job.sandbox_id)
@@ -914,13 +927,17 @@ def run_one_session(
914927
sandbox_locks,
915928
locks_mu,
916929
)
917-
job.suspended = True
930+
job.suspended = True
918931
log(f"[session {index+1}/{total}] done {sid_label}")
919932
except Exception as e: # noqa: BLE001 — per-session errors collected in report
920933
job.error = f"{sid_label}: {e}"
921934
log(f"[session] FAIL {job.error}")
922-
if job.sandbox_id:
923-
_try_suspend(client, job.sandbox_id, sandbox_locks, locks_mu)
935+
# Baseline may still try a best-effort Suspend; semantic never self-Suspends.
936+
if job.sandbox_id and not semantic_slots:
937+
try:
938+
_try_suspend(client, job.sandbox_id, sandbox_locks, locks_mu)
939+
except RuntimeError:
940+
pass
924941
finally:
925942
if held:
926943
inflight.release()

internal/scheduler/scheduler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,20 @@ func (s *Scheduler) ensureKnockerTurn(sb types.Sandbox) error {
520520
}
521521

522522
func (s *Scheduler) Pause(ctx context.Context, id string) (types.Sandbox, error) {
523+
s.knockMu.Lock()
524+
defer s.knockMu.Unlock()
523525
return s.pauseOrSuspend(ctx, id, false)
524526
}
525527

526528
func (s *Scheduler) Suspend(ctx context.Context, id string) (types.Sandbox, error) {
527-
return s.suspendLocked(ctx, id)
529+
// Serialize with Resume eviction so the same sandbox is checkpointed once.
530+
s.knockMu.Lock()
531+
defer s.knockMu.Unlock()
532+
return s.pauseOrSuspend(ctx, id, true)
528533
}
529534

535+
// suspendLocked checkpoints a running sandbox. Caller must hold knockMu
536+
// (Resume knocker path) so voluntary Suspend and eviction cannot race.
530537
func (s *Scheduler) suspendLocked(ctx context.Context, id string) (types.Sandbox, error) {
531538
return s.pauseOrSuspend(ctx, id, true)
532539
}

0 commit comments

Comments
 (0)