Reconcile docs with reality; fix module graph, token compare, health #1
@@ -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
|
||||
}
|
||||
|
||||
@@ -136,6 +136,76 @@ func TestRotationEmitsValidReleaseWithAnchorSHA(t *testing.T) {
|
||||
|
||||
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
|
||||
|
||||
// keyedHarnessAdapter reports Session.Harness as the harness kind ("claude"),
|
||||
// distinct from the herdr instance id ("homesrv-claude") under which it is
|
||||
// registered in AdapterFactory.Herdrs — reproducing production's real key
|
||||
// mismatch (adapters are keyed by herdr instance id; CLIAdapter.Lease sets
|
||||
// Session.Harness to the harness kind).
|
||||
type keyedHarnessAdapter struct{ fakeAdapter }
|
||||
|
||||
func (a *keyedHarnessAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) {
|
||||
return herdr.Session{Harness: "claude", PaneID: "pane-1", Worktree: worktree}, nil
|
||||
}
|
||||
|
||||
// TestAdapterResolvedByHerdrIDNotHarnessKind guards B2: AdapterFactory.Herdrs
|
||||
// is keyed by herdr instance id (e.g. "homesrv-claude"), never by the
|
||||
// harness kind Session.Harness holds (e.g. "claude"). Reconcile, expire, and
|
||||
// rotate must all resolve the adapter via Session.HerdrID (set at lease
|
||||
// time), not Session.Harness, or every one of them silently no-ops via a
|
||||
// bare Adapter-not-registered continue.
|
||||
func TestAdapterResolvedByHerdrIDNotHarnessKind(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
run(t, repo, "init")
|
||||
run(t, repo, "config", "user.email", "t@t")
|
||||
run(t, repo, "config", "user.name", "t")
|
||||
run(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
|
||||
s, err := store.Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{
|
||||
"source": "jsonl", "external_id": "1", "project": "p",
|
||||
})}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := s.Tasks()[0]
|
||||
ref, err := s.PutArtifact([]byte("handoff"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := &keyedHarnessAdapter{fakeAdapter{occupancy: .95, boundary: true, ref: ref}}
|
||||
factory := orchestrator.AdapterFactory{Herdrs: map[string]herdr.Adapter{"homesrv-claude": a}}
|
||||
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: factory, StatePath: t.TempDir() + "/sessions.json"}
|
||||
|
||||
leaseEvt, err := s.Lease(task.ID, "homesrv-claude", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Start(context.Background(), leaseEvt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go c.Monitor(ctx, .8, time.Millisecond)
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if got, ok := s.Task(task.ID); ok && got.State == domain.StateQueued {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
got, ok := s.Task(task.ID)
|
||||
if !ok || got.State != domain.StateQueued {
|
||||
t.Fatalf("rotation did not complete via herdr-id-keyed adapter: state=%v ok=%v", got.State, ok)
|
||||
}
|
||||
if a.releases == 0 {
|
||||
t.Fatalf("adapter Release was never invoked — adapter lookup used Session.Harness instead of Session.HerdrID")
|
||||
}
|
||||
}
|
||||
|
||||
// erroringBoundaryAdapter supports Face B but its probe always fails — this
|
||||
// must block release (never silently treat an unanswerable boundary check
|
||||
// as safe to interrupt), unlike an adapter that doesn't implement the
|
||||
|
||||
Reference in New Issue
Block a user