feat(orchestrator): soft occupancy threshold requests handoff early (S11 partial)

Coordinator.Soft (default 0.55, ORCHESTRA_OCCUPANCY_SOFT) makes rotate()
and TurnDecision request a handoff advisory-only once occupancy crosses
the soft threshold, well before Hard forces a release/rotation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 23:40:46 +04:00
parent 8b4955a687
commit 84d09ce114
5 changed files with 116 additions and 4 deletions
+54 -2
View File
@@ -180,6 +180,23 @@ type Coordinator struct {
// counterpart driven by the Face-B stop hook) evaluates the same
// threshold rather than needing its own copy passed in by the caller.
Hard float64
// Soft is the advisory occupancy threshold (spec §5.3: "soft ~55%
// threshold") at which TurnDecision starts asking the agent to prepare a
// handoff — non-blocking, doesn't require a turn boundary — well before
// Hard forces one. Zero means "use the package default" (see
// defaultSoft), so existing callers that never set this field keep
// working unchanged.
Soft float64
}
// defaultSoft is used whenever Coordinator.Soft is unset (zero value).
const defaultSoft = 0.55
func (c *Coordinator) soft() float64 {
if c.Soft > 0 {
return c.Soft
}
return defaultSoft
}
type MonitorHealth struct {
@@ -538,7 +555,23 @@ func (c *Coordinator) rotate(ctx context.Context, hard float64) {
}
reason := "threshold"
occupancy, err := a.Occupancy(session)
if err != nil || occupancy < hard {
if err != nil || occupancy < c.soft() {
continue
}
if occupancy < hard {
// Soft threshold (§5.3): request a handoff early, advisory only
// — no release, no turn-boundary requirement.
if requester, ok := a.(herdr.HandoffRequester); ok {
if _, statErr := os.Stat(filepath.Join(session.Worktree, herdr.HandoffFile)); statErr != nil && !session.HandoffRequested {
if reqErr := requester.RequestHandoff(ctx, session); reqErr == nil {
session.HandoffRequested = true
c.mu.Lock()
c.sessions[taskID] = session
_ = c.saveSessionsLocked()
c.mu.Unlock()
}
}
}
continue
}
// Face B is treated as required, not best-effort (spec §5.2/§5.3):
@@ -639,9 +672,28 @@ func (c *Coordinator) TurnDecision(ctx context.Context, taskID string) (string,
if err != nil {
return "", fmt.Errorf("orchestrator: occupancy: %w", err)
}
if occupancy < c.Hard {
if occupancy < c.soft() {
return TurnContinue, nil
}
if occupancy < c.Hard {
// Soft threshold (§5.3): advisory only. Ask the agent to start
// preparing a handoff well before Hard forces one, but don't block
// the turn on a boundary check — the agent is free to keep working.
if requester, ok := a.(herdr.HandoffRequester); ok {
if _, statErr := os.Stat(filepath.Join(session.Worktree, herdr.HandoffFile)); statErr != nil {
if !session.HandoffRequested {
if reqErr := requester.RequestHandoff(ctx, session); reqErr == nil {
session.HandoffRequested = true
c.mu.Lock()
c.sessions[taskID] = session
_ = c.saveSessionsLocked()
c.mu.Unlock()
}
}
}
}
return TurnPrepareHandoff, nil
}
if boundary, ok := a.(herdr.TurnBoundary); ok {
atBoundary, boundaryErr := boundary.AtTurnBoundary(ctx, session)
if boundaryErr != nil {
+20
View File
@@ -218,6 +218,26 @@ func TestTurnDecision(t *testing.T) {
}
})
t.Run("prepare_handoff at soft threshold, below hard, without a turn boundary", func(t *testing.T) {
a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .6, boundary: false}}
c, st, task := newCoordinator(&a.fakeAdapter)
c.Adapters = adapters{a}
decision, err := c.TurnDecision(context.Background(), task.ID)
if err != nil {
t.Fatal(err)
}
if decision != orchestrator.TurnPrepareHandoff {
t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff)
}
if a.requests == 0 {
t.Fatal("RequestHandoff was never invoked at the soft threshold")
}
got, ok := st.Task(task.ID)
if !ok || got.State != domain.StateLeased {
t.Fatalf("task state=%v ok=%v, want still leased (soft threshold is advisory)", got.State, ok)
}
})
t.Run("prepare_handoff requests handoff without releasing", func(t *testing.T) {
a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .95, boundary: true}}
c, st, task := newCoordinator(&a.fakeAdapter)