fix(orchestrator): resolve adapters by herdr instance id, not harness kind (B2)
AdapterFactory.Herdrs is keyed by herdr instance id (e.g. "homesrv-claude"), but Reconcile, expire, and rotate all looked adapters up by session.Harness (the harness kind, e.g. "claude"). In production this key never resolves, so every one of those call sites silently no-ops via a bare `continue`: orphaned panes are never killed on restart, expired leases never kill their pane, and rotation exits before it begins. Add Coordinator.adapterFor(taskID, session), matching the fallback already used correctly by refreshSessionHealth (HerdrID, then the lease's HarnessID, then Harness as a last resort), and route all four call sites through it. Regression test TestAdapterResolvedByHerdrIDNotHarnessKind registers an adapter under "homesrv-claude" and leases with Session.Harness == "claude" (reproducing the real key mismatch) and asserts rotation still fires — the existing rotation tests used a keyed-by-nothing fake adapter that matched any lookup string and so masked this bug entirely. AUDIT.md B2.
This commit is contained in:
@@ -175,6 +175,25 @@ type SessionHealth struct {
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// adapterFor resolves the herdr adapter for a session. Session.HerdrID (the
|
||||
// registered herdr instance id, e.g. "homesrv-claude") is authoritative;
|
||||
// Session.Harness (the harness kind, e.g. "claude") is only a fallback for
|
||||
// sessions persisted before HerdrID was tracked. Adapters are keyed by
|
||||
// instance id, so falling back to the lease's harness id (recorded on the
|
||||
// task) rather than the kind keeps this resolvable even then.
|
||||
func (c *Coordinator) adapterFor(taskID string, session herdr.Session) (herdr.Adapter, error) {
|
||||
id := session.HerdrID
|
||||
if id == "" {
|
||||
if task, ok := c.Store.Task(taskID); ok && task.Lease != nil {
|
||||
id = task.Lease.HarnessID
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
id = session.Harness
|
||||
}
|
||||
return c.Adapters.Adapter(id)
|
||||
}
|
||||
|
||||
func (c *Coordinator) MonitorHealth() MonitorHealth {
|
||||
c.healthMu.RLock()
|
||||
defer c.healthMu.RUnlock()
|
||||
@@ -218,13 +237,7 @@ func (c *Coordinator) refreshSessionHealth(ctx context.Context) {
|
||||
}
|
||||
c.healthMu.Unlock()
|
||||
for taskID, session := range sessions {
|
||||
adapterID := session.HerdrID
|
||||
if adapterID == "" {
|
||||
if task, ok := c.Store.Task(taskID); ok && task.Lease != nil {
|
||||
adapterID = task.Lease.HarnessID
|
||||
}
|
||||
}
|
||||
a, err := c.Adapters.Adapter(adapterID)
|
||||
a, err := c.adapterFor(taskID, session)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -288,7 +301,7 @@ func (c *Coordinator) Reconcile(ctx context.Context) error {
|
||||
if ok && t.State == domain.StateLeased {
|
||||
continue
|
||||
}
|
||||
if a, err := c.Adapters.Adapter(session.Harness); err == nil {
|
||||
if a, err := c.adapterFor(taskID, session); err == nil {
|
||||
_ = a.Kill(ctx, session)
|
||||
}
|
||||
delete(c.sessions, taskID)
|
||||
@@ -373,7 +386,7 @@ func (c *Coordinator) expire(ctx context.Context) ([]domain.Event, error) {
|
||||
c.mu.Lock()
|
||||
for taskID, s := range c.sessions {
|
||||
if t, ok := c.Store.Task(taskID); ok && t.State == domain.StateLeased {
|
||||
if a, ae := c.Adapters.Adapter(s.Harness); ae == nil {
|
||||
if a, ae := c.adapterFor(taskID, s); ae == nil {
|
||||
if p, ok := a.(herdr.PaneExit); ok {
|
||||
if exited, ee := p.PaneExited(ctx, s); ee == nil && exited {
|
||||
b, _ := json.Marshal(map[string]string{"reason": "pane_exited", "harness_id": s.Harness})
|
||||
@@ -394,7 +407,7 @@ func (c *Coordinator) expire(ctx context.Context) ([]domain.Event, error) {
|
||||
s, ok := c.sessions[e.TaskID]
|
||||
delete(c.sessions, e.TaskID)
|
||||
if ok {
|
||||
if a, ae := c.Adapters.Adapter(s.Harness); ae == nil {
|
||||
if a, ae := c.adapterFor(e.TaskID, s); ae == nil {
|
||||
_ = a.Kill(ctx, s)
|
||||
}
|
||||
}
|
||||
@@ -416,7 +429,7 @@ func (c *Coordinator) rotate(ctx context.Context, hard float64) {
|
||||
if !ok || task.State != domain.StateLeased {
|
||||
continue
|
||||
}
|
||||
a, err := c.Adapters.Adapter(session.Harness)
|
||||
a, err := c.adapterFor(taskID, session)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user