Reconcile docs with reality; fix module graph, token compare, health #1

Open
kami wants to merge 216 commits from webui-and-audit-reconciliation into master
3 changed files with 114 additions and 17 deletions
Showing only changes of commit ae4d3a57bc - Show all commits
+28 -17
View File
@@ -80,6 +80,7 @@ func main() {
}
}
mux := http.NewServeMux()
providerHealth := map[string]*provider.Supervisor{}
surface := func(r *http.Request) authz.Surface {
v := authz.ParseSurface(r.Header.Get("X-Orchestra-Surface"))
if v == "" {
@@ -321,29 +322,39 @@ func main() {
}
json.NewEncoder(w).Encode(map[string]any{"ready": ready, "checks": checks})
})
mux.HandleFunc("/v1/providers/health", func(w http.ResponseWriter, r *http.Request) {
out := map[string]provider.Health{}
for name, sup := range providerHealth {
out[name] = sup.Health()
}
json.NewEncoder(w).Encode(out)
})
if base := os.Getenv("ORCHESTRA_GITEA_URL"); base != "" {
g := provider.Gitea{BaseURL: base, Token: os.Getenv("ORCHESTRA_GITEA_TOKEN"), WebhookSecret: os.Getenv("ORCHESTRA_GITEA_WEBHOOK_SECRET"), Owner: os.Getenv("ORCHESTRA_GITEA_OWNER"), Repo: os.Getenv("ORCHESTRA_GITEA_REPO")}
mux.Handle("/v1/providers/gitea/webhook", g.WebhookHandler(s))
go func() {
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
_, e := g.Poll(ctx, s)
cancel()
if e != nil {
log.Printf("gitea poll: %v", e)
reflecting := provider.ReflectingSink{Sink: s, Tasks: s, Reflector: g}
mux.Handle("/v1/providers/gitea/webhook", g.WebhookHandler(reflecting))
sup := &provider.Supervisor{Name: "gitea", Run: func(ctx context.Context) error {
pollCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
_, err := g.Poll(pollCtx, reflecting)
if err == nil {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Minute):
}
time.Sleep(time.Minute)
}
}()
return err
}}
sup.Start(context.Background())
providerHealth["gitea"] = sup
}
if path := os.Getenv("ORCHESTRA_JSONL"); path != "" {
go func() {
ctx := context.Background()
err := (provider.JSONLWatcher{Path: path, Interval: time.Second, Provider: provider.JSONL{Source: "jsonl"}}).Run(ctx, s)
if err != nil {
log.Printf("jsonl provider: %v", err)
}
}()
sup := &provider.Supervisor{Name: "jsonl", Run: func(ctx context.Context) error {
return (provider.JSONLWatcher{Path: path, Interval: time.Second, Provider: provider.JSONL{Source: "jsonl"}}).Run(ctx, s)
}}
sup.Start(context.Background())
providerHealth["jsonl"] = sup
}
port := os.Getenv("ORCHESTRA_PORT")
if port == "" {
+84
View File
@@ -15,6 +15,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"orchestra/internal/domain"
@@ -29,6 +30,89 @@ type TaskReflector interface {
ReflectTask(domain.Task, domain.Event) error
}
type TaskLookup interface {
Task(string) (domain.Task, bool)
}
// ReflectingSink preserves the append-first rule while asynchronously
// reflecting terminal state to an external provider. Reflection failures are
// returned to the caller so the supervisor can retry and surface health.
type ReflectingSink struct {
Sink Sink
Tasks TaskLookup
Reflector TaskReflector
}
func (s ReflectingSink) Append(e domain.Event) error {
if err := s.Sink.Append(e); err != nil {
return err
}
if s.Reflector == nil || s.Tasks == nil {
return nil
}
if t, ok := s.Tasks.Task(e.TaskID); ok {
return s.Reflector.ReflectTask(t, e)
}
return nil
}
type Health struct {
Name string `json:"name"`
Running bool `json:"running"`
LastError string `json:"last_error,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
// Supervise restarts a provider loop with bounded backoff and exposes its
// current health. The loop exits only when its context is cancelled.
type Supervisor struct {
Name string
Run func(context.Context) error
Backoff time.Duration
mu sync.RWMutex
health Health
}
func (s *Supervisor) Start(ctx context.Context) {
if s.Backoff <= 0 {
s.Backoff = time.Second
}
s.mu.Lock()
s.health = Health{Name: s.Name, Running: true, UpdatedAt: time.Now().UTC()}
s.mu.Unlock()
go func() {
for {
err := s.Run(ctx)
if ctx.Err() != nil {
s.mu.Lock()
s.health.Running = false
s.health.UpdatedAt = time.Now().UTC()
s.mu.Unlock()
return
}
s.mu.Lock()
s.health.LastError = errString(err)
s.health.UpdatedAt = time.Now().UTC()
s.mu.Unlock()
t := time.NewTimer(s.Backoff)
select {
case <-ctx.Done():
t.Stop()
return
case <-t.C:
}
}
}()
}
func errString(err error) string {
if err == nil {
return "provider stopped"
}
return err.Error()
}
func (s *Supervisor) Health() Health { s.mu.RLock(); defer s.mu.RUnlock(); return s.health }
type JSONL struct{ Source string }
func (j JSONL) Ingest(r io.Reader, sink Sink) (int, error) {
+2
View File
@@ -61,6 +61,8 @@ Added bounded `POST /v1/artifacts` CAS upload support for report/handoff evidenc
The coordinator now persists active task→herdr session mappings in an atomic runtime state file, reloads them after restart, reconciles them against durable task leases, kills stale recoverable sessions, and removes orphan mappings before monitoring begins.
Provider supervision/reflection is now wired: Gitea webhook and polling use append-first task reflection, JSONL and Gitea loops restart with bounded backoff, and `/v1/providers/health` exposes running/error state. Provider lifecycle cancellation remains tied to process shutdown until the server gains a root cancellation context.
Recommended order:
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.