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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user